Progress!

And yes, I am tracking his health. Each bullet has a damage to it. And he also gets hit if he runs into an ememy. Niiiice.
Coroutines
I was introduced to a new concept in Unity called a coroutine. Java (my native language) doesn’t really have anything super analogous; typically, I’d just spawn a second thread and sleep for a period before executing. However, after asking Gemini a bit, it appears that’s inadvisable in Unity because things generally run on the main thread.
I discovered a need for coroutines because I wanted enemies to blink whenever they get hit. You can kind of see it in the gif above (though it has a low framerate): each enemy takes two shots, and it blinks when it gets hit by the first shot. I wanted an indicator the shot landed.
To do this, I used a coroutine. Basically, the main thread will execute the coroutine until it hits a yield, at which point it pauses the coroutine and resumes the other part of the main thread… or, at least that’s how I’m using it right now. So, to get the flashing effect, we start a coroutine which changes the sprite’s alpha to 50%, then yields for 0.15 seconds before returning back to normal. Thus, a 0.15 second flash. I made this change to the base Enemy so other extending classes can benefit:
private readonly Color _flash = new Color(1f, 1f, 1f, 0.5f);
protected void Hit()
{
StartCoroutine(Flash());
}
private IEnumerator Flash()
{
var original = _spriteRenderer.color;
_spriteRenderer.color = _flash;
yield return new WaitForSeconds(0.15f);
_spriteRenderer.color = original;
}
And now, in my enemy implementation script, I can just call Hit whenever it gets hit but doesn’t die. It flashes!
Of course, I didn’t stop there. Remember my auto-firing script from before? While I’m happy I was able to think through it a bit, coroutines make life a lot easier here. I don’t have to track when the next shot is, I can just yield for some time, then fire again so long as the button is still being held. Whenever they press the shoot button, we fire the started event, which calls ShootAction, which starts the coroutine if the button is being pressed:
public float autoShootInterval = 0.2f;
private void ShootAction(InputAction.CallbackContext context)
{
if (!_shootAction.inProgress) return;
StartCoroutine(AutoShoot());
}
private IEnumerator AutoShoot()
{
while (_shootAction.inProgress)
{
yield return new WaitForSeconds(autoShootInterval);
if (_shootAction.inProgress)
{
CreateBullet();
}
}
}
Significantly simpler than before.
Enemy Shots
We need our little chopper guys shooting. I created another prefab for an enemy bullet. Luckily, I already refactored, so it can just extend Projectile and it has most of what I need. I just need to override the GetInitialDirection method to point the bullet at the player. This isn’t a tracking bullet, so it’s still straight once fired, so we only need to set the initial direction.
Luckily, we can do this with a tag. I tagged Mega Man with the “Player” tag, and we can get it position by getting the tagged game object, do a little vector math, and normalize. Thus, we get this:
protected override Vector2 GetInitialDirection()
{
var player = GameObject.FindGameObjectWithTag("Player");
var direction = player.transform.position - transform.position;
direction.Normalize();
return direction;
}
Of course, our enemy now has to actually shoot bullets. Time for another coroutine! These are coming in highly useful. I want them to shoot every 1-3 seconds (randomly) while they’re alive, so I kick off a coroutine inside of Start:
[SerializeField] private float averageFireRate = 2f;
[SerializeField] private Projectile bullet;
private void Start()
{
StartCoroutine(Attack());
}
private IEnumerator Attack()
{
while (true)
{
yield return new WaitForSeconds(
Mathf.Clamp(
Random.Range(averageFireRate-1f, averageFireRate+1f), 1f, 3f));
Instantiate(bullet, transform.position, Quaternion.identity);
}
}
Once I hooked up the bullet prefab to the script, we’re shooting. Nice.
Enemy Damage
NO MORE INVINCIBILITY! Mega Man must take damage!
I tossed a Rigidbody2D (kinematic) on him with a PolygonCollider2D. I don’t want shots that hit the edge of his rectangular sprite area (much of which is transparent space) to register as hits, only when it hits is sprite, so the polygon collider allows me to be more precise.
Additionally, to the Mega Man game object, I added another animation of him getting hit. Really, the hardest part about all of this is getting the animation right… I really suck at that. The animation triggers whenever the “isHit” property is flipped to true. We’ll need to make sure it’s flipped back to false quickly so he doesn’t play the animation multiple times.
Finally, I want him to be invulnerable for a couple seconds (and flashing) so he can’t just get spam hit. As you can imagine, it’s time for yet another coroutine. So, to the Mega Man script, we’ve got quite a few changes. I’ll go section by section.
private readonly int _hitAnimId = Animator.StringToHash("isHit");
private readonly Color _flash = new Color(1f, 1f, 1f, 0.2f);
[SerializeField] private float health = 100;
[SerializeField] private float invulnerabilityDuration = 2f;
private bool _isInvulnerable = false;
Some properties we need to keep track of:
_flash– Alpha used for blinking (20%)._hitAnimId– ID of the “isHit” parameter of the animation.health– Total health.invulnerabilityDuration– How long he stays invulnerable._isInvulnerable– Whether he’s invulnerable right now.
And now, the collision trigger.
private void OnTriggerEnter2D(Collider2D other)
{
if (!_isInvulnerable)
{
float damage = 0f;
// Hit by a bullet.
if (other.CompareTag("EnemyBullet"))
{
damage = other.GetComponent<Projectile>().GetDamage();
Destroy(other.gameObject);
}
// Hit by an enemy directly.
if (other.CompareTag("Enemy"))
{
damage = 50f;
}
if (damage > 0f)
{
health = Mathf.Max(0, health - damage);
StartCoroutine(Invulnerable());
}
}
}
I have added two tags to some prefabs: enemies will be tagged with the “Enemy” tag, and enemy projectiles are tagged with “EnemyBullet”. In the future, projectiles and enemies may have different damage, so I wanted to separate those two now since they are different components.
So, firstly, we skip all of this if he’s invulnerable. Then, we figure out how much damage he’s taking. If it’s a bullet, we get the component’s damage value and destroy it. If it’s the enemy itself (Mega Man flew into it), I’m just doing a flat 50 damage for now. If he’s damaged at all, recalculate his health and start the invulnerability coroutine:
private IEnumerator Invulnerable()
{
_isInvulnerable = true;
_animator.SetBool(_hitAnimId, true);
var original = _spriteRenderer.color;
var flashDelay = 0.07f;
float startTime = Time.time;
while (Time.time < startTime + invulnerabilityDuration)
{
yield return new WaitForSeconds(flashDelay);
_animator.SetBool(_hitAnimId, false);
_spriteRenderer.color = _flash;
yield return new WaitForSeconds(flashDelay);
_spriteRenderer.color = original;
}
_isInvulnerable = false;
}
This is a bit more complex, I think. Not too bad though:
- Mark him as invulnerable.
- Set “isHit” to true so the hit animation begins.
- Get the original color (alpha) of the sprite and keep track of it.
- Also keep track of the time he got hit.
- Loop until the invulnerability duration is hit.
- Each iteration will wait a delay of time, change his alpha to flash, waiting again, then change back to the original.
- Also, set “isHit” back to false so the animation doesn’t keep playing.
- Once the duration has passed, set his vulnerability back to false so he can get hit again.
In retrospect, I don’t like the handling of “isHit” here. It seems odd to set it in the loop, but I noticed that if I set it to false before the first delay, the animation doesn’t play at all. Perhaps parameters aren’t the right solution here, and we should figure out a way to play a specific animation once instead.
Anyway, in the end, he can now get his and hit health drops appropriately. He doesn’t die yet, but his health can definitely hit zero.
Up Next
I really want to script encounters, so that’s probably next.
Leave a comment