Unity中的输入事件可分为两种,一种是全局触发的,需要在更新每一帧来判断;还要一种是监听式触发的,就是监听点击的事件回调从而处理后面的逻辑。

全局事件

全局输入事件需要使用Input这个类,它可以监听键盘、鼠标、手势以及移动设备上的3D Touch事件等。

例如点击事件,它只能监听屏幕中的事件,并不能判断是点在了3D模型上还是点在了UI上。并且它没有提供触发事件,需要在Update()中通过每一帧去判断。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
using UnityEngine;

public class UnityInputTest : MonoBehaviour
{
void Update()
{
//按下Horizontal指定的键,并且有平滑
Input.GetAxis("Horizontal");
//按下Horizontal指定的键,并且无平滑
Input.GetAxisRaw("Horizontal");
//按下空格键
if (Input.GetKeyDown(KeyCode.Space)) { }
//抬起空格键
if (Input.GetKeyUp(KeyCode.Space)) { }
//按住空格键
if (Input.GetKey(KeyCode.Space)) { }
//按下鼠标左键,手机上则是按下屏幕
if (Input.GetMouseButton(0))
Debug.LogFormat("点击屏幕坐标{0}", Input.mousePosition);

//手指触摸屏幕中
if(Input.touchCount > 0)
{
Touch touch = Input.GetTouch(0);
//开始触摸
if(touch.phase == TouchPhase.Began) { }
//触摸移动
if(touch.phase == TouchPhase.Moved) { }
//触摸结束
if(touch.phase == TouchPhase.Ended) { }
//是否支持3D Touch
if (Input.touchPressureSupported)
Debug.LogFormat("3D Touch的力度{0}", touch.pressure);
}
}
}

射线

射线就是由某一个点向一个方向发射的一条无尽的线,它通常用来做鼠标的3D拾取。以摄像机为原点向屏幕中的一点发射射线,当发生射线碰撞时,即可拾取鼠标点击在3D世界的坐标。射线可能会碰到多个碰撞器(Collider),也可以全部拾取出来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
using UnityEngine;

public class CameraRayTest : MonoBehaviour
{

void Update()
{
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
RaycastHit hit;
if(Physics.Raycast(ray,out hit))
{
Debug.LogFormat("Raycast:{0} 3D坐标:{1}", hit.collider.name, hit.point);
}
RaycastHit[] hits = Physics.RaycastAll(ray);
foreach (RaycastHit h in hits)
{
Debug.LogFormat("RaycastAll:{0} 3D坐标:{1}", h.collider.name, h.point);
}

}
}

Physics.Raycast()表示只检测最先碰撞到的射线上的点;Physics.RaycastAll()表示检测所有碰到射线上的点。

Unity默认提供了一个层可以忽略射线。如果将游戏对象的图层设置成Ignore Raycast,此对象将不再接收射线碰撞。

点选3D模型

Unity提供了Event System组件,可以用来处理UI与3D对象的点击。它的原理也是发送射线,但是它封装得会更好一些。

首先,摄像机绑定Physics Raycaster组件,绑定后摄像机来发送射线,其中Event Mask可以过滤掉某些不需要的层。使用Physics Raycaster组件还有一个好处,如果UI一部分挡在3D模型上面,会优先响应UI事件。

物理射线投射组件

将Click3D脚本挂载需要点击的3D模型上,在统一的地方来监听并处理它们的点击事件。

Click3D脚本,相当于UI模块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.Events;

public class Click3DEvent : UnityEvent<GameObject, PointerEventData> { }

public class Click3D : MonoBehaviour, IPointerClickHandler
{
public static Click3DEvent click3DEvent = new Click3DEvent();

public void OnPointerClick(PointerEventData eventData)
{
click3DEvent.Invoke(gameObject, eventData);
}

}

处理点击事件的脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
using UnityEngine;
using UnityEngine.EventSystems;

public class ClickEventCatch : MonoBehaviour
{
void Start()
{
Click3D.click3DEvent.AddListener((go, ptEvent) =>
{
Debug.LogFormat("点选3D模型:{0}", go.name);
});
}
}

