一个简单的 Godot C# 对象池

using System.Collections.Generic;
using Godot;

namespace GoDogKit
{
    /// <summary>
    /// A simple implementation of an object pool without security checks.    
    /// </summary>
    public partial class ObjectPool : Node
    {
        /// <summary>
        /// The scene stored in the pool.        
        /// </summary>
        [Export] public PackedScene Scene { get; set; }

        /// <summary>
        /// The initial size of the pool.
        /// </summary>
        [Export]
        public int InitialSize
        {
            get => m_initialSize;
            set => m_initialSize = value < 0 ? 0 : value;
        }
        private int m_initialSize = 10;

        /// <summary>
        /// Emitted when a node is gotten from the pool.    
        /// </summary>
        /// <param name="node"> The node that was gotten.</param>
        [Signal] public delegate void GottenEventHandler(Node node);

        /// <summary>
        /// Emitted when a node is instantiated inside the pool.        
        /// </summary>
        /// <param name="node"> The node that was instantiated.</param>
        [Signal] public delegate void InstantiatedEventHandler(Node node);

        /// <summary>
        /// Emitted when a node is released back to the pool.        
        /// </summary>
        /// <param name="node"> The node that was released.</param>
        [Signal] public delegate void ReleasedEventHandler(Node node);

        /// <summary>
        /// Emitted when a node is freed from the pool.
        /// </summary>
        /// <param name="node"> The node that was freed.</param>
        [Signal] public delegate void FreedEventHandler(Node node);

        // The queue of nodes in the pool.
        private Queue<Node> queue;

        /// <summary>
        /// The number of nodes in the pool currently.       
        /// </summary>
        public virtual int Size
        {
            get => queue.Count;
        }

        public override void _Ready()
        {
            queue = new Queue<Node>();

            // Auto initialize if initial size is greater than 0
            if (InitialSize > 0)
            {
                for (int i = 0; i < InitialSize; i++)
                {
                    Node node = Scene.Instantiate();
                    queue.Enqueue(node);
                }
            }
        }

        /// <summary>
        /// Get a node from the pool as Node, and add it to the scene tree root.  
        /// If the pool is empty, a new node will be instantiated and added to the scene tree.
        /// </summary>         
        /// <returns> The node that was gotten. </returns>
        public virtual Node Get()
        {
            if (!queue.TryDequeue(out Node node))
            {
                node = Scene.Instantiate();
                EmitSignal(SignalName.Instantiated, node);
            }
            GetTree().Root.AddChild(node);
            EmitSignal(SignalName.Gotten, node);
            return node;
        }


        /// <summary>
        /// Get a node from the pool as specified Type, and add it to the scene tree root.  
        ///If the pool is empty, a new node will be instantiated and added to the scene tree root.
        /// </summary>
        /// <typeparam name="T"> Node type to get. </typeparam>
        /// <returns> The node that was gotten. </returns>        
        public virtual T Get<T>() where T : Node
        {
            if (!queue.TryDequeue(out Node node))
            {
                node = Scene.Instantiate() as T;
                EmitSignal(SignalName.Instantiated, node);
            }
            GetTree().Root.AddChild(node);
            EmitSignal(SignalName.Gotten, node);
            return node as T;
        }

        /// <summary>
        /// Release a node back to the pool and remove it from the scene tree root.
        /// Notice that there are no checks for whethet the node was creatd by the pool or not.
        /// </summary>
        /// <param name="node"> The Node to release. </param>
        public virtual void Release(Node node)
        {
            GetTree().Root.RemoveChild(node);
            queue.Enqueue(node);
            EmitSignal(SignalName.Released, node);
        }

        /// <summary>
        /// Free a node from the pool and remove it from the scene tree at the end of the frame.
        /// Notice that there are no checks for whethet the node was creatd by the pool or not.
        /// </summary>
        /// <param name="node"> The Node to free. </param>
        public virtual void Free(Node node)
        {
            node.QueueFree();
            EmitSignal(SignalName.Freed, node);
        }
    }
}

        这是一个及其简单的Godot对象池,而且还非常不安全。不过作为接触Godot的一些设计理念看来刚刚好。

        这里边唯一值得注意的点应该就是AddChild和RemoveChild了,对象池技术的本质实际上基于对实例的管理,用的时候激活,不用的时候失活。但是在Godot尽然没有明确的Enable或Disable操作,比如Unity中的。

        但是官方介绍了几种改变场景的策略和方式。其中一种就是这个对象池用的:AddChild和RemoveChild。这两种方式改变节点在场景树中的存在,但不改变它们在内存中的存在。

        也就是说它们实际上在内存中存在,不过还没进入到场景树中,也就不会被处理,所以才类似于Unity中的Enable和Disable。PackedScene实例化出来的节点也是这个状态。

        这个对象池最大的问题就是没有跟它所管理的有很好的耦合。这也就意味着我们要在它所管理的对象中至少设置好以下内容,否则会有很多奇奇怪怪的问题:

        1.首先要建立对象与对象池的依赖。否则等到后面回收时,够不清楚到底是哪个池子里的对象就很麻烦了。

        2.管理对象状态。该对象池仅仅负责存取对象,根本没有任何初始化对象状态的功能。打个比方,一颗子弹被从对象池拿出来后射了出去,但是回收它时没有进行任何设置操作,意味着它还在原来的位置或者具有原本的状态,第二次拿出来后依然保留。所以需要我们手动添加一些代码帮助对象池对对象的管理,比如拿到对象时做什么,回收对象时做什么。

        3.设置回收时机。只拿不回的对象池根本没有用,所以要设置好被管理对象的回收时机。如上文说的子弹,在Godot中有个很好用的节点叫做Timer,用来计时的,我们可以设置时间一到就马上回收子弹,不过要注意Timer的设置以及子弹状态的设置。

        值得一提的是,我们除了可以通过代码(比如C#事件:信号+=方法)的模式,还能通过在Editor直接建立信号链接的形式,这样可以直接不用写+=了。

        综上所述,该对象池的缺点是:

        1.缺乏安全检查。如判断对象是否属于自己。

        2.缺乏与对象的耦合。需要多余代码构建完整存取流。     

        优点: 轻量,灵活。 

猜你喜欢

转载自blog.csdn.net/m0_73087695/article/details/142389598