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
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions addons/statemachine/$PBOPREFIX$
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
x\cba\addons\statemachine
11 changes: 11 additions & 0 deletions addons/statemachine/CfgEventHandlers.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
class Extended_PreStart_EventHandlers {
class ADDON {
init = QUOTE(if (!IS_LINUX) then {call COMPILE_FILE(XEH_preStart)});
};
};

class Extended_PreInit_EventHandlers {
class ADDON {
init = QUOTE(if (!IS_LINUX) then {call COMPILE_FILE(XEH_preInit)});
};
};
8 changes: 8 additions & 0 deletions addons/statemachine/XEH_PREP.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
PREP(addState);
PREP(addTransition);
PREP(clockwork);
PREP(create);
PREP(createFromConfig);
PREP(delete);
PREP(toString);
PREP(updateList);
7 changes: 7 additions & 0 deletions addons/statemachine/XEH_preInit.sqf
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
#include "script_component.hpp"

ADDON = false;

#include "XEH_PREP.hpp"

ADDON = true;
3 changes: 3 additions & 0 deletions addons/statemachine/XEH_preStart.sqf
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#include "script_component.hpp"

#include "XEH_PREP.hpp"
16 changes: 16 additions & 0 deletions addons/statemachine/config.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
#include "script_component.hpp"

class CfgPatches {
class ADDON {
author = "$STR_CBA_Author";
name = CSTRING(component);
url = "$STR_CBA_URL";
units[] = {};
requiredVersion = REQUIRED_VERSION;
requiredAddons[] = {"CBA_common"};
version = VERSION;
authors[] = {"BaerMitUmlaut"};
};
};

#include "CfgEventHandlers.hpp"
33 changes: 33 additions & 0 deletions addons/statemachine/example.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
This is an example for a CBA state machine config that can be read by the
CBA_statemachine_fnc_createFromConfig function.
This example would result in the same state machine as the one from
example.sqf.
*/

class MyAddon_Statemachine {
// Class properties have the same name as the corresponding function parameters
// and code goes into strings.
list = "allGroups select {!isPlayer leader _x}";
skipNull = 1;

// States are just subclasses of the state machine
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


// Transitions are also just subclasses of states
class InCombat {
targetState = "Alert";
condition = "combatMode _this == 'YELLOW'";
onTransition = "{ \
_x setSkill ['spotDistance', ((_x skill 'spotDistance') * 1.5) min 1]; \
_x setSkill ['spotTime', ((_x skill 'spotTime') * 1.5) min 1]; \
} forEach (units _this);";
};
};

// Empty classes will also work if the state contains no transitions or onState code.
class Alert {};
};
33 changes: 33 additions & 0 deletions addons/statemachine/example.sqf
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
This is an example for a script that uses the CBA state machine system.
In this case, we're "sharpening the senses" of AI units that got attacked before.
Once they had contact, they stay alert.

This would simulate units that did not expect an attack, but are now aware that
they might come under fire again.

You can test this example by executing the following code in the debug console:
[] call compile preprocessFileLineNumbers "\x\cba\addons\statemachine\example.sqf"
*/

private _stateMachine = [{allGroups select {!isPlayer leader _x}}, true] call CBA_statemachine_fnc_create;

[_stateMachine, {}, {}, {}, "Initial"] call CBA_statemachine_fnc_addState;
[_stateMachine, {}, {}, {}, "Alert"] call CBA_statemachine_fnc_addState;

[_stateMachine, "Initial", "Alert", {combatMode _this == "YELLOW"}, {
// Set skill once on transition
// This could also be done in the onStateEntered function
{
_x setSkill ["spotDistance", ((_x skill "spotDistance") * 1.5) min 1];
_x setSkill ["spotTime", ((_x skill "spotTime") * 1.5) min 1];
} forEach (units _this);
}, "InCombat"] call CBA_statemachine_fnc_addTransition;

// This makes sure you can execute this through the debug console
_stateMachine spawn {
sleep 0.1;
private _output = [_this, true, true] call CBA_statemachine_fnc_toString;
copyToClipboard _output;
hintC _output;
};
66 changes: 66 additions & 0 deletions addons/statemachine/fnc_addState.sqf
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/* ----------------------------------------------------------------------------
Function: CBA_statemachine_fnc_addState

Description:
Adds a state to a state machine.

Parameters:
_stateMachine - a state machine <LOCATION>
_onState - code that is executed when state is active (frequency
depends on amount of objects active in state machine)
<CODE>
(Default: {})
_onStateEntered - code that is executed once when state was entered, after
onTransition (also once for the intial state) <CODE>
(Default: {})
_onStateLeaving - code that is executed once when exiting state, before
onTransition <CODE>
(Default: {})
_name - unique state name <STRING>
(Default: "stateX" with X being a unique number)

Returns:
_name - unique state name or empty string on error <STRING>

Examples:
(begin example)
_name = [_stateMachine, {}] call CBA_statemachine_fnc_addState;
(end)

Author:
BaerMitUmlaut
---------------------------------------------------------------------------- */
#include "script_component.hpp"
SCRIPT(addState);
params [
["_stateMachine", locationNull, [locationNull]],
["_onState", {}, [{}]],
["_onStateEntered", {}, [{}]],
["_onStateLeaving", {}, [{}]],
["_name", "", [""]]
];

private _states = _stateMachine getVariable QGVAR(states);

if (isNull _stateMachine || {_name in _states}) exitWith {""};

// Autogenerate unique name
if (_name == "") then {
private _nextUniqueID = _stateMachine getVariable QGVAR(nextUniqueStateID);
_name = "state" + str _nextUniqueID;
_stateMachine setVariable [QGVAR(nextUniqueStateID), _nextUniqueID + 1];
};