通过点击控制人物移动

学习了鼠标拾取,就可以点击地面来控制角色移动了,由于移动是一个过程,可以使用Vector3.MoveTowards根据步长来移动模型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
using UnityEngine;
using UnityEngine.EventSystems;

public class Move3DObject : MonoBehaviour
{
public Transform model;//模型
public TextMesh textMesh;//3D文字网格
private Vector3 m_MoveToPosition = Vector3.zero;//移动目的地

void Update()
{
if (Input.GetMouseButton(0))
{
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
RaycastHit hit;
if(Physics.Raycast(ray,out hit))
{
//面朝选择点
m_MoveToPosition = new Vector3(hit.point.x, model.position.y, hit.point.z);
model.LookAt(m_MoveToPosition);

textMesh.text = string.Format("点击位置:{0}", hit.point);
textMesh.transform.position = hit.point;
}
}
if(model.position != m_MoveToPosition)
{
//步长
float step = 5f * Time.deltaTime;
model.position = Vector3.MoveTowards(model.position, m_MoveToPosition, step);
}
}
}

控制模型移动

物理碰撞

在Unity中,3D物理碰撞和2D碰撞的用法都差不多,使用的都是Rigidbody和Collider组件。在《2D游戏开发》章节中有叙述。3D提供了一个角色控制器的组件,专门用来处理人物胶囊体碰撞。

碰撞器

3D部分一共提供了六类碰撞器

  • Box Collider:最常用的立方体碰撞器。
  • Sphere Collider:球体碰撞器。它的效率是最高的,因为球体的直径是相同的。
  • Capsule Collider:胶囊体碰撞器,适用于控制的主角。
  • Mesh Collider:网格碰撞器,根据模型网格生成的碰撞器,一般在开发中用另一套简易网格来生成碰撞器
  • Wheel Collider:轮胎碰撞器,Unity为载具轮胎物理专门提供的碰撞器。
  • Terrain Collider:地形碰撞器,可以在地形编辑时减少网格的数量,这样就能优化效率。

角色控制器

角色控制器(Character Controller)是用来控制第三人称移动的,它并不会像刚体那样被另外的刚体对象击飞,更适合于人形控制器。

它的本质是一个Capsule Collider,在官方文档中可以看到它是继承自Collider类的

绑定角色控制器后,会出现角色胶囊体,可以设置爬坡的角度以及皮肤的宽度。如果不希望模型走上坡,可以将Slope Limit设置成0或者比较小的值。

Unity官方手册提供了CharacterController.Move()方法的使用范例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class CharacterControllerMoveTest : MonoBehaviour
{
private CharacterController controller;
private Vector3 playerVelocity;
public bool groundedPlayer;
private float playerSpeed = 2.0f;
private float jumpHeight = 1.0f;
private float gravityValue = -9.81f;

private void Start()
{
controller = gameObject.GetComponent<CharacterController>();
}

void Update()
{
groundedPlayer = controller.isGrounded;
if (groundedPlayer && playerVelocity.y < 0)
{
playerVelocity.y = 0f;
}

Vector3 move = new Vector3(Input.GetAxis("Horizontal"), 0, Input.GetAxis("Vertical"));
controller.Move(move * Time.deltaTime * playerSpeed);

if (move != Vector3.zero)
{
gameObject.transform.forward = move;//move是使用了Input.GetAxis的Vector3,所以转向也能平滑转向
}

// Changes the height position of the player..
if (Input.GetButtonDown("Jump") && groundedPlayer)
{
playerVelocity.y += Mathf.Sqrt(jumpHeight * -3.0f * gravityValue);//根据重力加速度衍生公式 v的平方=2gh得到对象跳起来的初速度
}

playerVelocity.y += gravityValue * Time.deltaTime;//根据(delta)v=g(delta)t得到每一帧速度的减量,从初速度一直减去此量
controller.Move(playerVelocity * Time.deltaTime);//根据s = (delta)v*(delta)t得到每一帧移动的距离,CharacterController.Move(Vector3 motiom)的motion参数都是需要乘以deltatime的
}
}

