前言
游戏开发过程中,要实现玩家和物体之间的交互是非常常见的事情。如果在开发过程中,你希望和箱子交互触发开箱子的方法,和门交互,又触发开门的方法,实现方式其实有很多,比如继承同一个分类或者定义一个接口就是不错的方法,门和箱子都继承这个接口,然后各自重写方法接口里面的触发方法。
但是,如果我说要实现点击一个按钮,打开几个箱子同时开启几个门呢?你可能会说我先获取所有的箱子或者门,然后循环遍历执行里面对应的触发方法不就可以了?当然这种方法是可行的,但是不够优雅。今天我就分享一种使用委托和事件的方式来实现一个通用的物品的交互方式。
至于什么是委托和事件,之前文章我已经有所介绍了,感兴趣可以先去看看:
【unity小技巧】委托(Delegate)的基础使用和介绍
实现
1. InteractEvent 类:
- InteractHandler 委托: 定义了一个没有参数和返回值的委托类型。
- HasInteracted 事件: 事件,当触发时调用所有注册的委托。
方法:
- CallInteractEvent 方法: 触发
HasInteracted
事件,如果有订阅者,则调用它们。
public class InteractEvent { public delegate void InteractHandler(); public event InteractHandler HasInteracted; // 调用互动事件 public void CallInteractEvent() => HasInteracted?.Invoke(); }
2. Interact 类:
- InteractEvent interact: 这是一个
InteractEvent
类的实例,用于处理交互事件。 - Player player: 用于存储与之交互的玩家实例。
属性和方法:
- GetInteractEvent 属性: 返回
InteractEvent
实例。如果interact
为 null,会在首次访问时初始化。 - GetPlayer 属性: 返回存储的玩家实例。
- CallInteract 方法: 接受一个
Player
参数,设置player
属性为该参数,然后调用interact
的CallInteractEvent
方法。
public class Interact : MonoBehaviour { InteractEvent interact = new InteractEvent(); Player player; // 获取互动事件 public InteractEvent GetInteractEvent { get { if (interact == null) interact = new InteractEvent(); return interact; } } // 获取玩家 public Player GetPlayer { get { return player; } } // 调用互动方法 public void CallInteract(Player interactedPlayer) { player = interactedPlayer; interact.CallInteractEvent(); } }
3. Player 类:
- Update 方法: 每帧检查玩家是否按下 E 键,如果是,则调用
PlayerInteract
方法。 - PlayerInteract 方法: 创建一条射线从相机的视口中心向前发射,然后检测是否击中了特定层级(层0和层3)的物体。如果击中了一个具有
Interact
组件的物体,则调用其CallInteract
方法,并传递自身实例。
public class Player : MonoBehaviour { void Update() { // 如果玩家按下E键,进行交互操作 if (Input.GetKeyDown(KeyCode.E)) { PlayerInteract(); } } // 处理玩家交互的函数 public void PlayerInteract() { // 定义用于层0和层3的层蒙版,0和3图层都可以满足条件 var layerMask0 = 1 << 0; var layerMask3 = 1 << 3; var finalMask = layerMask0 | layerMask3; // 从屏幕中心创建一条射线 Ray ray = Camera.main.ViewportPointToRay(new Vector3(.5f, .5f, 0)); // 进行射线投射,检查在最终蒙版上是否命中物体(距离不超过15个单位) RaycastHit hit; if (Physics.Raycast(ray, out hit, 15, finalMask)) { // 从命中的物体获取Interact脚本组件 Interact interactScript = hit.transform.GetComponent<Interact>(); // 如果Interact脚本组件存在,调用其CallInteract方法并传递玩家实例 if (interactScript != null) { interactScript.CallInteract(this); } } } }
4. Chest 类:
- Interact openFromInteraction: 存储一个
Interact
类的实例,用于处理与宝箱的交互。
方法:
- OnEnable 方法: 当对象激活时,订阅
openFromInteraction
的HasInteracted
事件到OpenChest
方法。 - OnDisable 方法: 当对象禁用时,取消订阅
openFromInteraction
的HasInteracted
事件。 - OpenChest 方法: 当宝箱应该打开时调用,可以在其中添加具体的打开宝箱逻辑,例如生成掉落物品。
public class Chest : MonoBehaviour { public Interact interact; // 当对象启用时订阅交互事件 private void OnEnable() { if (interact != null) { interact.GetInteractEvent.HasInteracted += OpenChest; } } // 当对象禁用时取消订阅交互事件 private void OnDisable() { if (interact != null) { interact.GetInteractEvent.HasInteracted -= OpenChest; } } // 处理宝箱打开的函数 public void OpenChest() { // 掉落一些酷炫的东西 } }
工作流程说明:
- Player 类中的
PlayerInteract
方法检测玩家按下 E 键后,发射一条射线检测是否与可交互物体碰撞。 - 如果射线击中了具有
Interact
组件的物体,就调用其CallInteract
方法,并传递玩家实例。 Interact
类中的CallInteract
方法将玩家实例存储,并调用其内部的InteractEvent
实例的CallInteractEvent
方法,从而触发HasInteracted
事件。Chest
类中的OnEnable
方法在对象启用时订阅HasInteracted
事件,当事件触发时调用OpenChest
方法来打开宝箱。
开单个箱子
挂载脚本
效果
按钮触发打开很多箱子
我们可以让一个按钮与多个对象进行交互,挂载脚本
效果
拾取物品(传参)
带玩家参数的拾取物品功能,脚本挂载在物体预制体上即可
public class ItemPickup : MonoBehaviour { public string item; // 物品名称 public int amount; // 物品数量 public Interact interact; // 拾取物品的交互对象 private void OnEnable() { Interact getInteract = GetComponent<Interact>(); if (getInteract == null) { getInteract = gameObject.AddComponent<Interact>(); // 如果没有,则添加Interact组件 } interact = getInteract; interact.GetInteractEvent.HasInteracted += InteractPickup; } private void OnDisable() { if (interact) { interact.GetInteractEvent.HasInteracted -= InteractPickup; // 取消监听交互事件 } } public void InteractPickup() { AddItem(interact.GetPlayer); // 执行AddItem方法,传入交互对象的玩家信息 } public void AddItem(Player player) { // 给玩家添加物品的逻辑在这里实现 } }