Quest Debugging System

Yup. Here’s the entire script, if needed:

using System.Collections.Generic;
using System.Linq;
using RPG.States.Enemies;
using RPG.States.Player;
using UnityEngine;

// This function, unlike other attacking states, is simply here to allow the boat to be capable of
// Ramming other boats

public class BoatAttackingState : BoatBaseState
{
    AllyBoatPosition[] allyBoatPositions;

    // The list of boat allies that are alive
    // (if none are left, this boat has no permission to be in any movement-based state.
    // Will be applied in 'BoatChasingState.cs', 'BoatAttackingState.cs' and 'BoatBackingUpState.cs')
    List<EnemyStateMachine> aliveBoatAllies = new List<EnemyStateMachine>();


    public BoatAttackingState(BoatStateMachine stateMachine) : base(stateMachine)
    {
        // Cache the ally boat positions
        allyBoatPositions = stateMachine.GetComponentsInChildren<AllyBoatPosition>();
    }

    public override void Enter()
    {
        Debug.Log($"{stateMachine.gameObject.name} has entered the boat attacking state");
        stateMachine.OnVictimBoatTilted += HandleBoatCollision;
    }

    public override void Tick(float deltaTime)
    {
        // If the last attacker is not driving a boat,
        // (or on one, which is automatically checked for
        // after checking for whether the last attacker is driving a boat)
        // get back to mounted state (which will automatically
        // take you to chasing state)
        if (!IsLastAttackerDrivingBoat())
        {
            stateMachine.SwitchState(new BoatMountedState(stateMachine));
            return;
        }

        // -------------------------------------------------------------------- IF THE BOAT HAS NO ALLIES ON HIM, SWITCH TO THE MOUNTED STATE, SINCE IT CAN NO LONGER BE DRIVEN -------------------------------------------------------------------------

        // If the boat has no driver, then it shouldn't move (because nobody is driving it)

        aliveBoatAllies = stateMachine.GetComponentsInChildren<EnemyStateMachine>().ToList();
        bool allAlliesDead = aliveBoatAllies.All(ally => ally.Health.IsDead());

        if (allAlliesDead)
        {
            Debug.Log($"No Boat Allies are left alive, switching to boat mounted state");
            stateMachine.ClearBoatPatrolPath(); // Clear the patrol path first, so it doesn't accidentally go to patrolling state (it's an empty boat)
            stateMachine.SwitchState(new BoatMountedState(stateMachine));
            return;
        }

        // ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

        // Assuming the last attacker is on a boat,
        // go for the RAMMING attempt
        RamLastAttackerBoat(deltaTime);

        // -------------------------------------------------------------------- BOAT ALLIES -----------------------------------------------------------------------------------------------------------

        // The boat allies need to be in the exact same position as their spots (i.e: Get the boat NPC allies to move along with the boat)
        if (allyBoatPositions != null)
        {
            foreach (AllyBoatPosition allyBoatPosition in allyBoatPositions)
            {
                if (allyBoatPosition.GetComponentInChildren<EnemyStateMachine>() != null && // rotation is only for boat allies, which are children of the boat
                allyBoatPosition.GetComponentInChildren<EnemyStateMachine>().GetLastAttacker() == null)
                {
                    allyBoatPosition.GetComponentInChildren<EnemyStateMachine>().transform.position = allyBoatPosition.transform.position;
                    allyBoatPosition.GetComponentInChildren<EnemyStateMachine>().transform.rotation = allyBoatPosition.transform.rotation;
                }
            }
        }

        // --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
    }

    public override void Exit()
    {
        // Not much to do when the State Machine exits here
        stateMachine.OnVictimBoatTilted -= HandleBoatCollision;
    }