注意使用此脚本时,Character Controller组件的Min Move Distance要设为0,否则isGrounded参数会一直为false

另外,还提供了SimpleMove()方法,它只能控制X轴和Z轴的移动。忽略沿 y 轴的速度。速度以单位/秒为单位。重力是自动应用在Y轴的。如果角色接地则返回true。建议每帧只调用一次 MoveSimpleMove。相比之下Move()方法没有重力。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
using UnityEngine;

[RequireComponent(typeof(CharacterController))]
public class CharacterControllerSimpleMoveTest : MonoBehaviour
{
public float speed = 3.0F;
public float rotateSpeed = 3.0F;

void Update()
{
CharacterController controller = GetComponent<CharacterController>();

// Rotate around y - axis
transform.Rotate(0, Input.GetAxis("Horizontal") * rotateSpeed, 0);

// Move forward / backward
Vector3 forward = transform.TransformDirection(Vector3.forward);//将对象的前方从局部空间转换为世界空间
float curSpeed = speed * Input.GetAxis("Vertical");
controller.SimpleMove(forward * curSpeed);//使用controller.SimpleMove(transform.forward * curSpeed);也可以,省略前面的转换
}
}

碰撞区域

控制胶囊体移动时,会和碰撞器发生碰撞,此时就有可能出现脚与地面的碰撞,头与房顶碰撞,身子四周与墙发生碰撞。调用Move()后,即可判断是否发生了碰撞,它会返回CollisionFlags

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class CharacterControllerMoveTest : MonoBehaviour
{
private CharacterController controller;
private Vector3 playerVelocity;
public bool groundedPlayer;
private float playerSpeed = 2.0f;
private float jumpHeight = 1.0f;
private float gravityValue = -9.81f;

private void Start()
{
controller = gameObject.GetComponent<CharacterController>();
}

void Update()
{
groundedPlayer = controller.isGrounded;
if (groundedPlayer && playerVelocity.y < 0)
{
playerVelocity.y = 0f;
}

Vector3 move = new Vector3(Input.GetAxis("Horizontal"), 0, Input.GetAxis("Vertical"));
controller.Move(move * Time.deltaTime * playerSpeed);

if (move != Vector3.zero)
{
gameObject.transform.forward = move;//move是使用了Input.GetAxis的Vector3,所以转向也能平滑转向
}

// Changes the height position of the player..
if (Input.GetButtonDown("Jump") && groundedPlayer)
{
playerVelocity.y += Mathf.Sqrt(jumpHeight * -3.0f * gravityValue);//根据重力加速度衍生公式 v的平方=2gh得到对象跳起来的初速度
}

playerVelocity.y += gravityValue * Time.deltaTime;//根据(delta)v=g(delta)t得到每一帧速度的减量,从初速度一直减去此量
controller.Move(playerVelocity * Time.deltaTime);//根据s = (delta)v*(delta)t得到每一帧移动的距离,CharacterController.Move(Vector3 motiom)的motion参数都是需要乘以deltatime的
}
private void OnGUI()
{
if (GUILayout.Button("检测碰撞"))
{
if (controller.collisionFlags == CollisionFlags.None)
print("没有发生碰撞");
if ((controller.collisionFlags & CollisionFlags.Sides) != 0)//使用了枚举位运算
print("身体和四周发生碰撞");
if (controller.collisionFlags == CollisionFlags.Sides)
print("只有身体的四周发生碰撞");
if ((controller.collisionFlags & CollisionFlags.Above) != 0)//使用了枚举位运算
print("身体和头部发生了碰撞");
if (controller.collisionFlags == CollisionFlags.Above)
print("只有头部与上面发生碰撞");
if ((controller.collisionFlags & CollisionFlags.Below) != 0)//使用了枚举位运算
print("身体和脚部发生了碰撞");
if (controller.collisionFlags == CollisionFlags.Below)
print("只有脚部发生碰撞");
}
}
}

