开发人员如何继续参考大量不同的实体预制件?

FIC*_*EKK 1 c# unity-game-engine

我正在开发一个游戏,我正在创造像Minecraft一样的无限世界.问题是我的游戏将有数百种不同的动物和敌人,但我不确定引用这些预制件的常用方法是什么.

我现在的解决方案是拥有一个可以产卵的动物工厂.看起来像这样:

public static class AnimalFactory
{
    public static GameObject sheep = Resources.Load<GameObject>("Prefabs/Animal/Sheep");
    public static GameObject cow = Resources.Load<GameObject>("Prefabs/Animal/Cow");

    public static void SpawnSheep(float x, float y)
    {
        GameObject drop = Object.Instantiate(sheep, new Vector3(x, y, 0f), Quaternion.identity);
    }

    public static void SpawnCow(float x, float y)
    {
        GameObject drop = Object.Instantiate(cow, new Vector3(x, y, 0f), Quaternion.identity);
    }
}
Run Code Online (Sandbox Code Playgroud)

但是,我应该缓存所有预制件的参考,或者只需使用Resources.Load<GameObject>一次我需要参考?

我还应该提到动物经常产卵.

那么,这里的常用方法是什么?如果游戏中有数百种不同的类型,开发者通常如何产生动物?

AnL*_*Log 10

你肯定应该缓存对Resources的调用,因为除非Unity3D为你做一些缓存,否则需要重复查询资源.但是,代码中最紧迫的问题不是性能问题.更确切地说,你必须为每只动物编写一种方法,如果你后来使产卵方法更复杂,那么这种方法非常麻烦且容易出错.

更好的方法是通过字符串或数字ID或字典中的枚举来引用您的生物.利用词典的力量取得胜利!

使用基于字符串的字典

一种解决方案是使用基于字符串的字典,如下所示:

public static class AnimalFactory
{
    // Dictionary to map a string to each animal object.
    private static Dictionary<string, GameObject> animalDictionary;

    // We'll build our dictionary in the static constructor.
    static AnimalFactory()
    {
        // We can load all the animals from that folder.
        var animals = Resources.LoadAll<GameObject>("Prefabs/Animal");
        animalDictionary =
            new Dictionary<string, GameObject>(animals.Length);

        foreach (GameObject animal in animals)
        {
            animalDictionary.Add(animal.name, animal);
        }
    }