    /// <summary>
    /// Is the Last Attacker driving a boat? (Checks for
    /// the parent of the Last Attacker, whether it has a 'BoatStateMachine.cs' or not)
    /// </summary>
    /// <returns></returns>
    private bool IsLastAttackerDrivingBoat()
    {
        var lastAttacker = stateMachine.GetBoatLastAttacker();

        if (lastAttacker != null && lastAttacker.GetComponentInParent<BoatStateMachine>() != null)
        {
            return true;
        }

        return IsLastAttackerOnBoat(); // If he wasn't driving a boat, at least is he on a boat?
    }

    /// <summary>
    /// Is the Last Attacker on a boat? (Does an overlap sphere check to determine if the
    /// Last Attacker has a boat he's on, so we can RAM it, or not)
    /// </summary>
    /// <returns></returns>
    private bool IsLastAttackerOnBoat()
    {
        // This is untested, as the bug is quite rare. If it happens, 
        // though, we'll get back to investigate this block
        if (stateMachine.GetBoatLastAttacker() == null)
        {
            Debug.Log($"The boat's Last Attacker is dead, so the last attacker is not on a boat");
            return false; // Kills a NullReferenceException if the Boat's Last Attacker is null
        }

        // Is there a boat anywhere near the last attacker?
        float detectionRadius = 5.0f; // Reduce this from 5 to 1, if needed
        Vector3 lastAttackerPosition = stateMachine.GetBoatLastAttacker().transform.position;
        Collider[] hitColliders = Physics.OverlapSphere(lastAttackerPosition, detectionRadius);

        foreach (Collider collider in hitColliders)
        {
            if (collider.GetComponentInParent<BoatStateMachine>() != null)
            {
                return true;
            }
        }

        return false;
    }

    private void RamLastAttackerBoat(float deltaTime)
    {
        var targetBoat = stateMachine.GetBoatLastAttacker().GetComponentInParent<BoatStateMachine>();

        if (!stateMachine.Agent.enabled) stateMachine.Agent.enabled = true; // If you turn this off, the code below won't work

        if (targetBoat != null)
        {
            var playerOnBoat = targetBoat.GetComponentInChildren<PlayerStateMachine>();
            if (playerOnBoat != null)
            {
                stateMachine.Agent.SetDestination(targetBoat.transform.position); // If you turn this off, the boat won't even try to reach you

                Vector3 direction = stateMachine.Agent.desiredVelocity; // The direction the NavMeshAgent prefers to use to reach its goal
                direction.y = 0; // Eliminate the y-axis (no flying stuff for boats, PLEASE!)
                Move(direction.normalized * stateMachine.MovementSpeed, deltaTime); // If you turn this off, the boat will hug you (don't want that)
                stateMachine.Agent.velocity = direction * stateMachine.MovementSpeed; // The velocity of the boat each frame
                stateMachine.Agent.nextPosition = stateMachine.transform.position; // The next position for the NavMeshAgent to go towards (if it's not itself, it'll teleport. Don't want that!)
                Debug.Log($"{stateMachine.gameObject.name} is moving towards {stateMachine.GetBoatLastAttacker().gameObject.name}, BoatAttackingState");
            }
            else
            {
                stateMachine.Agent.ResetPath(); // Don't try to move, because the player is not on the opponent boat (i.e: his allies are fighting you, so you should stop moving too)
                Debug.Log($"{stateMachine.GetBoatLastAttacker().gameObject.name} is off the boat, resetting NavMeshAgent path, BoatAttackingState");
            }
        }
        else
        {
            stateMachine.Agent.ResetPath(); // Don't try to move, because the player is not on the opponent boat (i.e: his allies are fighting you, so you should stop moving too)
            Debug.Log($"TargetBoat not found. Resetting NavMeshAgent path, BoatAttackingState");
        }
    }

    private void HandleBoatCollision(Collider thisCollider)
    {
        Debug.Log($"{thisCollider.gameObject.name} is heading to Boat Backing Up State");
        thisCollider.GetComponent<BoatStateMachine>().SwitchState(new BoatBackingUpState(stateMachine));
    }
}

