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 () { Input.GetAxis("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) { } 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; 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; } if (Input.GetButtonDown("Jump" ) && groundedPlayer) { playerVelocity.y += Mathf.Sqrt(jumpHeight * -3.0f * gravityValue); } playerVelocity.y += gravityValue * Time.deltaTime; controller.Move(playerVelocity * Time.deltaTime); } }
注意使用此脚本时,Character Controller组件的Min Move Distance要设为0,否则isGrounded
参数会一直为false
另外,还提供了SimpleMove()
方法,它只能控制X轴和Z轴的移动。忽略沿 y 轴的速度。速度以单位/秒为单位。重力是自动应用在Y轴的。如果角色接地则返回true。建议每帧只调用一次 Move
或 SimpleMove
。相比之下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>(); transform.Rotate(0 , Input.GetAxis("Horizontal" ) * rotateSpeed, 0 ); Vector3 forward = transform.TransformDirection(Vector3.forward); float curSpeed = speed * Input.GetAxis("Vertical" ); controller.SimpleMove(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; } if (Input.GetButtonDown("Jump" ) && groundedPlayer) { playerVelocity.y += Mathf.Sqrt(jumpHeight * -3.0f * gravityValue); } playerVelocity.y += gravityValue * Time.deltaTime; controller.Move(playerVelocity * Time.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); } } if (Vector3.Distance(m_MovePosition,controller.transform.position) > 0.1f ) { Vector3 backUp = controller.transform.position; CollisionFlags flags = controller.Move(controller.transform.rotation * Vector3.forward * Time.deltaTime * 2f ); if ((flags & CollisionFlags.Sides) != 0 ) { m_MovePosition = controller.transform.position = backUp; } } } private void OnControllerColliderHit (ControllerColliderHit hit ) { 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菜单项,即可打开调试器窗口,可以批量隐藏、显示触发器和碰撞器等,还可以自定义颜色,更方便全局预览所有的物理元素。