如何为组件/脚本创建泛型池系统?

7
我的理解是泛型能够帮助我简化池化,但我不知道如何实现。
我的池化系统非常简单,但却很混乱。现在变得越来越难以管理和混乱。它无法良好地扩展......
我的FXDistribrutor.cs类是附加到初始场景中对象的组件,旨在永久存在于游戏的所有场景中。它具有对自身的静态引用,因此我可以轻松地从任何地方调用它。关于这个构造的更多信息请参见最后。我甚至不确定这是否是正确的方法。但它工作得很好。
FXDistributor为每种类型的FX Unit提供了一个公共插槽,以及该类型FX的池数组、数组索引和池大小。
以下是两个示例:
    public BumperFX BmprFX;
    BumperFX[] _poolOfBumperFX;
    int _indexBumperFX, _poolSize = 10;

    public LandingFX LndngFX;
    LandingFX[] _poolOfLndngFX;
    int _indexLndngFX, _poolSizeLndngFX = 5;

在Unity的Start调用中,我填充每个FX单元的池:
void Start(){

    _poolOfBumperFX = new BumperFX[_poolSize];
    for (var i = 0; i < _poolSize; i++) {
    _poolOfBumperFX[i] = Instantiate(BmprFX, transform );
    }

    _poolOfLndngFX = new LandingFX[_poolSizeLndngFX];
    for ( var i = 0; i < _poolSizeLndngFX; i++ ) {
    _poolOfLndngFX[i] = Instantiate( LndngFX, transform );
    }
}

在类的主体中,我为每种FX类型编写了一堆方法,以便将它们提供给需要的地方:

public LandingFX GimmeLandingFX ( ){
    if ( _indexLndngFX == _poolSizeLndngFX ) _indexLndngFX = 0;
    var lndngFX = _poolOfLndngFX[_indexLndngFX];
    _indexLndngFX++; return lndngFX;
}
public BumperFX GimmeBumperFX ( ) {
    if ( _indexBumperFX == _poolSize ) _indexBumperFX = 0;
    var bumperFX = _poolOfBumperFX[_indexBumperFX];
    _indexBumperFX++;   return bumperFX;
}

所以当我想要使用其中一个FX时,我可以从任何地方通过静态引用这样调用:

    FXDistributor.sRef.GimmeLandingFX( ).Bounce(
            bounce.point,
            bounce.tangentImpulse,
            bounce.normalImpulse 
            );

我该如何使用泛型方式简化此方法,使得我能够轻松地并且更加简洁地完成这样一种形式的操作,适用于几十种FX Units?

BumperFX和LandingFX有共同的父类吗? - derHugo
不完全是这样。它们都是MonoBehaviours,这是Unity基类的一种分类方式,但不是常见父类的正常意义。而且,我并不太熟悉C#中的层次结构,所以我的整个项目都是扁平的。我认为这是我需要学习克服的问题,以使用泛型。我来自Swift。 - Confused
通用池是否应该用于组件/脚本或游戏对象预制件? - Programmer
1
我没有收到你的评论通知。我的意思是当你有两个对象,例如BulletObject,GrenadeObjects时,你可以创建一个以GameObject为参数的对象池,并使用两个不同的池实例创建BulletObject和GrenadeObjects的池。你明白我的意思吗?我需要放代码给你看吗? - Programmer
@程序员 抱歉没有@你。现在让我展示我的密度。我想你的意思是创建池的函数可以使用“GameObject”,而这可以是我的任何对象,因为它们都是从“GameObjects”派生出来的,尽管它们具有更高的复杂性和脚本等。然后,池可以返回一个“GameObject”...这意味着我需要获取“GameObject”上脚本组件的引用,以便调用FX激活...我理解得对吗?如果是这样,我的唯一担忧是如何缓存查找脚本组件以实现最小化的开销。 - Confused
显示剩余6条评论
2个回答

5
在Unity中,Instantiate()Destroy()函数用于创建对象的副本,特别是预制件,并销毁它们。当涉及到对象池时,池对象通常表示为GameObject类型。当需要从池中访问组件时,首先检索池GameObject,然后使用GetComponent函数从GameObject检索组件。
阅读您的问题和评论,您想避免GetComponent部分,仅表示组件而不是GameObject,以便您也可以直接访问组件。
如果这是您想要的,那么Unity的Component就是必需的。请参阅下面所需的步骤。
请注意,当我说组件/脚本时,我指的是从MonoBehaviour派生并可附加到GameObject或内置Unity组件(如RigidbodyBoxCollider)的脚本。
1. 将组件/脚本存储到Component列表中。
List<Component> components;

2. 使用Type作为键,List<Component>作为值,将组件列表存储在字典中。这样可以更轻松、更快速地按Type分组和查找组件。

Dictionary<Type, List<Component>> poolTypeDict;

3. 其余部分非常简单。将添加或检索池项的函数设置为通用函数,然后使用Convert.ChangeType在通用类型和Component类型之间进行转换,或从通用类型转换为请求返回的任何其他类型。

