top of page

At last a ranged enemy - Bat (not the Wuhan one)

  • Writer: Filip Walczak
    Filip Walczak
  • Jun 10, 2020
  • 2 min read

Actually, I don't even know if it's a bat or some other flying creature, but I like to call him that ¯\_(ツ)_/¯

Cute, isn't he?


Don't be fooled - it's the first enemy in the game which doesn't charge at you mindlessly. Instead he will try to hit you with his green projectiles if you come too close.


Greeeeeney


In comparison to the previous enemies, this one wasn't just a copy-paste with changed stats and sprite (im looking at you slime). The bat has a separate functionality which checks if a player is in a line of sight and if so - performs an attack.

How does this wonderful thing works?!


In order for the enemy to "see" the player, he is constantly checking if a player is close enough, then a raycast is shot from his position to a player's position. If this raycast doesn't hit anything other than a player/other enemy/item on the way, the enemy starts shooting.


Stupid bats, can't even hurt me

private bool TargetInRange() {
    if (attackTargetTransform == null) return false;
    
    if (Vector2.Distance(transform.position, attackTargetTransform.position) < 1)
        return true;
    entityAnimationController.SetIsRunning(false);
    return false;
}
private bool TargetInLineOfSight() {
    var currentPosition = transform.position;
    if (attackTargetTransform != null) {
        var targetPosition = attackTargetTransform.transform.position;
        
        RaycastHit2D[] hits = Physics2D.RaycastAll(currentPosition, 
        targetPosition - currentPosition,
        Vector2.Distance(currentPosition, targetPosition));
    
        foreach (var hit in hits) {
            if (hit.collider != null) {
                if (hit.transform.CompareTag("Player") == false && 
                    hit.transform.CompareTag("Enemy") == false  && 
                    hit.transform.CompareTag("Item") == false) {                  
                    return false;
                }
            }
        }
    }
    return true;
}
private void AttackTarget() {
    switch (type) {
        case CharacterType.Melee:
            //Melee combat functionality
            break;
        case CharacterType.Ranged:
            if (TargetInRange()         && 
                TargetInLineOfSight()   && 
                shootingOffCooldown) {
                Shoot();
            }
            break;
    }
}

I'll allow myself to skip the Shoot(), because its just a simple method instantiating the bullet, and moving him towards the player. The only interesting part which I used there is my Cooldown class:

public class Cooldown {

    private readonly float cooldownTime;
    private float elapsedTime;
    
    public event Action Started;
    public event Action Ended;
    
    public float Percentage => elapsedTime / (cooldownTime * 10);
    
    public Cooldown(float cooldownTime) {
        this.cooldownTime = cooldownTime;
    }

    public void Start() {
        GameController.Instance.StartCoroutine(CooldownCoroutine());
    }

    private IEnumerator CooldownCoroutine() {
        Started?.Invoke();

        for (float i = 0; i < cooldownTime * 10; i++) {
            yield return new WaitForSeconds(0.1f);
            elapsedTime = i;
        }

        elapsedTime++;
        Ended?.Invoke();
    }
}

I Was tired of creating coroutines like xxxCooldownCoroutine() everywhere where I needed some form of timer/cooldown, so I made this class. It lets me start a cooldown like this:

Cooldown shootingCooldown = new Cooldown(0.4f);
shootingCooldown.Ended += () => shootingOffCooldown = true;
shootingCooldown.Start();

which is faster to use and produces far less code.


That would be it when it comes to today's post. Hope you enjoyed it and till the next time!

Comments


Post: Blog2_Post
bottom of page