Enemy not standing on platform when chasing player

Wasn’t quite sure where to post this question, figured this was close enough for a place to post, apologies if it isn’t.

Here’s some context before the question. I’m doing the Tilevania course. So after doing most of the lectures (some followed along, others did the challenges for) I felt good about myself and decided to make a second enemy that chases the player. Initial idea was that the enemy would have two states -

  • state one: patrolling
  • state two: chasing the player

If the enemy sees the player they’d enter an aggro state and run after them. If they’re out of their area of detection (Checked by a raycast), they’d keep running towards the player until either A - player is in their detection zone again and the enemy continues to chase or B - player has not been in their detection zone for a period of time. If B happens, the enemy should return to their patrol state.

A little bit of tinkering with current knowledge gained from here, watching/following a video or two on the subject there, and a lot of reading the API and even more trial and error attempts later… I almost did it.

If the enemy is placed in an area surrounded by walls, the code works pretty well!

CodeWorksHere

hell-doggo bumps into the wall for a few and goes to his regular patrol route when he gets bored of headbanging. I’m okay with the current result

What is NOT working however, is when you try the same thing with a platform that has no walls surrounding it, well… This happens:

CodeDoesNotWorkHere

Hell-doggo has the urge to defy the laws of gravity, and also the laws of my spaghetti code.

Question is - what is going on, where does the code hiccup and how can I fix it? I’ll send the code, which is… I hope not too painfully long and spaghetti-ish for such a script. My best guess so far has been that the OnTriggerExit2D method hasn’t been working right, but even after hours of testing and attempts of getting it to work I am still not sure how to fix it, neither am I sure if that really is the problem here.

Help would be greatly appreciated, and feel free to give me an earful about how weird the code is.

public class HellhoundMovement : MonoBehaviour
{
    [Header("Components Outside GameObj")] //...mostly outside the gameObj

    [SerializeField] CompositeCollider2D terrainCol;
    [SerializeField] Transform player;
    [SerializeField] Transform castPoint;

    [Header("Agro Settings")]

    [SerializeField] float aggroRange;
    [SerializeField] float defaultAgroTime = 3f;
    [SerializeField] float aggroTimeLeft;

    [Header("Scripts and Components")]
    Rigidbody2D enemyRB;
    Animator enemyAnim;
    [SerializeField] BoxCollider2D[] enemyWallDetectors;
    EnemyUtil enemyUtil;

    [Header("Movement Speed Settings")]
    [SerializeField] public float defaultMoveSpeed;
    [SerializeField] float currentMoveSpeed;
    [SerializeField] float speedModif = 2;

    [Header("gameObj checks")]

    [SerializeField] public bool isFacingLeft = false;
    [SerializeField] public bool enemyIsAlive = true;

    [SerializeField] bool isAgro = false;
    [SerializeField] bool isSearching = false;

    private void Start()
    {
        enemyRB = GetComponent<Rigidbody2D>();
        enemyAnim = GetComponent<Animator>();
        enemyWallDetectors = GetComponents<BoxCollider2D>();

        enemyUtil = GetComponent<EnemyUtil>();

        currentMoveSpeed = defaultMoveSpeed;
        aggroTimeLeft = defaultAgroTime;

        enemyRB.constraints = RigidbodyConstraints2D.FreezeRotation | RigidbodyConstraints2D.FreezePositionY;
    }

    void Update()
    {
        if (CanSeePlayer(aggroRange))
        {

            ChasePlayer();
            isAgro = true;
        }
        else if (isAgro)
        {
            isSearching = true;
            if (isSearching)
            {                
                StartAgroTimer();
            }
        }
        else
        {
            StartPatrolling();
        }

    }
    private void ChasePlayer()
    {
        enemyAnim.speed = 2f;
        if (!enemyIsAlive)
        {
            enemyRB.constraints = RigidbodyConstraints2D.FreezePositionX;
            return;
        }
        else if (transform.position.x < player.position.x)
        {
            enemyRB.velocity = new Vector2((currentMoveSpeed * speedModif), 0);
            enemyUtil.FlipSpriteToPos();
            isFacingLeft = false;

        }
        else
        {
            enemyRB.velocity = new Vector2(-(currentMoveSpeed * speedModif), 0);
            enemyUtil.FlipSpriteToPos();
            isFacingLeft = true;
        }
    }