    public static void SpawnAnimal(string animalName, float x, float y)
    {
        if (animalDictionary.ContainsKey(animalName))
        {
            GameObject drop = Object.Instantiate(
                animalDictionary[animalName],
                new Vector3(x, y, 0f), Quaternion.identity);
        }
        else
        {
            Debug.LogError("Animal with " + animalName + "could not be " +
                "found and spawned.");
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

优点:

  • 整个事情只需要一个spawn方法.减少错误,减少代码.
  • 你需要为整个事情调用一次Resources.Load.更好的性能.
  • 轻松地允许您添加更多动物而无需更改代码.只需将它们放在Animal文件夹中即可.

缺点:

  • 依靠动物的名字保持不变.如果你打电话SpawnAnimal("Bear", 100.0f, 20.0f),后来你决定改变BearGrizzlyBear该方法将完全停止工作,因为它不会找到在词典中的条目.如果拼错名字,它也会失败.
  • 您必须只在Animal文件夹中保留动物,因为Resources.LoadAll会加载整个目录.

使用基于枚举的字典

第二个骗局是可以接受的.但第一个骗局非常讨厌!基于枚举的字典可以为我们修复这两个缺点.

  1. 创建一个enum包含动物名称的所有名称.这样一来,如果你使用的是一个enum而不是string拼错它的机会,你就可以在整个项目中安全地更改枚举的名称.我们称之为枚举AnimalType.
  2. 创建一个Monobehavior类,我们将AnimalTypeHolder使用单个AnimalType公共变量调用它.
  3. AnimalTypeHolder组件添加到动物文件夹中的每只动物.
  4. 当您运行静态构造函数AnimalFactory来构建您使用的字典时GetComponent<AnimalTypeHolder>().如果GameObject有一个AnimalTypeHolder组件,我们肯定知道我们应该加载它!这意味着我们摆脱了第二个骗局!在Animal文件夹中放置一个不应该存在的文件不会造成麻烦!在我们获得组件之后,我们检索AnimalType枚举并将枚举及其相应的GameObject存储到字典中.由于我们使用枚举而不是字符串,我们也摆脱了第一个骗局!
  5. 重新编码SpawnAnimal以使用AnimalType枚举而不是字符串.

恩欧姆:

public enum AnimalType
{
    Cow,
    Sheep,
    Bear
}
Run Code Online (Sandbox Code Playgroud)

保存我们的枚举的组件,需要添加并设置为Animal文件夹中的每个动物GameObject:

public class AnimalTypeHolder : MonoBehaviour
{
    public AnimalType type;
}
Run Code Online (Sandbox Code Playgroud)

我们的修改AnimalFactory:

public static class AnimalFactory
{
    private static Dictionary<AnimalType, GameObject> animalDictionary;

    static AnimalFactory()
    {
        var animals = Resources.LoadAll<GameObject>("Prefabs/Animal");
        animalDictionary =
            new Dictionary<AnimalType, GameObject>(animals.Length);

        foreach (GameObject animal in animals)
        {
            var typeHolder = animal.GetComponent<AnimalTypeHolder>();
            if (typeHolder != null)
            {
                animalDictionary.Add(typeHolder.type, animal);
            }
        }
    }

    public static void SpawnAnimal(AnimalType animalType, float x, float y)
    {
        if (animalDictionary.ContainsKey(animalType))
        {
            GameObject drop = Object.Instantiate(
                animalDictionary[animalType],
                new Vector3(x, y, 0f), Quaternion.identity);
        }
        else
        {
            Debug.LogError("Animal with " + animalType + "could not be " +
                "found and spawned.");
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

现在设置动物需要稍长的时间,因为你需要添加组件,但是通过完成这一切你已经消除了整个类的错误!添加新动物仍然很容易!只是:

  1. 将预制件放在Animals文件夹中.
  2. AnimalType为这个新动物的枚举添加一个新值.
  3. AnimalTypeHolder组件添加到预制件.
  4. 设置AnimalTypeAnimalTypeHolder组件我们做出了新的枚举值.完成!

当然,这只是关于如何做到这一点的一般概念.如果您的所有动物都已经拥有类似动物组件的东西,那么您可以将动物组件的整个功能放在其中AnimalTypeHolder.我只是想传达这个概念.您将了解如何根据自己的要求最好地实施它.


提高性能 - 对象池

现在进一步解决您对性能的担忧.有一个通用的答案可以提高在Unity中实例化GameObject的性能.对象池,对象池,对象池.

对象池是指您预先存储GameObjects和/或不是销毁GameObjects而是停用它们并重置它们并将它们存放在存储中以便重复使用.如果您要实例化200只绵羊,那可能会导致游戏滞后.因此,您可以在加载关卡时预先实例化200只绵羊,在游戏开始前停用它们,然后通过简单地将它们移动到适当的位置并激活它们的GameObject,您只需生成已经实例化的绵羊而不是实例化绵羊.因此,主要目标是避免在游戏过程中尽可能多地使用Instantiate,或者在游戏过于忙于将CPU用于其他事情时避免使用它.

互联网上有很多关于如何最好地实现对象池的资产和教程,我相信你会发现它很有用.我建议你开始研究这个话题.

但我仍然会给你一个如何使用对象池继续前一个例子的可靠例子!

我们只需在组件中添加一个preallocateCount变量AnimalTypeHolder,告诉AnimalFactory我们要提前实例化这些动物的数量:

public class AnimalTypeHolder : MonoBehaviour
{
    public AnimalType type;
    public int preallocateCount = 10;
}
Run Code Online (Sandbox Code Playgroud)

现在我们新的AnimalFactory:

public static class AnimalFactory
{
    private static Dictionary<AnimalType, GameObject> animalDictionary;
    private static Dictionary<AnimalType, List<GameObject>> animalPoolActive;
    private static Dictionary<AnimalType, List<GameObject>> animalPoolInActive;

    static AnimalFactory()
    {
        var animals = Resources.LoadAll<GameObject>("Prefabs/Animal");
        animalDictionary =
            new Dictionary<AnimalType, GameObject>(animals.Length);
        animalPoolActive =
            new Dictionary<AnimalType, List<GameObject>>();
        animalPoolInActive =
            new Dictionary<AnimalType, List<GameObject>>(animals.Length);

        foreach (GameObject animal in animals)
        {
            var typeHolder = animal.GetComponent<AnimalTypeHolder>();
            if (typeHolder != null)
            {
                animalDictionary.Add(typeHolder.type, animal);

                // Since there are no active animals in the beginning, we'll
                // create an empty list.
                animalPoolActive.Add(typeHolder.type,
                    new List<GameObject>());

                // Make a list to hold our inactive preallocated animals.
                var prellocAnimals
                    = new List<GameObject>(typeHolder.preallocateCount);

                for (int i = 0; i < typeHolder.preallocateCount; i++)
                {
                    var go = Object.Instantiate(animal);
                    go.SetActive(false);
                    prellocAnimals.Add(go);
                }

                animalPoolInActive.Add(typeHolder.type, prellocAnimals);
            }
        }
    }

    public static void SpawnAnimal(AnimalType animalType, float x, float y)
    {
        if (animalDictionary.ContainsKey(animalType))
        {
            var inactives = animalPoolInActive[animalType];

            // Check if we have inactive animals of this type we can use.
            if (inactives.Count > 0)
            {
                // We'll just get the last GameObject in the pool.
                int last = inactives.Count - 1;
                GameObject drop = inactives[last];

                // We have to remove it from the inactive pool now that
                // we're using it!
                inactives.RemoveAt(last);

                // Now we have to add it to the active pool.
                var actives = animalPoolActive[animalType];
                actives.Add(drop);

                drop.SetActive(true);
                drop.transform.SetPositionAndRotation(new Vector3(x, y, 0f),
                    Quaternion.identity);
            }
            // If we don't have them preallocated, we'll have to instantiate
            // normally.
            else
            {
                GameObject drop = Object.Instantiate(
                    animalDictionary[animalType],
                    new Vector3(x, y, 0f), Quaternion.identity);

                animalPoolActive[animalType].Add(drop);
            }
        }
        else
        {
            Debug.LogError("Animal with " + animalType + "could not be " +
                "found and spawned.");
        }
    }

    public static void UnspawnAnimal(GameObject animal)
    {
        var typeHolder = animal.GetComponent<AnimalTypeHolder>();

        if (typeHolder != null)
        {
            AnimalType type = typeHolder.type;

            var actives = animalPoolActive[type];
            var inactives = animalPoolInActive[type];

            // Check if we're not accidentally using unspawn more than once.
            if (inactives.Contains(animal))
            {
                Debug.LogWarning("Trying to unspawn an animal that " +
                    "should already be unspawned!");
                return;
            }

            // First we check if it exists in the active pool.
            if (actives.Contains(animal))
            {
                // If it exists then we have to remove it now.
                actives.Remove(animal);
            }

            // We have to add it to the inactive pool for later use.
            inactives.Add(animal);

            // WARNING: In most situations in order to be able to reuse
            // a GameObject like this you need to reset it! For example if
            // your animals have HP then you probably despawned them when
            // they got to zero! You need to reset the HP back to the
            // starting default if you want to reuse the animal!!
        }
        else
        {
            Debug.LogWarning("Attempting to use Unspawn Animal on a " +
                "GameObject that is either not an animal or doesn't have " +
                "an AnimalTypeHolder component!");
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

  • 这样一个惊人的,写得很好,详细的答案.谢谢你,先生! (2认同)