_states pushBack _name;
_stateMachine setVariable [QGVAR(states), _states];
_stateMachine setVariable [ONSTATE(_name), _onState];
_stateMachine setVariable [ONSTATEENTERED(_name), _onStateEntered];
_stateMachine setVariable [ONSTATELEAVING(_name), _onStateLeaving];
_stateMachine setVariable [TRANSITIONS(_name), []];

// First state added is always the intial state
if (isNil {_stateMachine getVariable QGVAR(initialState)}) then {
_stateMachine setVariable [QGVAR(initialState), _name];
};

_name
56 changes: 56 additions & 0 deletions addons/statemachine/fnc_addTransition.sqf
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/* ----------------------------------------------------------------------------
Function: CBA_statemachine_fnc_addTransition

Description:
Creates a transition between two states.

Parameters:
_stateMachine - a state machine <LOCATION>
_originalState - state the transition origins from <STRING>
_targetState - state the transition goes to <STRING>
_condition - condition under which the transition will happen <CODE>
_onTransition - code that gets executed once transition happens <CODE>
(Default: {})
_name - name for this specific transition <STRING>
(Default: "NONAME")

Returns:
_wasCreated - check if the transition was created <BOOL>

Examples:
(begin example)
[_stateMachine, "initial", "end", {true}, {
systemChat format [
"%1 transitioned from %2 to %3 via %4.",
_this, _thisOrigin, _thisTarget, _thisTransition
];
}, "dummyTransition"] call CBA_statemachine_fnc_addTransition;
(end)

Author:
BaerMitUmlaut
---------------------------------------------------------------------------- */
#include "script_component.hpp"
SCRIPT(addTransition);
params [
["_stateMachine", locationNull, [locationNull]],
["_originalState", "", [""]],
["_targetState", "", [""]],
["_condition", {}, [{}]],
["_onTransition", {}, [{}]],
["_name", "NONAME", [""]]
];

private _states = _stateMachine getVariable QGVAR(states);

if (isNull _stateMachine
|| {!(_originalState in _states)}
|| {!(_targetState in _states)}
|| {_condition isEqualTo {}}
) exitWith {false};

private _transitions = _stateMachine getVariable TRANSITIONS(_originalState);
_transitions pushBack [_name, _condition, _targetState, _onTransition];
_stateMachine setVariable [TRANSITIONS(_originalState), _transitions];

true
102 changes: 102 additions & 0 deletions addons/statemachine/fnc_clockwork.sqf
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
/* ----------------------------------------------------------------------------
Function: CBA_statemachine_fnc_clockwork

Description:
Clockwork which runs all state machines.

Parameters:
None

Returns:
Nothing

Author:
BaerMitUmlaut
---------------------------------------------------------------------------- */
#include "script_component.hpp"
SCRIPT(clockwork);

{
private _stateMachine = _x;
private _list = _stateMachine getVariable QGVAR(list);
private _skipNull = _stateMachine getVariable QGVAR(skipNull);
private _updateCode = _stateMachine getVariable QGVAR(updateCode);
private _id = _stateMachine getVariable QGVAR(ID);

// Skip state machine when it has no states yet
if !(isNil {_stateMachine getVariable QGVAR(initialState)}) then {
private _tick = _stateMachine getVariable QGVAR(tick);

// Skip to next non-null element or end of list
if (_skipNull) then {
while {(_tick < count _list) && {isNull (_list select _tick)}} do {
_tick = _tick + 1;
};
};

// When the list was iterated through, jump back to start and update it
if (_tick >= count _list) then {
_tick = 0;
if !(_updateCode isEqualTo {}) then {
_list = [] call _updateCode;

// Make sure list contains no null elements in case the code doesn't filter them
// Else they wouldn't be skipped at this point which could cause errors
if (_skipNull) then {
_list = _list select {!isNull _x};
};

_stateMachine setVariable [QGVAR(list), _list];
};
};

// If the list has no items, we can stop checking this state machine
// No need to set the tick when it will get reset next frame anyways
if (count _list > 0) then {
_stateMachine setVariable [QGVAR(tick), _tick + 1];

private _current = _list select _tick;
private _thisState = _current getVariable (QGVAR(state) + str _id);

if (isNil "_thisState") then {
// Item is new and gets set to the intial state, onStateEntered
// function of initial state gets executed as well.
_thisState = _stateMachine getVariable QGVAR(initialState);
_current setVariable [QGVAR(state) + str _id, _thisState];
_current call (_stateMachine getVariable ONSTATEENTERED(_thisState));
};

// onState functions can use:
// _stateMachine - the state machine
// _this - the current list item
// _thisState - the current state
_current call (_stateMachine getVariable ONSTATE(_thisState));

private _thisOrigin = _thisState;
{
_x params ["_thisTransition", "_condition", "_thisTarget", "_onTransition"];
// Transition conditions, onTransition, onStateLeaving and
// onStateEntered functions can use:
// _stateMachine - the state machine
// _this - the current list item
// _thisTransition - the current transition we're in
// _thisOrigin - the state we're coming from
// _thisState - same as _thisOrigin
// _thisTarget - the state we're transitioning to
// Note: onTransition and onStateLeaving functions can change
// the transition target by overwriting the passed
// _thisTarget variable.
// Note: onStateEntered functions of initial states won't have
// some of these variables defined.
if (_current call _condition) exitWith {
_current call (_stateMachine getVariable ONSTATELEAVING(_thisOrigin));
_current call _onTransition;
_current setVariable [QGVAR(state) + str _id, _thisTarget];
_current call (_stateMachine getVariable ONSTATEENTERED(_thisTarget));
};
} forEach (_stateMachine getVariable TRANSITIONS(_thisState));
};
};

false
} count GVAR(stateMachines);
Loading