    private void StartPatrolling()
    {
        isAgro = false;
        isSearching = false;
        enemyAnim.speed = 1f;
        if (!enemyIsAlive)
        {
            enemyRB.constraints = RigidbodyConstraints2D.FreezePositionX;
            return;
        }
        else
        {
            enemyRB.constraints = RigidbodyConstraints2D.FreezePositionY | RigidbodyConstraints2D.FreezeRotation;
            enemyRB.velocity = new Vector2((defaultMoveSpeed), 0);
            enemyUtil.FlipSpriteToPos();

            if (enemyRB.velocity.x > 0)
            {
                isFacingLeft = false;
            }
            else
            {
                isFacingLeft = true;
            }

        }
    }
    private void OnTriggerEnter2D(Collider2D collision)
    {
        if (enemyWallDetectors[1].IsTouchingLayers(LayerMask.GetMask("StaticTerrain")))
        {
            Debug.Log(collision);
            CheckSpriteAfterCollision(collision);
        }
    }

    private void OnTriggerStay2D(Collider2D collision)
    {
        if (enemyWallDetectors[1].IsTouchingLayers(LayerMask.GetMask("StaticTerrain")))
        {
            CheckSpriteAfterCollision(collision);
        }
    }
    private void OnTriggerExit2D(Collider2D collision)
    {
        if (!enemyWallDetectors[0].IsTouchingLayers(LayerMask.GetMask("StaticTerrain", "OneWayPlatform")))
        {
            CheckSpriteAfterCollision(collision);
            Debug.Log("Triggered with: " + collision.name);
        }
    }



    private void CheckSpriteAfterCollision(Collider2D collision) //not sure how to call this method TBH. Suggestions are welcome
    {
        defaultMoveSpeed = -defaultMoveSpeed;

        if (defaultMoveSpeed < 0)
        {
            isFacingLeft = true;
            enemyUtil.FlipSprite();
        }
        if (defaultMoveSpeed > 0)
        {
            isFacingLeft = false;
            enemyUtil.FlipSprite();
        }
         else { return; }
    }

    void StartAgroTimer()
    {
        if (aggroTimeLeft == defaultAgroTime || aggroTimeLeft > 0)
        {

            aggroTimeLeft -= Time.deltaTime;
            Physics2D.IgnoreCollision(enemyWallDetectors[0], terrainCol);
        }
        if (aggroTimeLeft <= 0)
        {
            Physics2D.IgnoreCollision(enemyWallDetectors[0], terrainCol, false);
            StartPatrolling();
        }

    } 



    bool CanSeePlayer(float distance)
    {
        bool val = false;
        float castDist = distance;

        if (isFacingLeft)
        {
            castDist = -distance;
        }
        else
        {
            castDist = distance;
        }
        Vector2 endPos = castPoint.position + Vector3.right * castDist;
        RaycastHit2D hit = Physics2D.Linecast(transform.position, endPos, 1 << LayerMask.NameToLayer("Player") |
            1 << LayerMask.NameToLayer("StaticTerrain"));                                                            


        if (hit.collider != null)
        {
            if (hit.collider.gameObject.CompareTag("Player"))
            {
                val = true;
                aggroTimeLeft = defaultAgroTime;
            }
            else
            {
                val = false;
            }
            Debug.DrawLine(castPoint.position, hit.point, Color.red);
        }
        else
        {
            Debug.DrawLine(castPoint.position, endPos, Color.blue);
        }

        if(isAgro)
        {
            ChasePlayer();
        }

        return val;
    }
}

Hi Trolly,

Have you already tried to add Debug.Logs to your code to see what is going on during runtime?

The reason for the enemy defying gravity might be this line: enemyRB.velocity = new Vector2(-(currentMoveSpeed * speedModif), 0);. The gravity affects the velocity but since you override the velocity value with hard-coded values, the gravity will not affect the player anymore.

Is the enemy supposed to be able to leave the platform? Is it supposed to be able to detect the player on the other platform? If not, maybe you could find a solution by (re)calculating the length of the ray depending on the respective situation.

Good luck! :slight_smile:


See also:

I just realized that I did not explain what I want the code to do when the enemy hits a dead end, which… kinda makes half of my earlier post redundant, sorry about that… :sweat_smile:

