游戏开发分享

事件总线与对象池

2026-05-24
2 分钟245 字

前言

最近事情有点多,很久没更新了,有很多东西可以讲,这里给大家挑出来讲一下项目里面常用的东西——事件总线和对象池


事件总线(EventBus)

介绍

大家应该使用过Godot的信号系统,我除了必要的交互(比如按钮的控制等),其他的我都会用事件总线 简而言之,就是使用者需要订阅,这样有对象发出广播后,他就会接收到并执行方法,注意退出场景需要取消订阅,不然会发生泄露,我这里没用static的原因也是因为这个,我给他绑在了我的GameManager上,我游戏结束后移除游戏场景后,他也会自然消失

或者也可以在切换场景的时候ClearAll

代码

public class EventBus
{
    //事件中转站
    private readonly Dictionary<Type, List<Delegate>> _subscribers = new();       //事件组

    public void Subscriber<T>(Action<T> handler)     //订阅事件
    {
        var type = typeof(T);
        if (!_subscribers.ContainsKey(type))
            _subscribers[type] = new List<Delegate>();
        _subscribers[type].Add(handler);
    }

    public void Publish<T>(T evt)        //广播事件
    {
        var type = typeof(T);
        if (!_subscribers.ContainsKey(type)) return;

        foreach (var b in _subscribers[type])
            (b as Action<T>)?.Invoke(evt);
    }

    public void UnsubScriber<T>(Action<T> handler)       //取消订阅
    {
        var type = typeof(T);
        if (_subscribers.ContainsKey(type))
            _subscribers[type].Remove(handler);
    }

    public void ClaerAll()       //清空事件组
    {
        _subscribers.Clear();
    }
}

核心就是一个Dictionary<Type, List<Delegate>>,用事件的类型当 Key,把所有订阅者存进去。用Type而不是字符串的好处是编译期就能确定,改名不会出运行时Bug

使用方式

这里我用选择建筑代码给大家举例 先定义一个事件,可以用类或者结构体:

public class Event_UI_SelectBuild       //选择建筑
{
    public BaseBuild chooseBuild { get; set; }      //这里是传了我的建筑对象
}

GameController鼠标点中建筑后发出事件,UI订阅后自己去更新面板,两边互不认识

//GameController 发布,选中时带上建筑数据,取消选中传空
eventBus.Publish(new Event_UI_SelectBuild { chooseBuild = build });
eventBus.Publish(new Event_UI_SelectBuild() { });

//UI订阅,判断 chooseBuild是否为空决定显示还是隐藏面板
eventBus.Subscriber<Event_UI_SelectBuild>(OnSelectBuild);

private void OnSelectBuild(Event_UI_SelectBuild evt)
{
    if (evt.chooseBuild == null) { /*隐藏面板*/ }
    else { /*显示面板填入建筑数据*/ }
}

如果是static,场景切换时调一下 ClaerAll 清空就好,我绑游戏场景了,场景没了事件总线也就没了


对象池(ObjectPool)

介绍

对象池的思路是:节点不销毁,隐藏起来,下次直接取出来复用,用来搞投掷物或者重复的实例比较好

这里我也是直接绑在了我的游戏场景上,因为还需要用场景树添加场景

public partial class ObjectPool : Node2D
{
    private Dictionary<string, Queue<Node>> pools = new();

    public T Get<T>(string key, PackedScene scene) where T : Node       //获取对象
    {
        Node obj = null;

        if (pools.ContainsKey(key) && pools[key].Count > 0)
            obj = pools[key].Dequeue();
        else
            obj = scene.Instantiate();

        //激活
        if (!obj.IsInsideTree())
            AddChild(obj);

        obj.ProcessMode = ProcessModeEnum.Inherit;
        obj.Set("visible", true);

        return obj as T;
    }

    public void Return(string key, Node obj)     //返回对象
    {
        if (obj == null || !obj.IsInsideTree()) return;
        obj.Set("visible", false);
        obj.ProcessMode = ProcessModeEnum.Disabled;

        if (!pools.ContainsKey(key))
            pools[key] = new Queue<Node>();
        pools[key].Enqueue(obj);
    }
}

一个Get一个Return,代码很简单

使用方式

//取出
var bullet = ObjectPool.Get<Bullet>("bullet", bulletScene);
bullet.Position = muzzlePosition;

//归还,不要QueueFree移除
ObjectPool.Return("bullet", bullet);

结尾

大家可以根据自己需要进行修改,我这里还有很多可以完善的,比如可以给对象池进行预热,让他提前生成一些,不会因为忽然大量生成导致卡顿之类的,这里就是给大家分享一些游戏基础,其实有很多知识可以讲的,之后有时间我再写个文章整理一下,感谢观看

许可协议: CC BY-SA 4.0 。转载请注明出处,允许商用;改编/转载须以相同许可(CC BY-SA 4.0)发布。如有问题请联系我。