Skip to content

Panicking! Well, at Least the Crew Are…

The next feature I pulled from the board was adding panicking to the Crew when the enter the Broke Morale state. The concept is that when they break, they pick a random spot away from the caravan, run to it, then cower until either the Player Soothes them or a glimmerstalker gets them.

Determining Where the Crew Runs To

Before we can do anything, the Crew will need to determine where they will run to when panicking. In a previous post, I had put up the following diagram for how we were going to handle selecting the points.

Flee points diagram

The more I thought about this the more I didn't like it. The main reason we wanted to go that route initially was because we wanted to be able to control where the points were in relation to the caravan/sleds. As I really started thinking about how I was going to implement this, the following issues came to light:

  • If there were multiple sleds and the trail was curved, the points could overlap with the sleds in front of or behind the current sled
  • If we wanted to adjust where generally the Crew should run to (angle or distance), we have to manually move each point and make sure to keep symmetry between the two sides
  • We'd have to keep a list of points that each Crew would use

These concerns were enough to make me rethink our approach. We originally wanted to use points because we were not sure if we would have the math chops to dynamically define the areas that the Crew should run in. Well, I decided I didn't care if I had the chops or not and wanted to see what I could come up with.

After quite a bit of hacking various snippets together I came up with the following. The red and cyan lines represent the line of points that the Crew will pick from when deciding where to run.

Gif of adjusting flee point arcs

Andrew had added some logic to determine the "center" of the caravan and I wanted to utilize that as the basis for the calculations, instead of the individual sleds. This would allow us a lot more control overall and would ensure that potential locations wouldn't overlap with the sleds. I also had the thought to use arcs instead of a circle. This would allow us to have gaps directly in front of or behind the caravan, since we wouldn't want them to flee directly in the path of the caravan or too far behind.

I started out with just one 90° arc and got that working. It still checks to see if the point is on the NavMesh and retries if it isn't. Once I got that working, I was able to expand it out to the other side and add the ability to "rotate" the arc. I was really glad to get the rotation in. This will allow us to point the arc more forward, since we want to avoid having the Crew flee too far backwards. The Player will eventually have a "tether" to the caravan to prevent them from running too far away from it. It would be unfair to the Player if the Crew ran directly behind the caravan and got out of range before the Player could rescue them.

Below is the code that gets the random point. It uses a lot less math than I had anticipated but it still took a bit to get working.

public static Vector3 GetRandomPointInArcOnNavMesh(Vector3 origin, float radius, Vector3 baseArcDirection, float arcDirectionOffset, float arcAngle, int maxAttempts = 5, int areaMask = NavMesh.AllAreas)
{
    var arcDirection = Quaternion.Euler(0f, arcDirectionOffset, 0f) * baseArcDirection; // Rotate the arc direction

    arcDirection.y = 0;
    arcDirection.Normalize();

    var halfArc = arcAngle * 0.5f;
    for (var i = 0; i < maxAttempts; i++)
    {
        var randomAngle = Random.Range(-halfArc, halfArc);
        var randomRotation = Quaternion.Euler(0f, randomAngle, 0f);
        var offset = randomRotation * arcDirection * radius;
        var rawPoint = origin + offset;

        if (NavMesh.SamplePosition(rawPoint, out var hit, 1.0f, areaMask))
        {
            return hit.position;
        }
    }

    return Vector3.zero;
}

Here is how that method is used:

private Vector3 GetRandomFleePoint()
{
    var caravanCenter = CaravanManager.Instance.CaravanCenter;
    var directionModifier = CoreUtils.FlipCoin() ? 1 : -1;
    // Need to apply modifier to both direction and offset
    // This ensures symmetry between the two sides
    var arcDirection = caravanCenter.right * directionModifier;
    var arcOffset = _fleeArcDirectionOffset * directionModifier;

    return CoreUtils.GetRandomPointInArcOnNavMesh(caravanCenter.position, _fleeDistance, arcDirection, arcOffset, _fleeArcAngle);
}

I also reworked the "Caravan Center" logic to use a full Transform instead of just returning a Vector3. The intent was to be able to turn the whole double arcs to be perpendicular to the caravan at all times. However, I still have something off with it. It appears that it it rotated the wrong way. In the image below, the green line is where the centerline should be but the magenta line is actually where it is. It appears to be flipped. Should be an easy fix but I'm not that great at Quaternion or Vector logic yet. Someday!

Bug with arcs rotating incorrectly with caravan center

I didn't see that issue until I got around to writing this post, so I'll have to figure that out later.

Reworking the Crew States

I was so gung-ho to figure out the flee point generation that I didn't check to see if the Crew script was actually set up well to handle moving through the different states. Spoiler...it was not! I took a step back from working on the fleeing state specifically to rework the whole script to integrate yet another state machine.

This state machine would be responsible handling the main/high-level state changes for the Crew. We had been using a couple of booleans to determine some of the state (i.e. _isDead). I wanted to get those all replaced with actual states and really just get it working exactly as it did previously. This mostly involved just renaming some methods and changing how they were called. Instead of calling the method directly, we now moved the main state machine to that state and let the Enter/Update/Exit events handle the rest.

private void InitializeMainStates()
{
    var normalState = new SimpleState<CrewState>(CrewState.Normal);
    normalState.OnUpdate += OnUpdateMainStateNormal;
    _mainStateMachine.AddState(normalState);

    var deadState = new SimpleState<CrewState>(CrewState.Dead);
    deadState.OnEnter += OnEnterMainStateDead;
    _mainStateMachine.AddState(deadState);

    MoveToMainState(CrewState.Normal);
}

