14.04.2015, 19:57
State machines (automata)
Disclaimer
This tutorial is apart of ******' Code Optimisation topic which is no longer available - I do not take credit for it.
Contents
- Contents
- Introduction
- Example 1
- Advantages
- Example 2
- Examination
- Explanation
- state
- Functions
- No states
- One state
- Multiple states
- Other states
- entry()
- Note
- Example 3
- Further information
This is something that I have NEVER seen used in ANY released script, which is a shame really (though I too am guilty of not using them) as they are very useful. PAWN natively supports a system called state machines (also known as automata), which basically allow you to define multiple copies of the same function to use in different circumstances.
Example 1
To demonstrate this system I have developed a very simple example mode:
Using standard techniques, so people will recognise what it is that this mode should do:
Код:
#include <a_samp> forward change(); enum { end = 0, gmx } new gExitType; main() { gExitType = end; SetTimer("change", 10000, 0); } public OnGameModeExit() { switch (gExitType) { case end: printf("This mode was ended by the server"); case gmx: printf("This mode was ended by the timer"); } } public change() { gExitType = gmx; GameModeExit(); }
Код:
#include <a_samp> forward change(); main() { state exitType:end; SetTimer("change", 10000, 0); } public OnGameModeExit() <exitType:end> { printf("This mode was ended by the server"); } public OnGameModeExit() <exitType:gmx> { printf("This mode was ended by the timer"); } public change() { state exitType:gmx; GameModeExit(); }
So what are the advantages to using this technique? Well, as you can clearly see the code is smaller; granted I added an enum to the first version to make the examples more similar but the OnGameModeExit functions are definately smaller. It's also alot more explicit - you don't need large switch statements in functions to determine what's going on, if you wanted you could put the different OnGameModeInits in different files to handle different sets of code independently. It also uses less global variables.
Example 2
Here's another handy example, this time using a debug command to quickly turn printing on and off:
Код:
#include <a_samp> dprint(message[]) <printState:on> { // Display the message print(message); } dprint(message[]) <printState:off> { // Do nothing #pragma unused message } forward output(); main() { state printState:off; // Turn on the example timer SetTimer("output", 1000, 1); } public OnPlayerCommandText(playerid, cmdtext[]) <printState:on> { state (!strcmp(cmdtext, "/debug")) printState:off; } public OnPlayerCommandText(playerid, cmdtext[]) <printState:off> { state (!strcmp(cmdtext, "/debug")) printState:on; } public output() { static sCount = 0; if (++sCount == 5) { OnPlayerCommandText(0, "/debug"); sCount = 0; } // Just constantly print a message here as an example. dprint("Hello World"); }
A few important things to note here:
Firstly, dprint is not a public function, it's just a regular function, but states still work on it.
Secondly the "state" directive can have conditionals in it. This line:
Код:
state (!strcmp(cmdtext, "/debug")) printState:on;
Код:
if (!strcmp(cmdtext, "/debug")) { state printState:on; }
Explanation
Now we've looked at some examples you're probably wondering what's going on (unless, of course, you've read pawn-lang.pdf), so let's look at the code closer:
- state
Код:
state a:b
As mentioned above you can have conditionals in state directives:
Код:
state (c == 1) a:b;
- Functions
- No states
Код:
dprint(message[]) { print(message); }
- One state
Код:
dprint(message[]) <debugState:on> { print(message); }
- Multiple states
Код:
dprint(message[]) <debugLevel:_1, debugLevel:_2> { print(message); }
- Other states
Код:
dprint(message[]) <> { print(message); }
- entry()
Код:
main () { printf("hello"); state myState:moo; printf("world"); } entry() <myState:moo> { printf("change"); }
Код:
hello change world
- Note
There is one firly major problem with using this in SA:MP - alot of what happens is player dependant, so one player could be doing one thing while another is doing another. You can only have functions tied to a single automata, so generally these would be used for global systems rather than anything which varies per player, but for those things they're fine.
Example 3
Lets look at one final example just to get this all straight. This time we'll look at a race written using automata, this is a very basic outline and some functions are missing, but shows how you would use this system to simplify the amount of code run. Although you still need checks to deal with the multiple player situation (otherwise you could wait till someone went through the second to last checkpoint then go through ANY checkpoint and win) this simplifies the amount of code run at any given time. If you know no-one is going for the final checkpoint why bother to see if they've got it? It's just a waste of time!
Код:
new // Number of people in the race gEntrants = 0; main() { state raceState:none; } StartRace() { // Start the race. state raceState:start; // No explicit handle, done in <>. } CancelRace() { state raceState:none; // Reset the race. gEntrants = 0; } public OnPlayerEnterRaceCheckpoint(playerid) <raceState:none> { // Called when there's no race on. SendClientMessage(playerid, 0xFF0000AA, "Sorry, there's no race on at this time"); } public OnPlayerEnterRaceCheckpoint(playerid) <raceState:finish> { // Called when someone is going for the last checkpoint. if (AtLastCheckpoint(playerid)) { // This person got to the last checkpoint first. SendClientMessage(playerid, 0xFF0000AA, "Congratulations, you won the race"); DisablePlayerRaceCheckpoint(playerid); state raceState:won; --gEntrants; } else { // Not there yet. SetNextCheckpoint(playerid); } } public OnPlayerEnterRaceCheckpoint(playerid) <raceState:won> { // Called when someone has already won but others are still going. if (AtLastCheckpoint(playerid)) { // Reached the end but didn't win. SendClientMessage(playerid, 0xFF0000AA, "Sorry, somebody already won"); DisablePlayerRaceCheckpoint(playerid); state (--gEntrants == 0) raceState:none; } else { // Not there yet. SetNextCheckpoint(playerid); } } public OnPlayerEnterRaceCheckpoint(playerid) <> { // Called during the race, just set their next checkpoint. SetNextCheckpoint(playerid); if (AtLastCheckpoint(playerid)) { // Someone is going for the finish line. state raceState:finish; } }
For more information and a more detailed explanation see page 37 on in pawn-lang.pdf.