We’re 66 days late for that. Been working on it since September 1st, literally. I’m like 93% done with the basic system. I might as well just get the job done now, considering that it (mostly) works quite well, for now at least. I just need a better escape algorithm, because mine is a little messy (I wrote a new one, and for now it seems to be doing the job fine… Probably a little computationally expensive though), fix a few bugs and hopefully it’ll be done

I’ll say this though, the issue was significantly reduced by just reducing the radius and height of the NavMeshAgent itself in the hierarchy. If the AI boat gets too close, and we dismount and don’t mount again quickly, the problem arises again, but if we are quickly mounting and dismounting with a much smaller Agent, then the AI boat will perform my Ramming operation just fine, and this problem will be significantly less visible

BUT… YOU NEED TO TRY AND ERROR A LOT BEFORE YOU GET THE VALUES. TOO BIG, OR TOO SMALL, WILL BOTH RESULT IN THE ERROR SHOWING UP

For example, this is what worked for my test boat (circled in red):

And earlier today, I wrote an interface to get the boat to accurately swap its chase range if the player is the last attacker, depending on whether he’s driving a boat or not. That one took me months to fix… :sweat_smile: (and I am sure it’ll need some fixing again when the game expands down the line)

(For context, ‘OnVictimBoatTilted’ is an event in ‘BoatStateMachine.cs’. It’s part of 3 events which are called when the boat gets hit to make it tilt. “HandleBoatCollision”, the subscribed event, ensures the AI boat backs off and then RAMs the victim again, so that the player has a fair battle. It’s a little complex to explain here, but it works perfectly fine. Just know the ships can bump you, back off for a few seconds and then bump you again)

Anyway, @Brian_Trotter - any idea how to solve this problem, please? I’ll reference the problem here:

It breaks the game, and I am genuinely clueless as of how to fix it (basically, I can’t set the destination of the stateMachine’s Agent, and the compiler constantly complains that the Agent is not on a NavMesh, although the NPC is on the ground)

It’s definitely not an NRE you’re getting… is this a yellow or red error message? Yellow messages can be ignored because on the next frame, the NavMeshAgent will move the character to a spot on the NavMesh. Red NavMesh errors are a bit trickier. For some reason, your character is no longer on a NavMesh, so you have to get him BACK to a NavMesh…