4. 当需要向字典中添加项目时,请检查Type是否已存在,如果存在,则检索现有的键,使用Instantiate函数创建添加新的Component,然后将其保存到字典中。

如果Type尚不存在,则无需从Dictionary中检索任何数据。只需创建新的并将其与其Type一起添加到字典中即可。

一旦将项目添加到池中,请停用带有component.gameObject.SetActive(false)的GameObject。

5. 当你需要从池中检索一个项目时,检查Type是否存在作为键,然后检索值,即ComponentList。循环遍历组件并返回任何具有非激活的游戏对象的组件。可以通过检查component.gameObject.activeInHierarchy是否为false来检查这一点。

一旦从池中检索到项目,使用component.gameObject.SetActive(true)激活游戏对象

如果未找到组件,则可以决定返回null或实例化新组件。

6. 当你完成使用一个项目并将其回收到池中时,不要调用Destroy函数。只需使用component.gameObject.SetActive(false)使游戏对象处于非激活状态。这将使组件能够在下次搜索DictionaryList中的可用组件时被找到。

以下是脚本和组件的最小通用池系统示例:

public class ComponentPool
{
    //Determines if pool should expand when no pool is available or just return null
    public bool autoExpand = true;
    //Links the type of the componet with the component
    Dictionary<Type, List<Component>> poolTypeDict = new Dictionary<Type, List<Component>>();

    public ComponentPool() { }


    //Adds Prefab component to the ComponentPool
    public void AddPrefab<T>(T prefabReference, int count = 1)
    {
        _AddComponentType<T>(prefabReference, count);
    }

    private Component _AddComponentType<T>(T prefabReference, int count = 1)
    {
        Type compType = typeof(T);

        if (count <= 0)
        {
            Debug.LogError("Count cannot be <= 0");
            return null;
        }

        //Check if the component type already exist in the Dictionary
        List<Component> comp;
        if (poolTypeDict.TryGetValue(compType, out comp))
        {
            if (comp == null)
                comp = new List<Component>();

            //Create the type of component x times
            for (int i = 0; i < count; i++)
            {
                //Instantiate new component and UPDATE the List of components
                Component original = (Component)Convert.ChangeType(prefabReference, typeof(T));
                Component instance = Instantiate(original);
                //De-activate each one until when needed
                instance.gameObject.SetActive(false);
                comp.Add(instance);
            }
        }
        else
        {
            //Create the type of component x times
            comp = new List<Component>();
            for (int i = 0; i < count; i++)
            {
                //Instantiate new component and UPDATE the List of components
                Component original = (Component)Convert.ChangeType(prefabReference, typeof(T));
                Component instance = Instantiate(original);
                //De-activate each one until when needed
                instance.gameObject.SetActive(false);
                comp.Add(instance);
            }
        }

        //UPDATE the Dictionary with the new List of components
        poolTypeDict[compType] = comp;

        /*Return last data added to the List
         Needed in the GetAvailableObject function when there is no Component
         avaiable to return. New one is then created and returned
         */
        return comp[comp.Count - 1];
    }


    //Get available component in the ComponentPool
    public T GetAvailableObject<T>(T prefabReference)
    {
        Type compType = typeof(T);

        //Get all component with the requested type from  the Dictionary
        List<Component> comp;
        if (poolTypeDict.TryGetValue(compType, out comp))
        {
            //Get de-activated GameObject in the loop
            for (int i = 0; i < comp.Count; i++)
            {
                if (!comp[i].gameObject.activeInHierarchy)
                {
                    //Activate the GameObject then return it
                    comp[i].gameObject.SetActive(true);
                    return (T)Convert.ChangeType(comp[i], typeof(T));
                }
            }
        }

        //No available object in the pool. Expand array if enabled or return null
        if (autoExpand)
        {
            //Create new component, activate the GameObject and return it
            Component instance = _AddComponentType<T>(prefabReference, 1);
            instance.gameObject.SetActive(true);
            return (T)Convert.ChangeType(instance, typeof(T));
        }
        return default(T);
    }
}

public static class ExtensionMethod
{
    public static void RecyclePool(this Component component)
    {
        //Reset position and then de-activate the GameObject of the component
        GameObject obj = component.gameObject;
        obj.transform.position = Vector3.zero;
        obj.transform.rotation = Quaternion.identity;
        component.gameObject.SetActive(false);
    }
}

用法:

它可以采用任何预制组件脚本。之所以使用预制件,是因为池化对象通常是预制件实例化并等待使用。

示例预制件脚本 (LandingFX, BumperFX):

public class LandingFX : MonoBehaviour { ... }

public class BumperFX : MonoBehaviour { ... }

有两个变量用于保存预制件的引用。您可以使用公共变量并从编辑器中分配它们,或使用资源API加载它们。

