Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

State machine system #389

Merged
merged 6 commits into from
Jun 29, 2016
Merged

State machine system #389

merged 6 commits into from
Jun 29, 2016

Conversation

BaerMitUmlaut
Copy link
Contributor

@BaerMitUmlaut BaerMitUmlaut commented Jun 20, 2016

This PR adds a state machine system, similar to BIs FSM, but much more performant (and not finite) and script/config based. It was originally ported from an AI system I'm working on, but has been pretty much rewritten since - thus I could need a second pair of eyes to look over it.

Theory

A state machine consists of a set of states which are connected by transitions. Each state machine starts at a defined initial state and will transition to another state, if the transition's condition is fulfilled. If it cannot transition, it simply stays at its current state.

Implementation

State machines can either be predefined through config or can be dynamically created through function calls. Upon creation, a list the state machine iterates over is defined and afterwards states and transitions are added which have code attached to them.
The state machines themselves run in a "clockwork": every tick (frame), all state machines get "executed" but only process one element each.

  • every tick an item of the state machine's list gets chosen
  • the onState function gets executed with said item
  • if any of the transitions conditions returns true, the transition is chosen
    • the onStateLeaving function gets executed
    • the onTransition function gets executed
    • the current state gets set to the transition's target state
    • the onStateEntered function gets executed
  • the next state machine gets executed by the clockwork

The list can either update itself automatically (just pass code as the list argument), be static or get updated manually. The automatic update happens when the list was fully iterated over, the manual update can be done by calling a function.

Use cases

Although this was made with AI systems in mind, this can be used for anything. You can write whole addons only with this.
The advantage of this whole system is that you can iterate over large lists of things and execute code depending on their status or a certain condition - whilst being extremly performant. This isn't perfect for frame critical tasks, however this isn't an issue most of the time. Even with 60 list elements, all elements will processed within one second (at 60fps).

Examples

I've added a very simplistic example in the files (example.sqf and example.hpp), but here's some more ideas how you could use this:

  • create a medical system based on states (alive, hurt, unconscious, ...)
  • create a watchdog for objects and execute code upon deletition (since that doesn't trigger any EHs)
  • create a mission without triggers, purely based on an event like structure

@nicolasbadano
Copy link
Contributor

nicolasbadano commented Jun 21, 2016

This is all good and well; now go and write a Behaviour tree system please ;)

I'm kidding (partially, BTs would be great!). Great job @BaerMitUmlaut; congratulations!

@nicolasbadano
Copy link
Contributor

nicolasbadano commented Jun 21, 2016

@BaerMitUmlaut, I think it would be great if the system auto created a namespace for each state machine (with CBA_fnc_createNamespace). That kind of thing is often very useful to store data that needs to be shared between states. That's not strictly necessary when the items in the list support setVariable (like units), but it is for more "abstract" items like missions or events.

@BaerMitUmlaut
Copy link
Contributor Author

Each state machine is already a CBA namespace 👍
https://github.com/BaerMitUmlaut/CBA_A3/blob/statemachine/addons/statemachine/fnc_create.sqf#L40

@nicolasbadano
Copy link
Contributor

nicolasbadano commented Jun 21, 2016

Hold on, I was editing the comment; I meant for every ITEM.

EDIT: Maybe it's overkill; e.g. I thought group didn't support setVariable. As for mission events, I assume those will usually will contain one item in the list only, so they can use the state machine namespace anyway.

@nicolasbadano
Copy link
Contributor

nicolasbadano commented Jun 21, 2016

Also, so other random thoughts:

  • All state machines are evaluated each frame. Maybe they shouldn't? Could be non-ideal in scenarios when you might have many state machines with few items in each one.
  • Maybe states could have a configurable tick interval, which would be great for stuff that's low prio (eg. units in "sleeping" vs "fighting" state)
  • I think that along "onState", it would be nice to have "onStateEnter" and "onStateLeave", which would be optional pieces of code that are run only once when an item enters or leaves a given state. That'd allow to hook up/down event handlers, keybinds, etc, as well as e..g display messages when a mission enters a new phase.

