Difference between revisions of "State machine"
Eric Walch (talk | contribs) (Added examples) |
Cholmondely (talk | contribs) m (Added links) |
||
(3 intermediate revisions by one other user not shown) | |||
Line 2: | Line 2: | ||
The AI system consists of a '[[Stacked AI|stack]]' of state machines (or AI's). | The AI system consists of a '[[Stacked AI|stack]]' of state machines (or AI's). | ||
− | The top AI of this stack is the active AI. This state machine has the structure of a plist file. The main level is an array of states. Each of this states is an array of messages. | + | The top AI of this stack is the active AI. This state machine has the structure of a [[plist]] file. The main level is an array of states. Each of this states is an array of [[messages]]. |
==States== | ==States== | ||
On creation of an entity the system assigns a state machine (= AI) to every entity. It looks in the shipdata.plist file for an entry with name "ai_type". If none is defined it defaults to nullAI.plist. Then it starts the state with name "GLOBAL". From hereon the machine is on its own and in most cases there is an instruction in the GLOBAL state to jump to an state with a more descriptive name. | On creation of an entity the system assigns a state machine (= AI) to every entity. It looks in the shipdata.plist file for an entry with name "ai_type". If none is defined it defaults to nullAI.plist. Then it starts the state with name "GLOBAL". From hereon the machine is on its own and in most cases there is an instruction in the GLOBAL state to jump to an state with a more descriptive name. | ||
− | |||
− | |||
− | + | ==Program flow== | |
+ | There are three messages that have a special meaning to the state machine: ENTER, UPDATE & EXIT. When the state machine enters a new state, it first looks for a message with name ENTER. When present, it will execute all the commands in this state. The update timer was set to its default value of 0.125 seconds on startup but can be changed by a command in the ENTER message. | ||
− | When there is an instruction to go to another state | + | After the update time is finished, the state machine sets a new update time at 0.125 seconds from now and than looks for an entry with the name UPDATE. When present, all commands in it will be executed. This means that without defined pauses, the update will be executed 8 times every second. Whenever a "pauseAI:" is executed, this existing update time is replaced by the new update time. So pause will not postpone the current command line but has only effect on the next update time. pauseAI is also not cumulative, but replaces any previously set time. |
+ | |||
+ | When all the UPDATE commands are executed, all collected normal messages are set aside and priority messages are executed immediately. Than all collected normal messages are executed in a random order. Any new normal message that the commands in these messages generate are set aside to be evaluated on the next UPDATE. Any priority message that is generated will be executed immediately. | ||
+ | |||
+ | When there is an instruction to go to another state, the state machine first executes the message in the current state with the name EXIT. Be aware that UPDATE evaluation of the old state not stops when switching states. Evaluation only stops when the last message that was set aside is executed. Although it will look for the message name in the new state when a switch occurred. | ||
+ | |||
+ | For complex state machines, it might be wise to switch the [[Oolite_JavaScript_Reference:_Ship#reportAIMessages|reportAIMessages]] property of the specific ship on during testing. Do this in the console or give the ship a ship script that switches it on. | ||
==Normal Messages== | ==Normal Messages== | ||
− | Some commands return messages to the state machine like "TARGET_FOUND" or TARGET_LOST. These messages are put on a | + | Some commands return messages to the state machine like "TARGET_FOUND" or TARGET_LOST. These messages are put on a list with a maximum size of 32 entries and duplicate messages are ignored. Whenever an update is executed, the messages in this list are compared against message entries in the currently active state. If it finds one, the commands in this message line are executed. Take note that normal messages are only examined after the UPDATE commands are executed and not after execution of the ENTER commands. |
To be more precise: After execution of the UPDATE commands, all received message are copied to a new execution list and the current message stack is cleared. Then all the commands that correspond to received messages are executed, and the new messages these commands may generate are not examined until the next update time. | To be more precise: After execution of the UPDATE commands, all received message are copied to a new execution list and the current message stack is cleared. Then all the commands that correspond to received messages are executed, and the new messages these commands may generate are not examined until the next update time. | ||
==Priority Messages== | ==Priority Messages== | ||
− | Some messages can't wait until the next update. Good examples are commands were the entity is removed from the universe like "performWitchspaceExit". When it jumps it returns "WITCHSPACE_OK". When it had to wait till the next update the entity could already be removed from the universe together with its state machine. But when the entity has escorts these must also get a command to follow. For these reason some messages are prioritized and will be executed immediately when there is a corresponding entry. | + | Some messages can't wait until the next update. Good examples are commands were the entity is removed from the universe like "performWitchspaceExit". When it jumps it returns "WITCHSPACE_OK". When it had to wait till the next update the entity could already be removed from the universe together with its state machine. But when the entity has escorts these must also get a command to follow. For these reason some messages are prioritized and will be executed immediately when there is a corresponding entry. When using the JS system, these priority messages are generated with the command "''reactToAIMessage()''". |
+ | |||
+ | A mix of normal and priority messages can be very confusing at first and lead to quite a different code flow than first thought. Specially when priority messages lead to switching states it may result in that the next message is also executed in an other state. And when that state also contains something like a TARGET_FOUND message, the state machine might do other things with the target than planned. Therefor it is quite important to distinguish between both message types.: Priority Messages execute on message generation and normal messages execute on update time. | ||
+ | |||
==Examples== | ==Examples== | ||
Best thing to understand how it works is to look at an unusual example with three states that only shows some messages: | Best thing to understand how it works is to look at an unusual example with three states that only shows some messages: | ||
WAIT_FOR_SHOT = { | WAIT_FOR_SHOT = { | ||
− | |||
− | |||
ATTACKED = ("setStateTo: WARN_PLAYER", "setStateTo: IDLE"); | ATTACKED = ("setStateTo: WARN_PLAYER", "setStateTo: IDLE"); | ||
− | + | }; | |
− | |||
WARN_PLAYER = { | WARN_PLAYER = { | ||
ENTER = ("commsMessage: Help, don't shoot"); | ENTER = ("commsMessage: Help, don't shoot"); | ||
− | + | }; | |
− | |||
− | |||
IDLE = { | IDLE = { | ||
ENTER = ("pauseAI: 10"); | ENTER = ("pauseAI: 10"); | ||
Line 39: | Line 42: | ||
}; | }; | ||
− | + | The first state waits only for the message "ATTACKED". If it receives it, it executes the first command: "setStateTo: WARN_PLAYER". For this it sets the pointers to the second state and executes its ENTER message, the commsMessage: "Help, don't shoot". | |
But the original commandline is not finished and so the second command is executed: "setStateTo: IDLE". Now the pointers are set to the third state and his ENTER message is executed. But this one only sets the next update time with a pause of 10 seconds. Now all commands are finished and control is given back to the game. On the next update time the statemachine is set to the third state and that UPDATE message is executed every 10 seconds. When is receives an attack in this state it executes his EXIT commands and returns to the first state. | But the original commandline is not finished and so the second command is executed: "setStateTo: IDLE". Now the pointers are set to the third state and his ENTER message is executed. But this one only sets the next update time with a pause of 10 seconds. Now all commands are finished and control is given back to the game. On the next update time the statemachine is set to the third state and that UPDATE message is executed every 10 seconds. When is receives an attack in this state it executes his EXIT commands and returns to the first state. | ||
− | Of course this is a nonsense script but clearly shows how the machine jumps from state to state. It always finishes all pending messages in the original state, even when there are "setStateTo:" commands inside. | + | Of course this is a nonsense script but clearly shows how the machine jumps from state to state. It always finishes all pending messages in the original state, even when there are "setStateTo:" commands inside. When there are more jumps in one line, or more messages contain a "setStateTo:" command, all the enter messages are still executed, but it ends in the last defined state. |
The only exeptions are the commands setAITo: or switchAITo:. In this case the whole statemachine is put on hold and a new one is started. | The only exeptions are the commands setAITo: or switchAITo:. In this case the whole statemachine is put on hold and a new one is started. |
Latest revision as of 06:37, 24 July 2024
Contents
The State Machine
The AI system consists of a 'stack' of state machines (or AI's).
The top AI of this stack is the active AI. This state machine has the structure of a plist file. The main level is an array of states. Each of this states is an array of messages.
States
On creation of an entity the system assigns a state machine (= AI) to every entity. It looks in the shipdata.plist file for an entry with name "ai_type". If none is defined it defaults to nullAI.plist. Then it starts the state with name "GLOBAL". From hereon the machine is on its own and in most cases there is an instruction in the GLOBAL state to jump to an state with a more descriptive name.
Program flow
There are three messages that have a special meaning to the state machine: ENTER, UPDATE & EXIT. When the state machine enters a new state, it first looks for a message with name ENTER. When present, it will execute all the commands in this state. The update timer was set to its default value of 0.125 seconds on startup but can be changed by a command in the ENTER message.
After the update time is finished, the state machine sets a new update time at 0.125 seconds from now and than looks for an entry with the name UPDATE. When present, all commands in it will be executed. This means that without defined pauses, the update will be executed 8 times every second. Whenever a "pauseAI:" is executed, this existing update time is replaced by the new update time. So pause will not postpone the current command line but has only effect on the next update time. pauseAI is also not cumulative, but replaces any previously set time.
When all the UPDATE commands are executed, all collected normal messages are set aside and priority messages are executed immediately. Than all collected normal messages are executed in a random order. Any new normal message that the commands in these messages generate are set aside to be evaluated on the next UPDATE. Any priority message that is generated will be executed immediately.
When there is an instruction to go to another state, the state machine first executes the message in the current state with the name EXIT. Be aware that UPDATE evaluation of the old state not stops when switching states. Evaluation only stops when the last message that was set aside is executed. Although it will look for the message name in the new state when a switch occurred.
For complex state machines, it might be wise to switch the reportAIMessages property of the specific ship on during testing. Do this in the console or give the ship a ship script that switches it on.
Normal Messages
Some commands return messages to the state machine like "TARGET_FOUND" or TARGET_LOST. These messages are put on a list with a maximum size of 32 entries and duplicate messages are ignored. Whenever an update is executed, the messages in this list are compared against message entries in the currently active state. If it finds one, the commands in this message line are executed. Take note that normal messages are only examined after the UPDATE commands are executed and not after execution of the ENTER commands.
To be more precise: After execution of the UPDATE commands, all received message are copied to a new execution list and the current message stack is cleared. Then all the commands that correspond to received messages are executed, and the new messages these commands may generate are not examined until the next update time.
Priority Messages
Some messages can't wait until the next update. Good examples are commands were the entity is removed from the universe like "performWitchspaceExit". When it jumps it returns "WITCHSPACE_OK". When it had to wait till the next update the entity could already be removed from the universe together with its state machine. But when the entity has escorts these must also get a command to follow. For these reason some messages are prioritized and will be executed immediately when there is a corresponding entry. When using the JS system, these priority messages are generated with the command "reactToAIMessage()".
A mix of normal and priority messages can be very confusing at first and lead to quite a different code flow than first thought. Specially when priority messages lead to switching states it may result in that the next message is also executed in an other state. And when that state also contains something like a TARGET_FOUND message, the state machine might do other things with the target than planned. Therefor it is quite important to distinguish between both message types.: Priority Messages execute on message generation and normal messages execute on update time.
Examples
Best thing to understand how it works is to look at an unusual example with three states that only shows some messages:
WAIT_FOR_SHOT = { ATTACKED = ("setStateTo: WARN_PLAYER", "setStateTo: IDLE"); }; WARN_PLAYER = { ENTER = ("commsMessage: Help, don't shoot"); }; IDLE = { ENTER = ("pauseAI: 10"); EXIT = ("commsMessage: Resetting things"); ATTACKED = ("setStateTo: WAIT_FOR_SHOT"); UPDATE = ("commsMessage: Nice not shooting at me", "pauseAI: 10"); };
The first state waits only for the message "ATTACKED". If it receives it, it executes the first command: "setStateTo: WARN_PLAYER". For this it sets the pointers to the second state and executes its ENTER message, the commsMessage: "Help, don't shoot".
But the original commandline is not finished and so the second command is executed: "setStateTo: IDLE". Now the pointers are set to the third state and his ENTER message is executed. But this one only sets the next update time with a pause of 10 seconds. Now all commands are finished and control is given back to the game. On the next update time the statemachine is set to the third state and that UPDATE message is executed every 10 seconds. When is receives an attack in this state it executes his EXIT commands and returns to the first state.
Of course this is a nonsense script but clearly shows how the machine jumps from state to state. It always finishes all pending messages in the original state, even when there are "setStateTo:" commands inside. When there are more jumps in one line, or more messages contain a "setStateTo:" command, all the enter messages are still executed, but it ends in the last defined state.
The only exeptions are the commands setAITo: or switchAITo:. In this case the whole statemachine is put on hold and a new one is started.