public LandingFX landingFxPrefab;
public BumperFX bumperFxPrefab;

创建新的组件池并禁用自动调整大小。
ComponentPool cmpPool = new ComponentPool();
cmpPool.autoExpand = false;

为 LandingFX 和 BumperFX 组件创建 2 个池。它可以使用任何组件。
//AddPrefab 2 objects type of LandingFX
cmpPool.AddPrefab(landingFxPrefab, 2);
//AddPrefab 2 objects type of BumperFX
cmpPool.AddPrefab(bumperFxPrefab, 2);

当您需要从池中获取LandingFX时,可以按如下方式检索它们:
LandingFX lndngFX1 = cmpPool.GetAvailableObject(landingFxPrefab);
LandingFX lndngFX2 = cmpPool.GetAvailableObject(landingFxPrefab);

当您需要从池中检索BumperFX时,可以按如下方式检索:
BumperFX bmpFX1 = cmpPool.GetAvailableObject(bumperFxPrefab);
BumperFX bmpFX2 = cmpPool.GetAvailableObject(bumperFxPrefab);

当你使用完检索到的组件后,将它们回收到池中而不是销毁它们:
lndngFX1.RecyclePool();
lndngFX2.RecyclePool();
bmpFX1.RecyclePool();
bmpFX2.RecyclePool();

也许这是一个不相关的问题。当获取组件并将其用作池和访问的对象时,访问父级变换来定位每个池化对象是否需要大幅攀升层次结构? - Confused
抱歉,我不理解这个评论。您能换一种说法吗? - Programmer
当然。@程序员。我需要将这些池化对象中的每一个定位到它们有效影响的位置。因此,我需要在某个地方获取父级变换,并告诉它要去哪里。这样做会有很多开销吗?还是只是正常的“transform.position = goHereDoYourStuff;”? - Confused
我真的不知道这样做会对性能造成多大影响,但是改变 transform.position 而不是父级对象似乎更合理,因为改变父级对象的位置将不必要地改变/更新所有子对象的 transform。 - Programmer
1
我过于考虑了变换问题。其实非常简单。该组件可以访问其宿主的变换... 哎呀!谢谢!!! - Confused
我刚意识到我可以稍作修改,将其用于计分和圈速。谢谢你!你真是个传奇! - Confused

3

我对这个解决方案并不是很满意,但是结合一个好的对象池和简单的Dictionary<K,V>的使用可以得到以下结果:

// pool of single object type, uses new for instantiation
public class ObjectPool<T> where T : new()
{
    // this will hold all the instances, notice that it's up to caller to make sure
    // the pool size is big enough not to reuse an object that's still in use
    private readonly T[] _pool = new T[_maxObjects];
    private int _current = 0;

    public ObjectPool()
    {
        // performs initialization, one may consider doing lazy initialization afterwards
        for (int i = 0; i < _maxObjects; ++i)
            _pool[i] = new T();
    }

    private const int _maxObjects = 100;  // Set this to whatever

    public T Get()
    {
        return _pool[_current++ % _maxObjects];
    }
}

// pool of generic pools
public class PoolPool
{
    // this holds a reference to pools of known (previously used) object pools
    // I'm dissatisfied with an use of object here, but that's a way around the generics :/
    private readonly Dictionary<Type, object> _pool = new Dictionary<Type, object>();

    public T Get<T>() where T : new()
    {
        // is the pool already instantiated?
        if (_pool.TryGetValue(typeof(T), out var o))
        {
            // if yes, reuse it (we know o should be of type ObjectPool<T>,
            // where T matches the current generic argument
            return ((ObjectPool<T>)o).Get();
        }

        // First time we see T, create new pool and store it in lookup dictionary
        // for later use
        ObjectPool<T> pool = new ObjectPool<T>();
        _pool.Add(typeof(T), pool);

        return pool.Get();
    }
}

现在,你只需要简单地执行以下步骤:
pool.Get<A>().SayHello();
pool.Get<B>().Bark();

然而,这仍然有改进的空间,因为它使用new实例化类,而不是您的工厂方法,并且没有以通用方式提供自定义池大小的方法。


对不起,我的无知(无止境)有什么问题可以使用_new_实例化类? 我在哪里和如何使用工厂方法? 我甚至真的不知道我是如何想出自己的方法的。 我理解数组,并选择使用它们。 如果池中对象正在使用中,我只需将其停用,从而重置它并使用它。 有点像优秀键盘上的多音功能,当它用完时会窃取最早播放的音符。 - Confused
使用new没有问题,问题在于new()要求类型T提供公共无参构造函数。我假设您正在使用工厂方法,因为调用_poolOfLndngFX[i] = Instantiate( LndngFX, transform );,其中我假设Instantiate是一些包装器,围绕着Activator.CreateInstance - orhtej2
2
啊,我想我明白了。不对。Instantiate是Unity系统的一个东西。它可以复制物体,所以...是的。很可能是一个工厂函数。 - Confused

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接