Stopping a Coroutine, starting a new one
So I'm working on boss mechanics for my danmaku game, and I'm using coroutines for starting the different patterns separately.
I want the coroutines to stop then start a new different coroutine when a variable (in this case health) reaches a certain point. I thought this would be possible with just if statements, but I don't seem to be getting any results.
As of here in the Enemy class, the coroutines run how I want them to:
using UnityEngine;
using System.Collections;
public class Enemy : MonoBehaviour
{
[System.Serializable]
public class Boundary
{
public float xMin, xMax, zMin, zMax;
}
public Transform shooter;
public Transform bulletPrefab;
public float speed = 2f;
Vector3 direct = -Vector3.forward;
Vector3 orgPos;
public Boundary boundary;
public float health;
public GameObject enemyExplosion;
void Start()
{
StartCoroutine(StartBulletPattern());
InvokeRepeating("ChangeDirect", 0f, 2f);
orgPos = transform.position;
}
void Update()
{
Vector3 pos = transform.position + direct * speed * Time.deltaTime;
//pos.x = Mathf.Clamp(pos.x, -4f, 4f);
//pos.z = Mathf.Clamp(pos.z, -4f, 4f);
pos = orgPos + Vector3.ClampMagnitude(pos - orgPos, 4f);
transform.position = pos;
GetComponent<Rigidbody>().position = new Vector3
(
Mathf.Clamp(GetComponent<Rigidbody>().position.x, boundary.xMin, boundary.xMax)
, 0.0f
, Mathf.Clamp(GetComponent<Rigidbody>().position.z, boundary.zMin, boundary.zMax)
);
}
void OnTriggerEnter(Collider other)
{
if(other.tag == "Player")
{
Destroy(other.gameObject);
health -= 10;
if(health <= 0)
{
Instantiate(enemyExplosion, other.transform.position, other.transform.rotation);
Destroy(other.gameObject);
Destroy(gameObject);
// gameController.GameOver();
}
}
}
void ChangeDirect()
{
//direct = Vector3.forward * (1 - Random.Range(0, 2)%2 * 2) + Vector3.right * (1 - Random.Range(0, 2)%2 * 2);
direct = Quaternion.Euler(0, Random.Range(0f, 360f), 0) * Vector3.forward;
}
IEnumerator StartBulletPattern()
{
yield return new WaitForSeconds(2f);
StartCoroutine(Flower(shooter, bulletPrefab, 5, 6, 5, 0.1f));
yield return new WaitForSeconds(6f);
StartCoroutine(StartSecondBulletPattern());
/* StartCoroutine(Blast(shooter, bulletPrefab, 20, 1, 120, 0.1f));
yield return new WaitForSeconds(6f);
StartCoroutine(Spiral(shooter, bulletPrefab, 20, 2, 0.1f, true));
StartCoroutine(Burst(shooter, bulletPrefab, 20, 5, 1.0f));
yield return new WaitForSeconds(6f);
StartCoroutine(Burst(shooter, bulletPrefab, 20, 3, 1.0f));
yield return new WaitForSeconds(0.5f);
StartCoroutine(Burst(shooter, bulletPrefab, 20, 3, 1.0f));
yield return new WaitForSeconds(3f);
StartCoroutine(Burst(shooter, bulletPrefab, 20, 3, 1.0f));
StartCoroutine(Flower(shooter, bulletPrefab, 5, 6, 5, 0.1f));
yield return new WaitForSeconds(8f);*/
}
IEnumerator StartSecondBulletPattern()
{
yield return new WaitForSeconds(3f);
StopCoroutine(StartBulletPattern());
yield return new WaitForSeconds(3f);
StartCoroutine(Burst(shooter, bulletPrefab, 20, 12, 0.5f));
StartCoroutine(StartSecondBulletPattern());
}
IEnumerator Blast(Transform shooter, Transform bulletTrans, int shotNum, int volly, float spread, float shotTime)
{
float bulletRot = shooter.eulerAngles.y; //The y-axis rotation in degrees.
if (shotNum <= 1)
{
// Just fire straight.
Instantiate(bulletTrans, shooter.position, Quaternion.Euler(0, bulletRot, 0));
}
else
{
while (volly > 0)
{
bulletRot = bulletRot - (spread / 2); //Offset the bullet rotation so it will start on one side of the z-axis and end on the other.
for (var i = 0; i < shotNum; i++)
{
Instantiate(bulletTrans, shooter.position, Quaternion.Euler(0, bulletRot, 0)); // Spawn the bullet with our rotation.
GetComponent<AudioSource>().Play();
bulletRot += spread / (shotNum - 1); //Increment the rotation for the next shot.
if (shotTime > 0)
{
yield return new WaitForSeconds(shotTime); //Wait time between shots.
}
}
bulletRot = shooter.eulerAngles.y; // Reset the default angle.
volly--;
}
}
}
IEnumerator Spiral(Transform shooter, Transform bulletTrans, int shotNum, int volly, float shotTime, bool clockwise)
{
float bulletRot = shooter.eulerAngles.y; //The y-axis rotation in degrees.
while (volly > 0)
{
for (var i = 0; i < shotNum; i++)
{
Instantiate(bulletTrans, shooter.position, Quaternion.Euler(0, bulletRot, 0)); // Spawn the bullet with our rotation.
GetComponent<AudioSource>().Play();
if (clockwise)
{
bulletRot += 360.0f / shotNum; //Increment the rotation for the next shot.
}
else
{
bulletRot -= 360.0f / shotNum; //Increment the rotation for the next shot.
}
if (shotTime > 0)
{
yield return new WaitForSeconds(shotTime); //Wait time between shots.
}
}
volly--; //Subtract from volly.
}
}
IEnumerator Burst(Transform shooter, Transform bulletTans, int shotNum, int volly, float vollyTime)
{
float bulletRot = 0.0f; //The y-axis rotation in degrees.
while (volly > 0)
{
for (var i = 0; i < shotNum; i++)
{
Instantiate(bulletTans, shooter.position, Quaternion.Euler(0, bulletRot, 0)); //Spawn the bullet with our rotation.
GetComponent<AudioSource>().Play();
bulletRot += 360.0f / shotNum; //Increment the rotation for the next shot.
}
bulletRot = 0.0f;
volly--;
yield return new WaitForSeconds(vollyTime);
}
}
IEnumerator Flower(Transform shooter, Transform bulletTrans, float flowerTime, int directions, float rotTime, float waitTime)
{
float bulletRot = 0.0f;
while (flowerTime > 0)
{
for (var i = 0; i < directions; i++)
{
Instantiate(bulletTrans, shooter.position, Quaternion.Euler(0, bulletRot, 0)); //Spawn the bullet with our rotation;
GetComponent<AudioSource>().Play();
bulletRot += 360.0f / directions;
}
bulletRot += rotTime;
if (bulletRot > 360)
{
bulletRot -= 360;
}
else if (bulletRot < 0)
{
bulletRot += 360;
}
flowerTime -= waitTime;
yield return new WaitForSeconds(waitTime);
}
}
}
My problem is both where to make this switch happen, and how to make it happen. I want StartBulletPattern() to run the pattern Flower() until the health reaches below a certain value, when it does I want to stop Flower() and start Burst(). But what happens when I try this is both the Flower and Burst coroutines run at the same time. How do I get this to work? I'm sorry if I posted this question confusingly I don't post often :(
Answer by Bunny83 · Nov 28, 2015 at 01:36 PM
Well, all your coroutines don't seem to consider the health of your enemy at any point. They all look like they are time or framecount based. If you want to chain your coroutines you have to wait for the nested coroutine to finish
IEnumerator StartBulletPattern()
{
yield return new WaitForSeconds(2f);
yield return StartCoroutine(Flower(shooter, bulletPrefab, 5, 6, 5, 0.1f));
// at this point the Flower coroutine has finished
//yield return new WaitForSeconds(6f); // not sure if you really want to wait 6 sec in between
yield return StartCoroutine(StartSecondBulletPattern());
// at this point the StartSecondBulletPattern coroutine has finished
}
If you want to run your coroutine until the health drop below a certain level, just do your while loop inside the coroutine until that happens. If you add a "health threshold" to each pattern coroutine you can even reuse them with different health periods. Just assume we have 1000 hp
IEnumerator StartBulletPattern()
{
yield return StartCoroutine(Pattern1(850f));
yield return StartCoroutine(Pattern2(700f));
yield return StartCoroutine(Pattern1(500f));
yield return StartCoroutine(Pattern3(100f));
yield return StartCoroutine(Pattern4(0f));
}
In this case we simple reuse Pattern1 once the health drop below 700.
IEnumerator PatternXX (float aHealthThreshold)
{
while (bossHealth > aHealthThreshold)
{
// do your fancy stuff
yield return ...;
}
}
"bossHealth" would be the variable that holds the hp value of your enemy.
edit
It's of course also possible If your pattern coroutines have a certain length (for example 1 sec, 3 sec, ...) and you want to cycle through them until the health drops below a certain point you can do this:
IEnumerator StartBulletPattern()
{
while (bossHealth > 700)
{
yield return StartCoroutine(Pattern1());
yield return StartCoroutine(Pattern2());
}
while (bossHealth > 500)
{
yield return StartCoroutine(Pattern3());
yield return StartCoroutine(Pattern4());
}
while (bossHealth > 0)
{
yield return StartCoroutine(Pattern4());
yield return StartCoroutine(Pattern5());
}
}
If you want your pattern coroutines still be interrupted when the health drops below the next stage you can still pass the threshold to the pattern coroutine and use an if statement in your pattern and if the health is below just do a yield break;
to exit the coroutine.
You can also add a random chance for a certain pattern
while (bossHealth > 700)
{
yield return StartCoroutine(Pattern1());
yield return StartCoroutine(Pattern2());
if (Random.value < 0.2f)
yield return StartCoroutine(SpecialPattern());
}
Here there's a 20% chance, everytime Pattern2 completes that the SpecialPattern runs in between.
second edit
Coroutines can be a bit limited if you need more advanced or more responsive behaviour you might want to use an actual state machine on your own.
Since Unity now can Stop any coroutine you like you can also have a single coroutine running and have an "EnemyAICoordinator" which starts / stops an appropriate coroutine.
Example:
private Coroutine currentCoroutine = null;
private float[] stages = new float[] { 800f, 600f, 400f, 50f, 0f};
private int currentStage = 0;
IEnumerator AutoClearCoroutine()
{
while( bossHealth > 0 )
{
if (currentCoroutine != null)
{
yield return currentCoroutine;
currentCoroutine = null;
}
else
yield return null;
}
}
void StopPattern()
{
if (currentCoroutine != null)
StopCoroutine(currentCoroutine);
}
void StartPattern(IEnumerator aCoroutine)
{
StopPattern();
currentCoroutine = StartCoroutine(aCoroutine);
}
IEnumerator StartCoordinator()
{
StartCoroutine(AutoClearCoroutine());
while (bossHealth > 0)
{
// if we reached the current stage health limit, go to the next stage
if (bossHealth < stages[currentStage])
{
StopPattern();
if (currentStage < stages.Length)
currentStage++;
}
if (currentCoroutine == null)
{
// start a new pattern
switch(currentStage)
{
case 0:
StartPattern(LightPattern());
break;
case 1:
StartPattern(MediumPattern());
break;
case 2:
StartPattern(HardPattern());
break;
case 3:
StartPattern(HarderPattern());
break;
case 4:
StartPattern(FinalPattern());
break;
}
}
yield return null; // wait one frame
}
}
Here the "StartCoordinator" coroutine will run just line Update (you could use Update instead if you want). It checks every frame if the health is below the next stage limit. If it is it stops the current pattern immediately and starts a new pattern from the next stage. This is the most responsive implementation.
We store a reference to the current pattern coroutine inside the variable "currentCoroutine". The coroutine "AutoClearCoroutine" runs in parallel to the StartCoordinator. All it does is waiting for the current coroutine to finish and clears the variable so a new coroutine is started. This in combination with the coordinator will ensure that there's always one pattern running.
In each "state case" of the switch you still can randomly choose a pattern. Keep in mind that with such an approach a pattern can be interrupted at any yield statement. So don't do some state changes at the beginning which should be reverted at the end since it's not ensured that the pattern will reach the end.
Thanks for your help! I didn't think to address this from inside of my pattern coroutines! But I'll definitely be trying out each of your suggestions and I'll be back when I make progress. Thank you very much!
Answer by cyth1 · Nov 29, 2015 at 12:09 AM
Thank you! Using the while loops to handle the boss' health is exactly what I needed. One question though. What's the difference between starting a coroutine normally, and having "yield return" in front of it?