@@ -0,0 +1,79 @@
/* ----------------------------------------------------------------------------
Function: CBA_statemachine_fnc_addState
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wrong function name

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

EVERY
TIME
;_;

@BaerMitUmlaut
Copy link
Contributor Author

  • Namespaces per item:
    I don't think a namespace should be created for every item, since so many things support getVariable ARRAY already. Additionally it could cause problems when updating the item list (say your list consists of non-unique numbers). I could add support for CBA namespaces as list items though in case somebody needs that (so generally add support for types that can only use getVariable STRING).
  • Not evaluating all state machines each frame:
    Neat idea, but since you already can't say for certain how long it will take to fully iterate through the list I'm not sure if it's that useful.
  • States with different tick interval:
    Also cool idea. Would mean skipping a condition when the item hasn't slept for x seconds if I understand you correctly? However, same issue as above. It could be that the list is so big that this sleep time is irrelevant.
  • onStateEnter and onStateLeave
    I like it, I'll add that. Probably cleaner than adding a piece of code on each incoming/outgoing transition.

@BaerMitUmlaut
Copy link
Contributor Author

BaerMitUmlaut commented Jun 21, 2016

The following was added:

  • onStateEntered and onStateLeaving functions
  • support for types that cannot use getVariable ARRAY

GVAR(stateMachines) pushBack _stateMachine;

if (isNil QGVAR(pfh)) then {
GVAR(pfh) = [FUNC(clockwork), 0, []] call CBA_fnc_addPerFrameHandler;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

optional parameter to set this value for you fsm?

@nicolasbadano
Copy link
Contributor

States with different tick interval:
Also cool idea. Would mean skipping a condition when the item hasn't slept for x seconds if I understand you correctly? However, same issue as above. It could be that the list is so big that this sleep time is irrelevant.

Let me illustrate what I was thinking about with an example: suppose I'm doing a FSM to control the behaviour of a zombie (yes, it will happen for sure!). I might have a "sleeping" state for zeds that are war away from the player. That state is very low prio, so i don't want its code or transitions to be evaluated the same amount of times per second as those awaken zeds, maybe in "chasing" state. Maybe I want those items ticked a maximum of once every 5 secs, while I want "chasing" zeds to be evaluated as often as possible.

That much is clear to me; what I'm not sure about is how you'd implement such a thing, because it'd mean that not all the items on the list are evaluated as often as each other, so it's harder to know when the list should be updated.

@commy2
Copy link
Contributor

commy2 commented Jun 21, 2016

Questions:
I understand onStateEntered and onStateLeaving, but when is onTransition being excuted?

What if two conditions are met at the same time?

Will it only evaluate the transitional conditions of the current state or more (like how the vanilla animations system picks transitional animations via connectTo and connectFrom).

class Initial {
onState = "";
onStateEntered = "";
onStateLeaving = "";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"Entered" vs "Leaving". I think you should probably conyugate those two consistently.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's on purpose, it follows an event naming convention from Microsoft. The point is that it tells you already exactly when this happens:

  • entered means you're already within the new state (thus past-tense)
  • leaving means you're still in the state, but are about in progress of leaving it (thus gerund)

More info: https://msdn.microsoft.com/de-de/library/h0eyck3s(v=vs.71).aspx

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very Microsoft

@nicolasbadano
Copy link
Contributor

I understand onStateEntered and onStateLeaving, but when is onTransition being excuted?

onTransition is executed when the transition condition is evaluated to true. FSM is then transicioned to the target state

What if two conditions are met at the same time?

Only the first is followed AFAIK.

Will it only evaluate the transitional conditions of the current state or more

Only of the current state

@commy2
Copy link
Contributor

commy2 commented Jun 21, 2016

So in case:
State A -> State B
onTransition of A, onStateLeaving of A, onStateEntered of B

?

@BaerMitUmlaut
Copy link
Contributor Author

@commy2: Pretty much like esteldunedain already said:

I understand onStateEntered and onStateLeaving, but when is onTransition being excuted?

Right in between, as mentioned in the OP execution order is:

  • onStateLeaving of old state
  • onTransition of the transition between the old and the new state
  • onStateEntered of new state

What if two conditions are met at the same time?

They're being checked in sequence, the transition of first one returning true will get chosen.

Will it only evaluate the transitional conditions of the current state (...) ?

Yes. Each state only cares about its own transitions.


@esteldunedain:

suppose I'm doing a FSM to control the behaviour of a zombie (yes, it will happen for sure!)

I know somebody who is already working on that 😄

That much is clear to me; what I'm not sure about is how you'd implement such a thing, because it'd mean that not all the items on the list are evaluated as often as each other, so it's harder to know when the list should be updated.

You could do a time check, and if the item hasn't slept in that state for x seconds, skip to the next item (so no conditions nor the onState function would get executed). Would that be what you're thinking of?

@BaerMitUmlaut
Copy link
Contributor Author

I notcied something that was lacking, a isNull skipper. Also, I'm wondering if the functions could use a different, better params order.

@commy2
Copy link
Contributor

commy2 commented Jun 25, 2016

The order of the function arguments seems fine to me.
I don't know what you mean by "isNull - skipper".
I tested this PR and it works good. Very clean code too 👍

@thojkooi
Copy link
Contributor

I think this is great, but I think an event driven statemachine would be much more suitable for the arma context.

@nicolasbadano
Copy link
Contributor

event driven statemachine

I'm not sure what you mean. I think the purpose of this is to replace the existing FSM, which work similarly to this PR.

If you mean that transitions should be able to be triggered through events, I think it's possible to achieve with the current implementation.

@nicolasbadano
Copy link
Contributor

I don't know what you mean by "isNull - skipper".

I think he means that if an item on the list is found to be null when evaluating a state that it should be skipped.

@BaerMitUmlaut
Copy link
Contributor Author

The isNull skipper simply skips elements that are null to prevent errors without needing to hardcode it in every condition, onState, onTransition etc. function. Should be quite handy.

@Glowbal: You can easily raise events from within the functions. Making this event based would make it a bit too cluttered in my opinion.

@commy2
Copy link
Contributor

commy2 commented Jun 28, 2016

👍

@commy2 commy2 added this to the 2.4.2 milestone Jun 29, 2016
@commy2 commy2 self-assigned this Jun 29, 2016
@commy2 commy2 merged commit 64cb370 into CBATeam:master Jun 29, 2016
@marceldev89
Copy link

This is probably not the place for it but I'll give it a shot anyway.

Isn't the onState event supposed to run once in a state machine?

@thojkooi
Copy link
Contributor

thojkooi commented Aug 6, 2016

The state pattern's intend is to

Allow an object to alter its behavior when its internal state changes.
https://sourcemaking.com/design_patterns/state

How many times it runs is not relevant for the state pattern. The only thing that matters is that each state has the same interface (in this case, the onState call back). Whenever some input happens, the current state will handle that. Behaviour will happen according to the current state.

The State methods change the "current" state in the wrapper object as appropriate.

So not after one run, but when something specific occurs that makes it appropriate to switch to a new state.

One example (shameless copied from wikipedia, if you are going to look it up) is a turnstile. It has two states; locked and unlocked. Push (input) while it's locked and it will not move. Throw a coin in it when it's locked (the input) and it will go to unlocked. When unlocked, push towards it and it will turn. After the turn, it will transition into the locked state.

So some states could depending on input, transition right into a next state and "only run it once", while others may run an undefined amount of time. It all depends on your design and intend.

tldr; this implementation is a valid state machine, or at least as close as you are going to get within sqf.

@marceldev89
Copy link

marceldev89 commented Aug 6, 2016

Thanks, makes sense. I was thinking more along the lines of how the FSMs work in Arma. onStateEntered it is. 😄

@desert5
Copy link

desert5 commented Jul 14, 2018

Sorry to bring this up after several years, but there seems to be no information on this topic elsewhere. I wonder - what makes your implementation of FSM so much more performant, as you are saying?

@BaerMitUmlaut
Copy link
Contributor Author

The gist of it is that in the CBA state machine exactly one entity is handled per frame. So if you want to handle AI behaviour for example, it doesn't matter how many AI you spawned, the state machine will always use up the same amount of performance and you won't get any frame degradation because of it.

I've also written a detailed post about it on the BI forum which might help you, too (seems like the forum is kind of broken right now, you should still be able to read it though).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

6 participants