为什么这种敌人生成方法会导致Unity崩溃?

3
我一直在改动Unity 2D项目中主要的Gameplay.cs脚本代码部分,不管我怎么修改和调整它,都会导致Unity在播放时锁定,或者经过一些调整后立即从第0波进入第9波。这是我的最初尝试,它会导致Unity在播放时冻结。我不明白为什么会出现这种情况,并希望了解我的逻辑错误在哪里。
我试图实现以下内容:
  • 播放时,游戏等待startWait秒钟。
  • 等待 startWait 后,游戏进入 SpawnWaves() 方法,并在每个波之间等待 waveWait 秒,并在每个波期间将波计数增加1。
  • 每个波生成 zombieCount 数量的敌人,并等待每个僵尸生成之间的 spawnWait 秒。


private float startWait = 3f;   //Amount of time to wait before first wave
private float waveWait = 2f;    //Amount of time to wait between spawning each wave
private float spawnWait = 0.5f; //Amount of time to wait between spawning each enemy

我是一名初学者,了解到我的代码可能不符合最佳实践。我也有一个使用协程和yield返回的工作敌人生成器,但我希望使这个版本的敌人生成器工作。


改变时间尺度需要极其谨慎。确保在脚本的首次更新之前玩家不会死亡。 - Bizhan
你也可以使用协程和 yield return new WaitForSecondsRealtime,以便在将时间缩放更改为零后,游戏仍然可以继续运行。 - Bizhan
在我的Player.cs脚本中,我通过Update()方法调用一个方法来检查玩家是否死亡,如果他们死了,我就将isDead == true,然后由Gameplay.cs使用。在Player.cs中,isDead == false在Awake()时被设置。目前游戏会一直运行,直到经过4秒钟,也就是第一波应该出现的时候它就会卡住。 - TechnoCat
2个回答

2

我没有使用Unity的经验,但是有使用XNA的经验,我认为它们的主要游戏处理方式相似,都使用Update()和Draw()函数。

Update()函数通常每秒被调用几次,在你的代码中,当startTimer >= startWait时,SpawnWaves()函数应该在每个Update调用之后执行一次。

现在让我们来看看SpawnWaves()函数。

void SpawnWaves()
{
    while (!Player.isDead && ScoreCntl.wave < 9)
    {
        if (waveTimer >= waveWait)
        {
            IncrementWave();
            for (int i = 0; i < zombieCount; i++)
            {
                if (spawnTimer >= spawnWait)
                {
                    [..]
                    spawnTimer = 0f;
                }
            }
            waveTimer = 0f;
        }
    }
}

这个while循环有问题,因为它是一个游戏循环中的游戏循环。只有当玩家死亡或您的波次超过九次时,它才会退出。

在调用SpawnWaves()之后,根据Time.deltaTime的不同(在程序启动后更多或更少的随机),waveTimer可能大于或小于waveWait。

  • 情况1:waveTimer >= waveWait

    所有代码都将立即执行。 因此,如果可以IncrementWave(),则将在毫秒内完成九次。然后while为false,代码内部再也不执行。

  • 情况2:waveTimer < waveWait

    在这种情况下,IncrementWave()永远不会被调用,您的游戏可能会在此while循环中无限旋转

所以你需要做的基本上是用if语句替换这个while循环,以允许程序继续执行。

If(!Player.isDead && ScoreCntl.wave < 9)

从那时起,您的计数器递增将在 Update() 上被调用超过一次。
waveTimer += Time.deltaTime;
spawnTimer += Time.deltaTime;

随着时间的推移,您对波形改变和生成的条件可能会实现。

if (waveTimer >= waveWait),
if (spawnTimer >= spawnWait)

我希望这可以指导您解决问题。 更新: 对于这种行为,最好将IncrementWave和生成条件分开。为此,您需要一个额外的布尔变量,在本例中称为haveToSpawn。
    if (waveTimer >= waveWait)
    {
      IncrementWave();
      waveTimer = 0f;
      haveToSpawn = true; //new variable
    }

    if (haveToSpawn && spawnTimer >= spawnWait)
    {
        for (int i = 0; i < zombieCount; i++)
        {
            [..] //spawn each zonbie
        }
        spawnTimer = 0f; //reset spawn delay
        haveToSpawn = false; //disallow spawing
    }