But to answer your question: No, the enemy is not supposed to leave the platform, and the only reason the raycast is long enough to spot the player from its platform to the one the player is on is done for testing purposes. (Although making the raycast scale with the size of the platform/shrink the raycast when it reaches the end-point of the platform is on the todo list as well.)

My initial idea was that as the player is in line of sight of the enemy, it’ll attempt to catch them, if they get away, a timer starts ticking. As long as the timer persists, the enemy will try to catch the player, if they hit a dead end (checked by the collider with OnTriggerExit2D) they should stop and/or turn around, but keep the aggro till the timer runs out.

The (unarguably lousy…) way I hoped that it would work is that the box collider would check if its not touching the ground layer (similar to how the regular movement works), force it to turn around, and then have the chase method force it to turn back at the player, which should again trigger the collider and make it turn around and repeat that process until the timer’s over. Evidently, my plan on being lazy with this type of enemy backfired and I’m left wondering what to do. Tried a couple of ways to fix it - nothing worked out so far.

That being said… After sleeping on it and explaining all of this, I might have an idea: maybe I could pass another method that would preemptively stop the aggro timer if the enemy is touching a ledge (Or in the collider’s case: stop the timer if its not touching the ground)and make it go back into its regular patrol state.

I’ll try that and post an update if it solves the issue, but if there’s any better way to make this work, i’d love to hear any kind of hint or thought behind what could actually solve the problem

Yes, you definitely need a condition that prevents the enemy from chasing the player if the enemy’s periscope collider is not touching the ground anymore. Define those edge cases in your code, and your idea will very likely work. At the moment, StartAgroTimer will get executed always as long as isAgro and isSearching are true and if CanSeePlayer(aggroRange) is false.

This piece of code makes no sense to me, by the way:

isSearching = true;
if (isSearching) { }

You don’t need a conditon if isSearching gets always gets set to true.

1 Like

I forgot to remove that bool. That’s a remnant on one of the many attempts :tm: to fix this thing before sending in the SOS on the forum.

The code partly works now. If anything, the enemy doesn’t jump from the platform anymore, but now the isAggro bool doesn’t want to set itself to true in the code.

I think I narrowed the problem down. The bool method I wrote as a stop condition is not working right. its constantly switching from true to false while the raycast hits the player, which jumps to if (IsTimerOff(true)) which leads to isAggro to never turn on.

