2D游戏当中,基本都会有玩家和场景物体交互的功能,简单的可以表示成下面几张图的样子(红色方块是玩家对象):
玩家与场景物体不接触 玩家与场景物体接触后触发提示 玩家与场景物体发生交互 上述演示效果很直观的体现了,玩家/场景交互功能大概有以下的几点需求:场景物体与玩家物体发生碰撞后会触发提示,解除碰撞后提示消失;交互之后,提示消失;为了组件的复用,场景物体,玩家物体和提示框之间应相互独立,代码中不允许获得对方的引用。我们首先创建提示框、场景物体和玩家对象。
1. 创建一个提示框对象
我们的需求是,玩家对象与场景物体接触后才会触发提示,那么提示框就应当作为Prefab对象被动态的生成出来,所以我们先构造一个提示框Prefab对象。如下图所示,修改渲染的层级顺序 (我这里没改Layer,而是把同级Layer下的序号改成了2,同级别的Layer下,序号大的会覆盖序号小的);
2. 创建场景物体对象
场景物体可以根据需求直接预设在世界坐标系中,所以我们直接在场景中新建一个Sprite,命名为“Chest”,并添加BoxCollider2D组件。如下图修改对应组件的参数,将此物体的渲染序号设为0,并设置碰撞盒为触发型。另外,再新建两个脚本:Interlocutor和Chest; 3. 创建玩家对象
与创建场景物体类似,我们直接在场景中新建一个Sprite,命名为“Player”,并添加Rigidbody2D和BoxCollider2D两个组件。修改渲染的序号,使之渲染顺序介于提示框和场景对象之间,并将物体的碰撞盒设置为触发型(实际上不设置不影响结果,两个碰撞体中只要有一个设置了Trigger就可以了)。
之后,新建一个名为“Player Controller”的脚本文件(代码在最后一节),并绑定跟随相机。
我们先回顾一下Unity的生命周期,这里有几个碰撞相关的重要方法:
OnTriggerEnter2D 。碰撞体设置为Trigger时,物体检测到碰撞时触发;OnTriggerStay2D。碰撞体设置为Trigger时,物体处于碰撞状态时触发;OnTriggerExit2D。碰撞体设置为Trigger时,物体解除碰撞时触发;OnCollisionEnter2D。碰撞体不是Trigger时,物体检测到碰撞时触发;OnCollisionStay2D。碰撞体不是Trigger时,物体处于碰撞状态时触发;OnCollisionExit2D。碰撞体不是Trigger时,物体解除碰撞时触发;很显然,我们要实现接触时显示提示框,不接触时隐藏提示框的需求,只需要调用OnTriggerEnter2D和OnTriggerExit2D两个方法就可以了。部分代码如下:
/// <summary> /// 当触发器检测到物理接触时触发。 /// </summary> /// <param name="collision">碰撞物体对象。</param> private void OnTriggerEnter2D(Collider2D collision) { if (Finished) return; if (collision.tag == reactTag) tip.SetActive(true); } /// <summary> /// 当触发器解除物理接触时触发。 /// </summary> /// <param name="collision">碰撞物体对象。</param> private void OnTriggerExit2D(Collider2D collision) { if (collision.tag == reactTag) tip.SetActive(false); }第一节中,我们为场景物体新建了一个“Interlocutor”组件类,它的功能就是要触发交互方法,改变物体属性,且能够在不改代码的前提下能给多个不同的物体使用。当然,很多策略都可以改变一个物体的属性,只不过当物体的数量和种类不断增加的时候,代码复用性会极大下降。所以,我们必须找到一种独立、高效的手段实现物体交互的功能。好在,Unity已经为我们提供了这样一个神器——UnityEvent。UnityEvent相当于Android里面绑定的Listener或者是.NET里面的事件委托,只不过使用UnityEvent时经常不需要用代码绑定事件,因为UnityEditor已经帮我们做完了这部分工作。
UnityEvent中有一个非常重要的方法——Invoke(),它的作用就是直接触发已经绑定好了的Unity事件(UGUI里,Button的触发就是这样实现的)。我们可以这样定义一个UnityEvent对象:
private UnityEvent keyDownEvent = null;当我们需要触发这个对象中的委托方法时,只需要如下操作即可:
keyDownEvent.Invoke();我们也不需要使用代码手动绑定事件,只需要在Inspector面板里使用传参的方式,就可以完成动态的交互事件绑定了:
Interlocutor类
using UnityEngine; using UnityEngine.Events; /// <summary> /// 交互者组件类。 /// </summary> public class Interlocutor : MonoBehaviour { #region 可视变量 [SerializeField] [Tooltip("可交互的标签。")] private string reactTag = "Player"; [SerializeField] [Tooltip("交互提示模板对象。")] private GameObject tipTemplate = null; [SerializeField] [Tooltip("是否可以重复交互。")] private bool repeat = false; [SerializeField] [Tooltip("交互事件按键。")] private KeyCode keyDownCode = KeyCode.F; [SerializeField] [Tooltip("交互事件列表。")] private UnityEvent keyDownEvent = null; #endregion #region 成员变量 private GameObject tip = null; #endregion #region 属性控制 /// <summary> /// 是否已经进行了交互。 /// </summary> public bool Finished { get; set; } = false; #endregion #region 基础私有方法 /// <summary> /// 第一帧调用之前触发。 /// </summary> private void Start() { tip = Instantiate(Resources.Load(tipTemplate.name) as GameObject); tip.SetActive(false); } /// <summary> /// 帧刷新时触发。 /// </summary> private void Update() { if (!tip.activeSelf || Finished) return; if (Input.GetKeyUp(keyDownCode)) { // 触发方法 keyDownEvent.Invoke(); // 运行重复触发将不回收对象 if (!repeat) Finished = true; tip.SetActive(false); } } /// <summary> /// 当触发器检测到物理接触时触发。 /// </summary> /// <param name="collision">碰撞物体对象。</param> private void OnTriggerEnter2D(Collider2D collision) { if (Finished) return; if (collision.tag == reactTag) tip.SetActive(true); } /// <summary> /// 当触发器解除物理接触时触发。 /// </summary> /// <param name="collision">碰撞物体对象。</param> private void OnTriggerExit2D(Collider2D collision) { if (collision.tag == reactTag) tip.SetActive(false); } #endregion }Chest类
Chest类是与“Chest”物体绑定的功能类,专门用来操纵箱子的状态,我们这里的功能是简单的改变箱子的Sprite,其完整代码如下:
using UnityEngine; /// <summary> /// 宝箱控制器脚本类。 /// </summary> public class Chest : MonoBehaviour { #region 可视变量 [SerializeField] [Tooltip("开宝箱后的贴图。")] private Sprite opened = null; #endregion #region 基础公有方法 /// <summary> /// 打开宝箱。 /// </summary> public void Open() { gameObject.GetComponent<SpriteRenderer>().sprite = opened; } #endregion }Player Controller类
using UnityEngine; /// <summary> /// 玩家控制器脚本类。 /// </summary> public class PlayerController : MonoBehaviour { #region 可视变量 [SerializeField] public Camera playerCamera = null; // 角色跟随相机 #endregion #region 成员变量 [HideInInspector] private float cameraDepth = -10; // 相机深度 #endregion #region 基础私有方法 /// <summary> /// 脚本实例化后立即触发。 /// </summary> private void Awake() { // 配置相机 cameraDepth = playerCamera.transform.position.z; } /// <summary> /// 帧刷新时触发。 /// </summary> private void Update() { // 移动物体 if (Input.GetKey(KeyCode.W)) gameObject.transform.Translate(2 * Vector2.up * Time.deltaTime); else if (Input.GetKey(KeyCode.S)) gameObject.transform.Translate(2 * Vector2.down * Time.deltaTime); else if (Input.GetKey(KeyCode.D)) gameObject.transform.Translate(2 * Vector2.right * Time.deltaTime); else if (Input.GetKey(KeyCode.A)) gameObject.transform.Translate(2 * Vector2.left * Time.deltaTime); // 重定位相机 SetCameraPosition(gameObject.transform.position); } /// <summary> /// 设定相机位置。 /// </summary> /// <param name="position">新的相机位置。</param> private void SetCameraPosition(Vector3 position) { playerCamera.transform.position = new Vector3(position.x, position.y, cameraDepth); } #endregion }