State Machine Update

I have been on fire adding functionality to my project. Loving it. So I am on to adding a “Composure” vital stat… Like a few other games, notably Outward and Souls games… If you block you still lose some of it… You get knocked back(and down for me) if you run out… It regenerates very quickly out of combat and pretty much instantly after you get knocked down. Composure.cs is one of my most elegant scripts I’ve written on my own.

However… In order for it to work I need to know if an enemy or player is in combat. And to keep Composure universal between the two types (player and enemy) I want the statemachines to call and tell it when combat is or isn’t happening rather than having an if block to check a PSM vs ESM. I think I landed on some decent logic for telling in combat. For the player it (will be) a list of enemies that are either attacking or in chasing state that fall of after X amount of seconds… But what I am testing first… Is flagging when an enemy takes damage, is chasing, or is attacking. Then after 5 seconds of not doing any of those things it will be out of combat.

Long story short… I am trying to keep track of the enemy’s last time they made a combat action inside EnemyStateMachine so it can be universal across all their states. However… It seems like ANYTHING in Update in the main StateMachines makes the character frozen. I tested this by simply throwing a debug.log in the player state machine… Sure enough… can’t move…

Am I total noob? Why is this the case? And bonus points if you can suggest a place that I try this logic(Though I suppose a general use Combat.cs slapped on to everyone and interfaces with the appropriate SM’s would be fine). I do just about EVERY main action from the PSM such as jump, dodge, etc because I want them available in any state (and there is a hefty IsPerformingAction() check I do to stop you from dodging in mid air for example). I also chose this route because I want to be able to interrupt animations to block or dodge anyway. These all work great but don’t need an update tick. Sigh.

Yeah I made an EnemyCombat.cs for tracking combat conditions and I believe it is working just fine. Got my Composure working with a rudimentary bar slapped into the HealthBar we made in the RPG course that is now VitalsBar:

This is because the ancestor of the Player and Enemy StateMachine is the StateMachine.cs which already has an Update where all that neat Tick() stuff is called… If a child class has an Update() method, it runs, but the StateMachine.cs method doesn’t, as it’s hidden.

There are two solutions to this…

Option 1

You can make the Update in StateMachine.cs a virtual method ``` protected virtual void Update() ``` Adding the virtual keyword means that unless the method is overriden, this one will be the one that is run, and make it possible to override the update method in a child class while still having the original code run. We need to make it protected because child classes can't call private classes in the parent, but they can call protected ones. In fact, you'll find that private virtual won't compile because of this. So in PlayerStateMachine/EnemyStateMachine, your Update method will look like this: ``` protected override void Update() { base.Update(); //Now do whatever you wanted to do in your Update() } ```

Option 2

Create a class that monitors the current state of combat. Combat actions report a combat event to the class and an Update() method in that class can manage the current state of combat. This is my preferred method, as it adheres to a single responsible principle.

Thanks for the concise answer, Brian. I don’t believe this was mentioned in the course, but it certainly makes sense. And you are right, it makes more sense to have a Combat monitor. However, since the player flagging in combat will be determined differently than the Enemy, I will be using two separate classes for it. They will just work fundamentally different. Then as usual, I’ll add that class as a field for the Player/Enemy StateMachines so it can be accessed elsewhere. BUT also… They will still reach out to the Composure class to tell it if Combat is present because it is so lame for it to have to make a check if it is a Player or Enemy to then reach out to the correct StateMachine to find out if Combat is present. It’s a twisted web at that point.

Hey Brian…

I had forgotten to ask if there is a more efficient way of doing what I am doing. I feel like having a check in Update every frame that also sets the bool based on the check is overkill but I am not sure if there is a better way of doing it.

public class EnemyCombat : MonoBehaviour
{
    private EnemyStateMachine stateMachine;
    private float timeSinceLastCombatAction = Mathf.Infinity;
    private bool isInCombat = false;

    private void Start()
    {
        stateMachine = GetComponent<EnemyStateMachine>();
    }

    private void Update()
    {
        if (isInCombat) timeSinceLastCombatAction += Time.deltaTime;

        if (timeSinceLastCombatAction < stateMachine.CombatThresholdTime) isInCombat = true;
        else isInCombat = false;

        stateMachine.SetCombat(isInCombat);
    }

    /// <summary>
    /// Lets state machine know that enemy is attacking, chasing, or being attacked to flag combat
    /// </summary>
    public void TriggerCombatAction()
    {
        timeSinceLastCombatAction = 0f;
    }
}

TriggerCombat is called when the enemy is chasing, attacking, or if he gets hit (goes into ImpactState). The SetCombat in EnemyStateMachine keeps a variable there for if I need to check somewhere else if it is in combat as well as telling the Composure script that combat is on. So I do only have to tell that script when it actually changes at least.

(For my PlayerCombat.cs I’m going to use a list of all “combatants” engaging me and whenever I remove someone I’ll check to see if the list is now empty and then flag out of combat if so which will let me NOT have to check in Update. Working on that now and see how that goes.)
UPDATE: HAHAHA… Well I thought it wasn’t working… But my check to turn off a combat indicator asked for LESS THAN 0 rather than 1 (or equal to 0) on the list count. That was fun. Changing to different classes to add and finally faking some GUIDs because I thought that was the issue with uniqueness. It still might NEED GUIDs and I will have proper ones later anyway, though. So no biggie.

