效果演示
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);
}
三、使用UnityEvent触发方法
第一节中,我们为场景物体新建了一个“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
}