Sending in the whole code again, because there’s been a couple of changes.

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class HellhoundMovement : MonoBehaviour
{
    [Header("Components Outside GameObj")] //...mostly outside the gameObj

    [SerializeField] CompositeCollider2D terrainCol;
    [SerializeField] Transform player;
    [SerializeField] Transform castPoint;

    [Header("Aggro Settings")]

    [SerializeField] float aggroRange;
    [SerializeField] float defaultAgroTime = 3f;
    [SerializeField] float aggroTimeLeft;

    [Header("Scripts and Components")]
    Rigidbody2D enemyRB;
    Animator enemyAnim;
    [SerializeField] BoxCollider2D[] enemyWallDetectors;
    EnemyUtil enemyUtil;

    [Header("Movement Speed Settings")]
    [SerializeField] public float defaultMoveSpeed;
    [SerializeField] float currentMoveSpeed;
    [SerializeField] float speedModif = 2;

    [Header("gameObj checks")]

    [SerializeField] public bool isFacingLeft = false;
    [SerializeField] public bool enemyIsAlive = true;
    [SerializeField] bool isAggro;
    [SerializeField] bool stopAggroTimer;

    private void Start()
    {
        enemyRB = GetComponent<Rigidbody2D>();
        enemyAnim = GetComponent<Animator>();
        enemyWallDetectors = GetComponents<BoxCollider2D>();

        enemyUtil = GetComponent<EnemyUtil>();

        currentMoveSpeed = defaultMoveSpeed;
        aggroTimeLeft = defaultAgroTime;

        //enemyRB.constraints = RigidbodyConstraints2D.FreezeRotation | RigidbodyConstraints2D.FreezePositionY;
    }

    void Update()
    {
        if (CanSeePlayer(aggroRange))
        {
            ChasePlayer(); 
        }
        else
        {
            StartPatrolling();
        }

    }
    private void ChasePlayer()
    {

        isAggro = true;
        StartAggroTimer();
        enemyAnim.speed = 2f;
        if (!enemyIsAlive)
        {
            enemyRB.constraints = RigidbodyConstraints2D.FreezePositionX;
            return;
        }
        else if (transform.position.x < player.position.x)
        {
            enemyRB.velocity = new Vector2((currentMoveSpeed * speedModif), 0);
            enemyUtil.FlipSpriteToPos();
            isFacingLeft = false;

        }
        else
        {
            enemyRB.velocity = new Vector2(-(currentMoveSpeed * speedModif), 0);
            enemyUtil.FlipSpriteToPos();
            isFacingLeft = true;
        }
    }

    private void StartPatrolling()
    {
        enemyAnim.speed = 1f;
        if (!enemyIsAlive)
        {
            enemyRB.constraints = RigidbodyConstraints2D.FreezePositionX;
            return;
        }
        else
        {
            enemyRB.constraints = RigidbodyConstraints2D.FreezePositionY | RigidbodyConstraints2D.FreezeRotation;
            enemyRB.velocity = new Vector2((defaultMoveSpeed), 0);
            enemyUtil.FlipSpriteToPos();

            if (enemyRB.velocity.x > 0)
            {
                isFacingLeft = false;
            }
            else
            {
                isFacingLeft = true;
            }
        }
    }
    private void OnTriggerEnter2D(Collider2D collision)
    {

        if (enemyWallDetectors[1].IsTouchingLayers(LayerMask.GetMask("StaticTerrain")))
        {
            CheckSpriteAfterCollision(collision);
        }
    }
    private void OnTriggerStay2D(Collider2D collision)
    {
        if (enemyWallDetectors[1].IsTouchingLayers(LayerMask.GetMask("StaticTerrain")))
        {
            CheckSpriteAfterCollision(collision);
        }
    }
    private void OnTriggerExit2D(Collider2D collision)
    {
        if (!enemyWallDetectors[0].IsTouchingLayers(LayerMask.GetMask("StaticTerrain", "OneWayPlatform")))
        {
            if (isAggro && CanSeePlayer(aggroRange))
            {
                Debug.Log("Is Aggro = " + isAggro);
                IsTimerOff(true);
                CheckSpriteAfterCollision(collision);
            }
            else if (isAggro && !CanSeePlayer(aggroRange))
            {
                Debug.Log("Is There");
                Debug.Log("Is Aggro = " + isAggro);
                IsTimerOff(true);
                CheckSpriteAfterCollision(collision);
            }
            else
            {
                CheckSpriteAfterCollision(collision);
            }
        }
    }
    private void CheckSpriteAfterCollision(Collider2D collision) //not sure how to call this method TBH
    {
        defaultMoveSpeed = -defaultMoveSpeed;

        if (defaultMoveSpeed < 0)
        {
            isFacingLeft = true;
            enemyUtil.FlipSprite();
        }
        if (defaultMoveSpeed > 0)
        {
            isFacingLeft = false;
            enemyUtil.FlipSprite();
        }
         else { return; }
    }
    void StartAggroTimer()
    {
        if (aggroTimeLeft == defaultAgroTime || aggroTimeLeft > 0)
        {

            aggroTimeLeft -= Time.deltaTime;
            Physics2D.IgnoreCollision(enemyWallDetectors[0], terrainCol);
        }
        if (IsTimerOff(true))
        {
            aggroTimeLeft = 0;
            IsTimerOff(false);
        }
        if (aggroTimeLeft <= 0)
        {
            Physics2D.IgnoreCollision(enemyWallDetectors[0], terrainCol, false);
            isAggro = false;
            StartPatrolling();
        }
    }
    bool IsTimerOff(bool value)
    {
        Debug.Log(value);
        if (value)
        {
            return true;
        }
        else
        {
            return false;
        }
    }
    bool CanSeePlayer(float distance)
    {
        bool val = false;
        float castDist = distance;

        if (isFacingLeft)
        {
            castDist = -distance;
        }
        else
        {
            castDist = distance;
        }
        Vector2 endPos = castPoint.position + Vector3.right * castDist;
        RaycastHit2D hit = Physics2D.Linecast(transform.position, endPos, 1 << LayerMask.NameToLayer("Player") |
            1 << LayerMask.NameToLayer("StaticTerrain"));                                                            


        if (hit.collider != null)
        {
            if (hit.collider.gameObject.CompareTag("Player"))
            {
                val = true;
                aggroTimeLeft = defaultAgroTime;
            }
            else
            {
                val = false;
            }
            Debug.DrawLine(castPoint.position, hit.point, Color.red);
        }
        else
        {
            Debug.DrawLine(castPoint.position, endPos, Color.blue);
        }

        return val;
    }
}

