You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Unity RPG地牢游戏中基于观察者模式的事件选择性更新方案问询

嘿,这个问题我在Unity做RPG地牢游戏的时候也碰到过——单一观察者主题下全量通知确实会搞出很多没必要的性能开销,还容易让逻辑变得乱糟糟的。针对你说的场景,其实可以给观察者模式加个事件类型过滤的机制,让每个观察者只订阅自己关心的事件,触发时只通知对应类型的观察者就行。

核心思路:给事件分类,让观察者精准订阅

不要让所有观察者都盯着同一个“大主题”,而是给主题里的事件做细分,比如把事件分成PlayerShoot、EnemyTakeDamage、ItemInteract这类具体类型,每个观察者只订阅自己需要响应的事件类型,触发时只遍历对应类型的观察者列表。

具体实现步骤(Unity C#示例)

1. 定义事件类型枚举

先把你游戏里可能出现的事件都归类,用枚举清晰区分:

public enum GameEventType
{
    PlayerShoot,          // 玩家射箭事件
    EnemyDetectPlayer,    // 敌人发现玩家事件
    ItemPickup,           // 物品拾取事件
    DoorOpen              // 门打开事件
    // 可以根据需求继续扩展
}

2. 重构GameEventManager:按事件类型管理观察者

原来的单一观察者列表改成字典结构,key是事件类型,value是对应类型的观察者列表。这样触发事件时,只需要找到对应类型的列表通知就行:

public class GameEventManager : MonoBehaviour
{
    // 单例模式,方便全局调用
    public static GameEventManager Instance { get; private set; }

    // 按事件类型存储观察者列表
    private Dictionary<GameEventType, List<IGameEventListener>> _eventListeners = new Dictionary<GameEventType, List<IGameEventListener>>();

    private void Awake()
    {
        if (Instance != null && Instance != this)
        {
            Destroy(gameObject);
        }
        else
        {
            Instance = this;
            DontDestroyOnLoad(gameObject);
        }

        // 初始化所有事件类型的空列表
        foreach (GameEventType type in Enum.GetValues(typeof(GameEventType)))
        {
            if (!_eventListeners.ContainsKey(type))
            {
                _eventListeners[type] = new List<IGameEventListener>();
            }
        }
    }

    // 订阅事件:观察者指定要监听的事件类型
    public void Subscribe(GameEventType eventType, IGameEventListener listener)
    {
        if (_eventListeners.TryGetValue(eventType, out var listeners))
        {
            if (!listeners.Contains(listener))
            {
                listeners.Add(listener);
            }
        }
    }

    // 取消订阅
    public void Unsubscribe(GameEventType eventType, IGameEventListener listener)
    {
        if (_eventListeners.TryGetValue(eventType, out var listeners))
        {
            if (listeners.Contains(listener))
            {
                listeners.Remove(listener);
            }
        }
    }

    // 触发事件:只通知对应类型的观察者
    public void TriggerEvent(GameEventType eventType, object eventData = null)
    {
        if (_eventListeners.TryGetValue(eventType, out var listeners))
        {
            // 遍历副本,避免遍历中列表修改导致的异常
            foreach (var listener in listeners.ToList())
            {
                listener.OnEventTriggered(eventType, eventData);
            }
        }
    }
}

3. 定义观察者接口

让所有需要响应事件的对象实现这个接口,接口里的方法可以接收事件类型和额外数据(比如射箭时的方向、位置):

public interface IGameEventListener
{
    void OnEventTriggered(GameEventType eventType, object eventData);
}

4. 具体观察者实现(比如玩家、敌人、交互物品)

  • 敌人类:只订阅PlayerShoot事件来做出躲避/反击反应:
public class Enemy : MonoBehaviour, IGameEventListener
{
    private void OnEnable()
    {
        // 敌人只关心玩家射箭的事件
        GameEventManager.Instance.Subscribe(GameEventType.PlayerShoot, this);
    }

    private void OnDisable()
    {
        GameEventManager.Instance.Unsubscribe(GameEventType.PlayerShoot, this);
    }

    public void OnEventTriggered(GameEventType eventType, object eventData)
    {
        if (eventType == GameEventType.PlayerShoot)
        {
            // 敌人做出躲避或反击逻辑
            Debug.Log("Enemy reacts to player's shoot!");
            // 用事件数据做精准反应,比如转向射箭方向
            Vector3 shootPos = (Vector3)eventData;
            transform.LookAt(shootPos);
        }
    }
}
  • 地面交互物品:订阅PlayerShoot事件,被箭射中时触发效果:
public class InteractiveItem : MonoBehaviour, IGameEventListener
{
    private void OnEnable()
    {
        // 物品关心被箭射中的事件
        GameEventManager.Instance.Subscribe(GameEventType.PlayerShoot, this);
    }

    private void OnDisable()
    {
        GameEventManager.Instance.Unsubscribe(GameEventType.PlayerShoot, this);
    }

    public void OnEventTriggered(GameEventType eventType, object eventData)
    {
        if (eventType == GameEventType.PlayerShoot)
        {
            // 检查是否被箭击中
            RaycastHit hit = (RaycastHit)eventData;
            if (hit.collider == GetComponent<Collider>())
            {
                Debug.Log("Interactive item triggered by player's shoot!");
                // 执行物品逻辑:爆炸、触发机关等
            }
        }
    }
}

5. 触发事件的示例(玩家按下空格键射箭)

public class PlayerController : MonoBehaviour
{
    private void Update()
    {
        if (Input.GetKeyDown(KeyCode.Space))
        {
            ShootArrow();
            // 获取射箭的射线检测结果作为事件数据
            RaycastHit hit;
            if (Physics.Raycast(transform.position, transform.forward, out hit))
            {
                // 触发PlayerShoot事件,传递命中数据
                GameEventManager.Instance.TriggerEvent(GameEventType.PlayerShoot, hit);
            }
        }
    }

    private void ShootArrow()
    {
        // 生成箭、播放动画等逻辑
        Debug.Log("Player shoots arrow!");
    }
}

额外优化建议

  • 避免空事件订阅:可以在GameEventManager里加日志,提醒开发者某个事件没有观察者,方便调试。
  • 事件数据封装:如果事件数据比较复杂,可以创建专门的GameEventData类,代替object类型,避免类型转换的麻烦。
  • 性能考量:如果游戏里事件非常频繁,比如每帧都触发的事件,可以考虑用对象池缓存观察者列表的副本,减少ToList()的开销。

这样一来,当玩家按下空格键触发PlayerShoot事件时,只有订阅了这个事件类型的观察者(比如敌人、可被箭触发的物品)会收到通知并更新,完全不会影响其他无关对象(比如门、未订阅该事件的NPC),完美解决你的问题。

内容的提问来源于stack exchange,提问作者Ben Schwabe

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.19 03:47:36