哇,这应该是非常明显的。我怀疑Update() 函数可能有问题,试着把SpawnWaves() 移到Start()函数里,但没想到只需要将while替换为if即可解决问题。谢谢!然而,我的后续代码肯定出了问题,因为无论我做什么它都几乎忽略了循环语句,然后产生一个僵尸,增加一个波次并重复此过程。但实际上每个波次应该生成更多的僵尸。我甚至尝试把i++放到循环之中,在一个僵尸生成后再执行,但这只会让Unity在僵尸应该生成的时刻卡死。 - TechnoCat
1
我写了一个小更新,应该会生成所有的僵尸。希望它能正常工作。 - Florian p.i.
有趣的是,它引发了一连串的僵尸洪水。波浪递增工作得很完美,但是spawnWait应该在给定波中的每个单个僵尸生成之间发生。在这种情况下,它一次生成了所有的僵尸数量,而不是一个接一个地等待waveWait秒钟,然后再释放所有的僵尸数量。这对于一次性生成所有物品非常有效。 - TechnoCat
1
在这种情况下,使用一个变量来保存生成的僵尸数量,并将for(int i...)替换为类似if(zombieSpawned < zombieCount)的内容,并每次增加zombieSpawned。为了确保haveToSpawn在第一次之后不会被重置,添加一个额外的if语句,如if(zombieSpawned == zombieCount){ haveToSpawn = false;}。因为生成时间现在被乘以,所以只要haveToSpawn == true,就应该禁用waveTimer++,否则新的波可能会到达得太快。最后,在haveToSpawn = true之后设置zombieSpawned = 0;。这是我能为您做的一切。 - Florian p.i.

1
使用协程来调用 spawn waves 方法:
private float startWait = 3f;   //Amount of time to wait before first wave
private float waveWait = 2f;    //Amount of time to wait between spawning each wave
private float spawnWait = 0.5f; //Amount of time to wait between spawning each enemy


private float startTimer = 0f; //Keep track of time that has elapsed since game launch
private float waveTimer = 0f;  //Keep track of time that has elapsed since last wave
private float spawnTimer = 0f; //Keep track of time that has elapsed since last spawn

private List<GameObject> zombies = new List<GameObject>();

[SerializeField]
private Transform spawnPoints; //The parent object containing all of the spawn point objects


void Start()
{
    deathUI.SetActive(false);

    //Set the default number of zombies to spawn per wave
    zombieCount = 5;

    PopulateZombies();

    StartCoroutine(SpawnWaves());
}

void Update()
{
    startTimer += Time.deltaTime;
    if (startTimer >= startWait)
    {
        waveTimer += Time.deltaTime;
        spawnTimer += Time.deltaTime;
    }


    //When the player dies "pause" the game and bring up the Death screen
    if (Player.isDead == true)
    {
        Time.timeScale = 0;
        deathUI.SetActive(true);
    }

}

IEnumerator SpawnWaves()
{

    //wait 3 seconds
    yield return new WaitForSeconds(startWait);



    //then:

    while (!Player.isDead && ScoreCntl.wave < 9)
    {
        if (waveTimer >= waveWait)
        {
            IncrementWave();
            for (int i = 0; i < zombieCount; i++)
            {
                if (spawnTimer >= spawnWait)
                {
                    Vector3 spawnPosition = spawnPoints.GetChild(Random.Range(0, spawnPoints.childCount)).position;
                    Quaternion spawnRotation = Quaternion.identity;
                    GameObject created = Instantiate(zombies[0], spawnPosition, spawnRotation);
                    TransformCreated(created);

                    spawnTimer = 0f;
                }
            }

            waveTimer = 0f;
        }

        //wait until the end of frame
        yield return null;
    }
}

为了更好地理解Unity协程的工作原理:
协程是一种具有IEnumerator返回类型的方法,行为类似于异步执行的代码块集合。yield指令分隔代码块并指定等待下一个代码块开始执行的时间帧数。
有几种类型的yield指令:
- null:等待帧结束(在渲染之前) - WaitForEndOfFrame:等待帧结束(在渲染之后) - WaitForSeconds:等待指定秒数(包括timescale) - WaitForSecondsRealtime:等待指定秒数(忽略timescale)
您可以将以下内容视为:
- Update是使用yield return null的协程 - LateUpdate是使用yield return new WaitForEndOfFrame()的协程 - FixedUpdate是使用yield return new WaitForSeconds(.2f)的协程

1
谢谢!我一直在避免使用协程和多个yield返回 - 因为我已经走过了那条路 - 但我没有想到可以将协程与计时器结合起来(而不是所有的yield返回)。 这确实有效,除了我的后续代码有些奇怪 - 这是另一个完全不同的问题,我会解决的。 感谢您提供有关协程和更新的其他信息。 - TechnoCat

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