发生碰撞

Unity只提供了上面、下面和周围这三个方面的碰撞,如果还需要检测身体的正面或者背面,需要根据自身旋转角度来计算。

碰撞检测

角色控制器只需要将脚本绑在自身,就可以监听碰撞物的信息了。

移动碰撞

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
using UnityEngine;

[RequireComponent(typeof(CharacterController))]
public class CharacterControllerMoveOnMouse : MonoBehaviour
{
CharacterController controller;

private void Awake()
{
controller = GetComponent<CharacterController>();
}

private Vector3 m_MovePosition;
void Update()
{
if (Input.GetMouseButtonDown(0))
{
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
RaycastHit hit;
if(Physics.Raycast(ray,out hit))
{
m_MovePosition = new Vector3(hit.point.x, controller.transform.position.y, hit.point.z);
controller.transform.LookAt(m_MovePosition);
}
}

//只要角色与目标的距离大于0.1米,就移动
if(Vector3.Distance(m_MovePosition,controller.transform.position) > 0.1f)
{
Vector3 backUp = controller.transform.position;
//Quaternion和一个固定向量相乘,会返回一个此向量相对于四元数旋转的向量
CollisionFlags flags = controller.Move(controller.transform.rotation * Vector3.forward * Time.deltaTime * 2f);

if((flags & CollisionFlags.Sides) != 0)
{
//发生碰撞时停下来,并且还原坐标,为了保持0.1米的距离
m_MovePosition = controller.transform.position = backUp;
}
}
}

private void OnControllerColliderHit(ControllerColliderHit hit)//MonoBehaviour自带的回调,只有在挂载了Character Controller组件的对象上可用
{
//获取控制器
CollisionFlags flags = hit.controller.collisionFlags;
//可能出现身体的四周发生碰撞,并且脚与地面也发生碰撞的现象
if ((flags & CollisionFlags.Sides) != 0)//使用了枚举位运算
Debug.LogFormat("身体和四周{0}发生碰撞", hit.collider.name);
if (flags == CollisionFlags.Sides)
Debug.LogFormat("只有身体的四周{0}发生碰撞", hit.collider.name);
if ((flags & CollisionFlags.Above) != 0)//使用了枚举位运算
Debug.LogFormat("身体和头部{0}发生了碰撞", hit.collider.name);
if (flags == CollisionFlags.Above)
Debug.LogFormat("只有头部与上面{0}发生碰撞", hit.collider.name);
if ((flags & CollisionFlags.Below) != 0)//使用了枚举位运算
Debug.LogFormat("身体和脚部{0}发生了碰撞", hit.collider.name);
if (flags == CollisionFlags.Below)
Debug.LogFormat("只有脚部{0}发生碰撞", hit.collider.name);
}
}

碰撞触发器

就像之前在2D触发器和动画触发器中学到的,我们在每一个Trigger中声明一个静态Event变量,方便添加各种Listener。

1
2
3
4
5
6
7
8
9
10
11
12
13
using UnityEngine;
using UnityEngine.Events;

public class Trigger3DEvent : UnityEvent<Collider> { }

public class TriggerListener3D : MonoBehaviour
{
public static Trigger3DEvent trigger3DEventEnter = new Trigger3DEvent();
private void OnTriggerEnter(Collider other)
{
trigger3DEventEnter.Invoke(other);
}
}

添加监听

1
2
3
TriggerListener3D.trigger3DEventEnter.AddListener( col => {
Debug.LogFormat("触发到{0}",col.name);
});

物理调试器

模型的显示和它的碰撞区域是两个不同的组件。例如,很多空气墙一类的碰撞器组件,可能会分别设置显示和真实碰撞的参数。如果碰撞器多了以后,管理起来会非常麻烦,无法统一预览所有的碰撞器。在导航菜单栏中选择Window——Analysis——Physics Debug菜单项,即可打开调试器窗口,可以批量隐藏、显示触发器和碰撞器等,还可以自定义颜色,更方便全局预览所有的物理元素。

物理调试器