One way is to try to use RaycastNavMesh. Refer to Sam’s video in the Core Combat course when he’s validating move input. This will find the closest point on the NavMesh from a given location. If it can’t find one nearby, it returns false. Now here’s the deal, you can increase that acceptance radius, giving you a better shot at finding a nearby location. If the first one fails, double the acceptance radius. Keep doing this until the agent finds a location, then warp the character to that location (actually, if you’re off the NavMesh, you might be forced to disable the NavMeshAgent (and CharacterController if you’re using one), move the character, then re-enable these components.

Red

Can I please have the Unity variant for that lecture again?

The Unity variant?
Not quite sure what you mean…
The RaycastNavMesh function gives you the nearest point on the Navmesh to the point you’re searching for, as long as it’s within the acceptance radius.

@Brian_Trotter Udemy* lecture, my bad. I’ve been coding a little too hard recently I make a lot of mistakes nowadays… :sweat_smile:

Can I please have the link to the Udemy lecture again?

(I googled up ‘RaycastNavMesh Function Unity’, can’t find anything there regarding that function name)

Edit: OH WAIT… ‘RaycastNavMesh’ is in ‘Mover.cs’ from the course, alright I’ll work on that later

1 Like

@Brian_Trotter how about this solution? I’m not sure if it works perfectly or not, since the bug happens occasionally, but it still happens from time to time, but I want to confirm if this solution works or not:

        private void FollowTargetPoint(float deltaTime)
        {
            if (!stateMachine.Agent.enabled) stateMachine.Agent.enabled = true;

            // Get the current target point position
            Vector3 targetPosition = stateMachine.GetCurrentTargetPoint().transform.position;

            // Calculate the distance to the target point
            float sqrDistanceToTarget = Vector3.SqrMagnitude(stateMachine.transform.position - targetPosition);

            // If the enemy is close enough to the target point, switch to idle state
            if (sqrDistanceToTarget < 0.1f * 0.1f)
            {
                stateMachine.Agent.velocity = Vector3.zero;
                stateMachine.Animator.SetFloat(FreeLookSpeedHash, 0f);
                FaceTarget(stateMachine.Player.GetComponent<PlayerStateMachine>().transform.position, deltaTime);
                stateMachine.SwitchState(new EnemyIdleState(stateMachine));
                return;
            }

            // ----------------------------- TEST ZONE 15/11/2024 ------------------------------------------------------------------------

            // Validate that the NPC's current position is on the NavMesh Agent
            Vector3 validPosition;
            if (!ValidateNavMeshPosition(stateMachine.transform.position, out validPosition))
            {
                Debug.Log($"NPC is not on a valid NavMesh Point. Adjusting position");
                stateMachine.Agent.Warp(validPosition);
                stateMachine.SwitchState(new EnemyFollowingPlayerState(stateMachine));
                return;
            }

            // ---------------------------------------------------------------------------------------------------------------------------

            // Set the target point's position as the destination
            // stateMachine.Agent.destination = targetPosition;
            stateMachine.Agent.SetDestination(targetPosition);
            
            // Calculate the desired velocity for movement
            Vector3 desiredVelocity = stateMachine.Agent.desiredVelocity.normalized;

            // Move the enemy towards the target point
            Move(desiredVelocity * stateMachine.MovementSpeed, deltaTime);
            stateMachine.Agent.velocity = desiredVelocity * stateMachine.MovementSpeed;
            stateMachine.Agent.nextPosition = stateMachine.transform.position;
        }

        // TEST FUNCTION - 15/11/2024
        private bool ValidateNavMeshPosition(Vector3 position, out Vector3 validPosition)
        {
            validPosition = Vector3.zero;

            NavMeshHit navMeshHit;
            bool isValid = NavMesh.SamplePosition(position, out navMeshHit, 1.0f, NavMesh.AllAreas);

            if (!isValid) return false;

            validPosition = navMeshHit.position;

            NavMeshPath path = new NavMeshPath();
            bool hasPath = NavMesh.CalculatePath(stateMachine.transform.position, validPosition, NavMesh.AllAreas, path);

            if (!hasPath || path.status != NavMeshPathStatus.PathComplete) return false;

            return true;
        }

I have no way of telling if it works or not unfortunately, but I’m just trying

And everytime it happens, it rotates the character in a weird way… How do I solve that too?

You’re the only one who can tell, as you’re the one running the game. The code looks right, from the assumptions I’m aware of.

Not a clue. It shouldn’t be rotating the character at all from what I can see.
One change I would consider is changing the stateMachine.Agent.Warp() to the old disable agent/move the character to the new position/enable agent. This is because Warp may not like that you’re not on the NavMesh to begin with. I can’t be positive, though.

It occurs to me that if there is some code running that keeps the character facing a certain target, then when the character is moved back on to the NavMesh, that may be turning the character to face a look target… that may be what’s turning your character in odd ways.

@Brian_Trotter OK so basically coding any sort of solutions for this specific problem seems really troublesome… I think my best bet is to just increase the base offset a tiny bit for all of the humanoid agents. For me, I incremented the base offset by 0.1 for all NPCs. Are there any risks to this?

I deleted all added code for this problem and just adjusted the base offset, something I learned when making the boat agents, and so far it seems to be doing the job perfectly, as this bug is probably a bug from Unity’s side instead of mine, without any visual issues when walking around the scene, but unfortunately it’s a game-breaking bug.

It’s one of these things usually solved by going to the main menu and coming back

Privacy & Terms