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.缺乏与对象的耦合。需要多余代码构建完整存取流。
优点: 轻量,灵活。