What if I told you you only need one CombatMonitor?

public class CombatMonitor : MonoBehaviour
{
     [SerializeField] float combatThresholdTime = 5.0f;
     
     float timeSinceLastCombatAction = Mathf.Infinity;
     
     public bool IsInCombat => timeSinceLastCombatAction < combatThresholdTime;

     public event System.Action OnCombatEntered;
     public event System.Action OnCombatEnded;

     public void TriggerCombatAction()
     {
          if(!IsInCombat) OnCombatEntered?.Invoke();
          timeSinceLastCombatAction = 0f;
     }

     private void Update()
     {
          if(!IsInCombat) return;
          timeSinceLastCombatAction += Time.deltaTime;
          if(!IsInCombat) OnCombatEnded?.Invoke();
     }
}

Both Enemy and Player states which infer combat related should call this in their Tick() method.

stateMachine.CombatMonitor.TriggerCombatAction();

Enemy states that infer combat should also get the CombatMonitor of the player and call TriggerCombatAction() on that CombatMonitor.

Any class that needs to know the Combat state can use

stateMachine.CombatMonitor.IsInCombat;

Alternatively, you could add this to StateMachine

public bool IsInCombat => CombatMonitor.IsInCombat;

The events will let you react to the presense of combat in real time… when combat starts, you could automatically blend the music into combat music. When it ends, you could blend it back… or it could be used for AI, etc.

No need to register a list of enemies in combat, as any enemy that is in combat with the player will simply be resetting the clock. When the clock expires, you’re not in combat.

Wow… I am simultaneously impressed and nauseous at the prospect of reworking in now that I got it working exactly as I wanted last night.

I am also trying to make sure I grasp it all. So theoretically… I would be JUST calling TriggerCombatAction() on BOTH the current enemy and the player wherever I am currently calling my setup for just the enemies? My hacking for adding enemies to a list for the player can just be completely ignored. (Though they are essentially in the same places).

It’s so backwards from what I set up. Hopefully I will just back up what I have and try to implement. I am terrified of this since I did a good bit of testing on how I have it set up and it works. As of right now, the combat helpers call a SetCombat method in the appropriate state machines which sets a bool AND tells the Composure script if we are in combat or not. I think I should have it, but ugh it works :joy:

EDIT: Ah… so yeah… Composure still won’t like this. It needs to know if it is looking at a Player or an Enemy StateMachine to check for combat.

    private void Update()
    {
        if(GetCurrentComposure() == 0)
        {
            onKnockdown?.Invoke();
            StartCoroutine(RefillComposure());
        }

        tick -= Time.deltaTime;

        if(tick <= 0f)
        {
            if(!isKnockedDown)
            {
                if (isInCombat) RegenComposure(combatComposureRegen);
                else RegenComposure(outOfCombatComposureRegen);
            }
            tick = 1f;
        }
    }

It just seems silly to check for PlayerTag and then pull PSM and if not PlayerTag pull ESM.

Though I suppose:

if (GetComponent-CombatMonitor>().IsInCombat)

Yeah… this was broken for me. I appreciate the suggestion, but I don’t want to fix what isn’t broken I suppose. My way might be a little more tedious, but I can track it in my head and do all the things that yours can do as well.

Ok, just dropped that in there as an idea or another way…
Out of curiousity, why does Composure need to know if it’s a PSM or ESM?

And much appreciated you showed another way. Had I started there it may have worked for me.

Composure needs to know if combat is on or off. So it’d have to ask the SM for that answer or to get to the CombatHelper. Which either way it can’t just say statemachine or even “look” for a statemachine since there are two flavors.

Getting the CombatHelper arbitrarily would work fine, just not going via state machine like you had suggested.

I hooked this all up and it made my enemies just not do anything. It’s certainly possible I made a mistake or didn’t unhook all my previous code, though.

I am now on to trying to smoothly fade in and out of regular bgm and combat music. I already have functionality to turn on and off an indicator on the hud for combat so I just have to plug it in there.

I really, really, wish Unity had better native sound handling. And stuff on the asset store is needlessly complicated sometimes. Right now I give everything an audio source and plug in audio clips on EnemyHelper or directly into statemachines to play SFX. I have a BGM object in the player rig that plays the default background music that I am throwing a script on to fade between. Which apparently involves having an extra audio source you add in code since the inspector doesn’t like doing it. I’ll have arrays for level music and combat music variations.

Amen

To my surprise, my combat music script worked flawlessly the first time. It’s needlessly repetitive where I could do some fancy ? operators to check which way I am transitioning, but I just wrote it twice. But nice smooth fade out/fade in. I am happy with that. Just two arrays of BG music and combat music (that I only have one element each in) but I could do some randomizing if wanted. And down the line I could figure out a way to keep track of what music SHOULD be playing easily enough.

I also just did some more surgery adding animation events to stop my enemies from being staggered by me blocking for if I have them performing a combo attack. Things are getting pretty complex… Up to about 3300 lines of code that I wrote(well. that also includes the Input thing that Unity generates… but it doesn’t include any unity libraries or downloaded assets that have code).

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

Privacy & Terms