In the code above, you can see that not every state needed all three event types. CrewState.Dead only needs OnEnter because we can never leave that state and CrewState.Normal only has OnUpdate because it is the default and doesn't have any cleanup/teardown that it needs specifically.

Run Away!

Now that I had the Crew script reworked to be more "stateful" I was able to get back to implementing the Fleeing state. I was able to successfully grab a point on the arcs, now I needed to add in the Fleeing state to the state machine and ensure that it was being hit.

The problem was, unless I wanted to add a console log, I couldn't tell when they entered the Fleeing state. I could assume that it was being entered when their Morale broke but I couldn't be sure. I realized there was a blank spot on the Debug panel under the Crews' names that would be perfect to show the main state in. After slapping some code in, I got the following.

Debug panel showing the main Crew states

Now I could ensure that the Crew was in the state I thought they should be in. Next was getting them to actually run to the point they had selected. This seemed straight-forward on the surface. We had logic to control the animations and since they use a NavMesh we could just set the destination.

private void OnEnterMainStateFleeing()
{
    var fleePoint = GetRandomFleePoint();
    _navMeshAgent.SetDestination(fleePoint);

    _navMeshAgent.speed = _defaultMovementSpeed * _fleeSpeedModifier;
}

private void OnUpdateMainStateFleeing(float obj)
{
    UpdateMovementAnims();

    // Determine when to cower
}

private void OnExitMainStateFleeing()
{
    _navMeshAgent.speed = _defaultMovementSpeed;
}

Well, it was mostly that simple. The difficult part was getting the Speed animation variable to be set correctly. I found the InverseLerp function which allowed me to crunch the current speed down into a 0-1 value, based on the min and max speed that the Crew can move at. Normally, a Lerp function would take two values and a percentage, then return the "interpolated" value based on that percentage. The Inverse Lerp does the inverse of that (shocking, I know!). It takes the first two values like the Lerp but the third value is another "real" value. Then it returns what percentage would be needed to achieve that third value.

var value = Mathf.Lerp(0, 10, 0.5); // Returns 5

var percent = Mathf.InverseLerp(0, 10, 5); // Returns 0.5

Putting it all together, you get this.

private void UpdateMovementAnims()
{
    var currentSpeed = _navMeshAgent.velocity.magnitude;
    var normalizedSpeed = Mathf.InverseLerp(_defaultMovementSpeed, _defaultMovementSpeed * _fleeSpeedModifier, currentSpeed);

    _animator.SetFloat(_speedAnimHash, normalizedSpeed);
    _animator.SetBool(_isMovingAnimHash, IsMoving());
}

This almost worked. Logging the actual values everything looked good but the actual animation only seemed to barely be blending into the run animation. Taking a peek at the animation blend tree, I found that that "Threshold" was 0 for the Walk animation and 3.5 for the Running animation. This didn't look right to me, at least not for using the normalized speed like I wanted to. I changed it to be 0 and 1 for the Walk and Running animations, respectively.

Showing the thresholds in the anim blend tree being set to 0 and 1

With the thresholds adjusted everything was looking like it should. You can see them transition between the Normal and Fleeing states and going from walking to running (and them promptly dying...). Also, I know the run is goofy. That will probably change moving forward.

Gif of the Crew fleeing when they break

Cower in Fear

Now that the Crew would flee correctly, I needed to make them cower once they reached their flee point. A quick check to see if they reached their NavMesh destination and a simple state change and we were looking (mostly) good! They cower but I don't know why they are hovering off the ground a few inches. I wonder if the fix that Andrew did for the dying animation would work for this.

Gif of the Crew cowering once they reach their flee point

A problem I ran into though is that the full cower animation wasn't playing. I narrowed it down to the fact that the IsCowering anim variable was a boolean. When I changed it to a trigger, it played the animation correctly but I couldn't get out of the cowering state (because there isn't really a "negative" state of a trigger). After skimming the internet, I ran across a Unity forum post that recommended disabling the "Can Transition To Self" checkbox on the Transition. This did the trick and allowed the animation to run completely, like in the gif above.

Showing the 'Can Transition To Self' checkbox being unchecked in the anim transition

Soothing Their Fears

Our Crew can flee and cower. They are at the mercy of either the Player to rescue them or a glimmerstalker to eliminate them. However, the Player didn't have any tools to rescue the Crew. From our talk the other day, we had decided that the Player should be able to rescue the Crew by Soothing them. Once Soothed, the Crew would return to their spot by the Sled.

This was achieved by adding an OnSoothe event on the MoraleMechanic script and hooking into that. Then, if we are in the Fleeing or Cowering state, we move to the Returning state.

private void OnSoothed()
{
    if (!_mainStateMachine.IsInState(CrewState.Fleeing) && !_mainStateMachine.IsInState(CrewState.Cowering)) return;

    MoveToMainState(CrewState.Returning);
}

Here you can see that they start running back as soon as they are Soothed.

Gif showing the Crew being Soothed while fleeing

I did run into a problem when Soothing the Crew when they were cowering. I was hoping that they would start running, instead, they just kind of act like they are driving an invisible car back to the sled.

Gif showing a bug where the Crew doesn't stop cowering when they start moving back to the caravan

This turned out to be because I did not have a Transition set up for when IsCowering went back to false, so it was never leaving the Terrified/Cower anim state.

Showing the IsCowering false transition

After adding that in, the Crew came out of the cower like I would expect.

Gif showing the Crew being Soothed while cowering