What is the purpose of IsTimerOff()? It does nothing but print to the console. Any check to it will always return the parameter

if (IsTimerOff(true))
{
    // This will always run
}
if (IsTimerOff(false))
{
    // This will never run
}

You have this

if (IsTimerOff(true))
{
    // This will ALWAYS run, so aggroTimeLeft will ALWAYS be set to 0
    aggroTimeLeft = 0;
    // This does nothing. It will print 'false' to the console, but has no effect on anything
    IsTimerOff(false);
}

You also use it here but again, it does nothing in this case

if (isAggro && CanSeePlayer(aggroRange))
{
    Debug.Log("Is Aggro = " + isAggro);
    IsTimerOff(true); // <- THIS DOES NOTHING
    CheckSpriteAfterCollision(collision);
}
else if (isAggro && !CanSeePlayer(aggroRange))
{
    Debug.Log("Is There");
    Debug.Log("Is Aggro = " + isAggro);
    IsTimerOff(true); // <- THIS DOES NOTHING
    CheckSpriteAfterCollision(collision);
}
else
{
    CheckSpriteAfterCollision(collision);
}

Basically, StartAggroTimer() never starts the timer because you are always setting aggroTimeLeft to 0 in that method

It sounds really dumb, but I hoped that I can use the method in the place of a class-level bool. When I declare stopAggroTimer and use it in place of IsTimerOff() the code breaks again.

Draw your logic flow with a pen on paper without taking your existing code into consideration. Just the logic. Once you did that and once it makes sense in theory, you could either check your code again, or you could comment your code out and implement the logic flow that you drew on paper. Make the relevant messages appear in your console. Then readd your relevant code bit by bit and test it bit by bit.

Guess i’ll have to do that (Though I haven’t been writing the logic on paper, I have been debugging like crazy the last two days). Oh well, back to the drawing board it is. Thanks for the help and sorry for the time-waste.

In that case, comment your current solution out and rewrite it from scratch based on the logic on paper. For rather short solutions, that’s often faster than trying to find a bug staring at the same code the entire time. Don’t forget to define the edge cases on paper too.

Well, I’m back. I didn’t get to rewrite the code from scratch, cause I finally managed to catch the problem. For one, I feel so dumb because Apparently my reading comprehension has gone down the drain.

StartAgroTimer will get executed always as long as isAgro and isSearching are true and if CanSeePlayer(aggroRange) is false.

You even told me that, and I yet still overlooked it like it was working exactly the way I wanted when obviously it didn’t. ˢᶦᵍʰ, ᶦᵐ ˢᵒ ᵈᵘᵐᵇ.

Anyway, Setting an || isAggro in Update()'s if statement solved most of my problems.

 void Update()
    {
        if (CanSeePlayer(aggroRange) || isAggro)
        {
            ChasePlayer();
            StartAggroTimer();
        }
        else
        {
            StartPatrolling();
        }
    }

I removed the illogical bool method and went back into using the class-level stopAggroTimer in place of wherever I put IsTimerOff().

I Removed the second collider (It was a band-aid solution to another problem that… after testing the current code without it, it no longer happened) (This one is more of a gut feeling than actual debugging, but something tells me that it was also causing some weird issues with the code as well.) Once the collider was removed, I no longer needed the OnTriggerEnter and OnTriggerStay methods, so those got axed as well.

Everything else is left mostly as it is.

So TLDR is: IT FINALLY WORKS! :tada:

Finally! Good job on fixing the problem! :slight_smile:

That’s a veeeeery common mistake that happens to everybody. Never assume that something is working just because it looks as if it might be working. Always check “everything” to be on the safe side.

1 Like

This topic was automatically closed 24 hours after the last reply. New replies are no longer allowed.

Privacy & Terms