Difference between revisions of "Actions (Programming)"
imported>Spunky |
imported>Spunky |
||
| Line 1: | Line 1: | ||
== What are actions? == | |||
As you've noticed, things don't all happen at once in Hell -- the game keeps track of how long | As you've noticed, things don't all happen at once in Hell -- the game keeps track of how long | ||
an action takes, and makes sure things happen in order. How does this bit of magic happen? The action queue. | an action takes, and makes sure things happen in order. How does this bit of magic happen? The action queue. | ||
| Line 11: | Line 13: | ||
* arguments - a list of arguments describing the action; exact format and meaning is specific to each action object. | * arguments - a list of arguments describing the action; exact format and meaning is specific to each action object. | ||
* 1 - this argument is currently unused. Always pass 1. | * 1 - this argument is currently unused. Always pass 1. | ||
* command-string - a string describing the action being performed. If a player-initiated action, the command typed to start the action. This is used in the {cmd|queue} command output. | * command-string - a string describing the action being performed. If a player-initiated action, the command typed to start the action. This is used in the {{cmd|queue}} command output. | ||
The action queue is simply a list of lists, each inner list being an | The action queue is simply a list of lists, each inner list being an ''action spec'' of this form. When you type a command that initiates an action, an action spec of this form is constructed and placed into your queue to be processed. | ||
type a command that initiates an action, an action spec of this form is constructed and placed into your queue to be processed. | |||
== Queueing actions == | == Queueing actions == | ||
Putting the action on your queue is done by calling | Putting the action on your queue is done by calling: | ||
actor:queue_action( | actor:queue_action(action-spec) | ||
The arguments to :queue_action are simply the four things from the action spec as described above. If the actor isn't doing anything else already, the action happens immediately. If she is, the action is placed in the actor's .queue property where it waits for its turn. | |||
In your code you can cause yourself or other creatures to perform actions simply by calling :queue_action() with a properly constructed action spec. | In your code you can cause yourself or other creatures to perform actions simply by calling :queue_action() with a properly constructed action spec. | ||
| Line 46: | Line 47: | ||
The third and fourth arguments are 1 and some semblance of the player's typed command (for queue display purposes). | The third and fourth arguments are 1 and some semblance of the player's typed command (for queue display purposes). | ||
And that's it! We've queued an action in response to the player typing {cmd|wield <whatever>}, and now the player will wield the item at his next convenience -- probably immediately, but quite possibly later on if he's already got actions going (say, in combat). It might never happen at all (say, if the player eats a grenade between the time he typed {cmd|wield <whatever>} and the time it would have executed). It may sound obvious, but this is very important to keep in mind: you can't assume when, if ever, your queued action will execute. | And that's it! We've queued an action in response to the player typing {{cmd|wield <whatever>}}, and now the player will wield the item at his next convenience -- probably immediately, but quite possibly later on if he's already got actions going (say, in combat). It might never happen at all (say, if the player eats a grenade between the time he typed {{cmd|wield <whatever>}} and the time it would have executed). It may sound obvious, but this is very important to keep in mind: you can't assume when, if ever, your queued action will execute. | ||
== Action Queue Processing: Under the hood == | == Action Queue Processing: Under the hood == | ||
| Line 56: | Line 57: | ||
# It pulls the next action off the .queue. This is just an action spec, as we described above. | # It pulls the next action off the .queue. This is just an action spec, as we described above. | ||
# It calls ''':forbid_action(action-spec)''' on everything in the room, including the room itself. If any of those verbs exist and return 1, the action is skipped entirely; it never had a chance to get started. This forbid_action call is your opportunity as a coder to stop actions from happening under certain circumstances. For instance, it could happen if you're coding a force field that, when active, prevents players from standing up. | |||
# It calls ''':forbid_action(action-spec)''' on everything in the room, including the room itself. If any of those verbs exist and return 1, | |||
the action is skipped entirely; it never had a chance to get started. This forbid_action call is your opportunity as a coder to stop actions from | |||
happening under certain circumstances. For instance, it could happen if you're coding a force field that, when active, prevents players from standing up. | |||
# The action's spec is copied into '''actor.executing''', so we know what action we're in the middle of. You can test this property on creatures to see if they're doing something. | # The action's spec is copied into '''actor.executing''', so we know what action we're in the middle of. You can test this property on creatures to see if they're doing something. | ||
# '''action:_start(OBJ actor, LIST arguments)''' is called on the action object, where 'actor' is the actor performing the action, and 'arguments' is the list of arguments the action object expects -- the arguments originally given to us when :queue_action() was called. action:_start() does what it needs to do -- test for sanity, print a start message, etc -- and returns back a two item list of the form ''{FLOAT duration, ANY pass-to-finish}'' The 'duration' parameter is simply a duration (in seconds) to wait before the action finishes. The 'pass-to-finish' parameter can be anything -- it's used for any data that needs to be given to the :_finish() verb about the results of starting this actions. For many actions this is nothing. You may be tempted to decide whether the action succeeded in the :_start verb, but if that decision depends on a skill check you should not do this. Save the skill check for the :_finish verb, otherwise you could be creating an exploit that allows people to improve quickly by repeatedly starting and stopping the action (and never having to wait for it to finish). | |||
# '''action:_start(OBJ actor, LIST arguments)''' is called on the action object, where 'actor' is the actor performing the action, and 'arguments' | # ''':process_queue()''' suspends for the number of seconds given back by the :_start() verb in 'duration'. While this task is suspended, any additional :queue_action() calls on the actor will simply push actions onto the queue, because the actor knows it's already doing something. | ||
is the list of arguments the action object expects -- the arguments originally given to us when :queue_action() was called. action:_start() does what it needs to do -- test for sanity, print a start message, etc -- and returns back a two item list of the form | # '''action:_finish(OBJ actor, LIST arguments, ANY pass-to-finish''' is called. For most actions, the meat of the work happens here. The appropriate time has elapsed and the action can actually '''do''' whatever it is it wanted to do, possibly based on the results forwarded to us by 'pass-to-finish'. | ||
'duration' is simply a duration | |||
# ''':process_queue()''' suspends for the number of seconds given back by the :_start() verb in 'duration'. While this task is suspended, any additional :queue_action() calls | |||
on the actor will simply push actions onto the queue, because the actor knows it's already doing something. | |||
# '''action:_finish(OBJ actor, LIST arguments, ANY pass-to-finish''' is called. For most actions, the meat of the work happens here. The appropriate time has elapsed and | |||
the action can actually '''do''' whatever it is it wanted to do, possibly based on the results forwarded to us by 'pass-to-finish'. | |||
# Now that the action is completely done, .executing is cleared, and we move on to the next action, or die quietly if there are no more actions to process. | # Now that the action is completely done, .executing is cleared, and we move on to the next action, or die quietly if there are no more actions to process. | ||
== Creating your own actions == | == Creating your own actions == | ||
Generally, anything that would take more than a second or two to do in the real world should be an action. Anything that you shouldn | Generally, anything that would take more than a second or two to do in the real world should be an action. Anything that you shouldn't be able to do instantly in combat should be an action. In fact, almost anything that interacts with or alters the virtual reality in any way should probably be an action. As an example, let's create a rape action. | ||
First, @create a child of $action. Give it a nice short one-word name -- in this case, | First, @create a child of $action. Give it a nice short one-word name -- in this case, 'rape'. | ||
Next, write action:_start(). As noted above, this takes the arguments | Next, write action:_start(). As noted above, this takes the arguments ''(OBJ actor, LIST arguments)'' -- you'll need to decide for yourself what arguments your action needs to know about. In this case, we only need one argument -- the rape victim. | ||
$actions.rape:_start this none this | |||
rapist = args[1]; | |||
$actions.rape:_start this none this | |||
rapist = args[1] | |||
This is the actor performing the action. | This is the actor performing the action. | ||
victim = args[2][1]; | |||
This is our one argument -- the victim. Since 'arguments' is a list, we have to pull off the first (and only) element of it. | |||
victim = args[2][1] | if (rapist.location == victim.location) | ||
rapist:aat(rapist:dnamec(), " begins raping the hell out of ", victim:dname(), "!"); | |||
This is our one argument -- the victim. Since | |||
if (rapist.location == victim.location) | |||
Notice we have to sanity-check the locations of victim and rapist, because someone might have moved since the time this action was queued. Never underestimate the importance of this sort of checking! | Notice we have to sanity-check the locations of victim and rapist, because someone might have moved since the time this action was queued. Never underestimate the importance of this sort of checking! | ||
return {3.0, 1}; | |||
Now we return two elements -- the duration of the action, and a pass-to-finish value to pass to :_finish(). In this case we're passing the success of the rape. | |||
else | |||
return 0; | |||
endif | |||
For the failure case, we're returning 0. Note that :_finish() will still get called. | |||
Now we write the :_finish() verb. Same arguments as before, except we also get a third argument -- the pass-to-finish value returned from :_start(). | |||
$actions.rape:_finish this none this | |||
& | rapist = args[1]; | ||
victim = args[2][1]; | |||
passtofinish = args[3]; | |||
if (passtofinish) | |||
We test passtofinish to see if the :_start() succeeded okay. | |||
rapist:aat(rapist:dnamec(), " pulls up ", rapist.pp, " pants and smiles at ", victim:dname(), "."); | |||
endif | |||
And we're done! A brand new action. | |||
== Simplifying Actions == | |||
Not every action that gets performed needs to have its own action object. There are a couple of ways to simplify things if the action you want to perform is something fairly trivial. | |||
=== Integrated Actions === | |||
Suppose there is some action that is only and always associated with some particular object, like doing math on a calculator for instance. There's no need for a child of $action, since this action will never happen unless someone has a calculator anyway. We can simply add :_start and :_finish verbs to the calculator, and queue the calculator as the action object. Any object that implements :_start and :_finish properly can become a queued action. | |||
=== $actions.verb === | |||
If your action doesn't need to do anything except delay the execution of some existing verb, you can use 'verb' action (#721). This method works by passing the verb to call and its arguments as the action arguments. For instance: | |||
who:queue_action($actions.verb, {#335361, "_open", {who, 1}, 5.0}, 1, "open west"); | |||
This will queue an action that calls the :_open verb on #335361 with the arguments {who, 1}, and that action will take five seconds to complete. Note that if you want the verb to run when the action starts instead of when it finishes, you can pass an additional argument in the action args, like this: | |||
who:queue_action($actions.verb, {#335361, "_open", {who, 1}, 5.0, 1}, 1, "open west"); | |||
This will cause the same thing to happen, but the :_open verb will run at the beginning of the action instead of the end. Probably not what you want in this example, but there are times when that might be appropriate. | |||
Revision as of 18:01, 29 January 2009
What are actions?
As you've noticed, things don't all happen at once in Hell -- the game keeps track of how long an action takes, and makes sure things happen in order. How does this bit of magic happen? The action queue.
The action queue is simply a list of actions waiting to be performed. What's an action?
{OBJ action, LIST arguments, INT 1, STR command-string}
This is an action spec -- a list of four elements describing an action.
- action - the action object to be performed (a child of $action). Most of these have name properties under $actions (i.e. $actions.attack, $actions.move, etc).
- arguments - a list of arguments describing the action; exact format and meaning is specific to each action object.
- 1 - this argument is currently unused. Always pass 1.
- command-string - a string describing the action being performed. If a player-initiated action, the command typed to start the action. This is used in the queue command output.
The action queue is simply a list of lists, each inner list being an action spec of this form. When you type a command that initiates an action, an action spec of this form is constructed and placed into your queue to be processed.
Queueing actions
Putting the action on your queue is done by calling:
actor:queue_action(action-spec)
The arguments to :queue_action are simply the four things from the action spec as described above. If the actor isn't doing anything else already, the action happens immediately. If she is, the action is placed in the actor's .queue property where it waits for its turn.
In your code you can cause yourself or other creatures to perform actions simply by calling :queue_action() with a properly constructed action spec. Let's look at an example: wielding a weapon.
#5:"hold wield draw" this none none
1: if (this in player.wielding)
2: player:notify("You're already holding " + this:dname() + ".");
3: elseif (this.location != player)
4: player:notify("You're not carrying " + this:dname() + ".");
5: else
6: if (this.handed > 1 || $skills.quickdraw:check(player, this.size) < 0);
7: player:queue_action($actions.wield, {this}, 1, verb + " " + argstr);
8: else
9: player:queue_action($actions.wield, {this}, 1, verb + " " + argstr);
10: endif
11: endif
This code does a few sanity checks, then gets to the meat -- player:queue_action().
The first argument is $actions.wield -- the wield action. Simple enough.
The second is a list of arguments to the action -- in this case, we know $actions.wield only takes one argument, the item being wielded, so we pass that as the only element of the list. How do we know that? In the worst case we find out by looking at the arguments to the action's :_start verb, but in a perfect world we would find documentation in the Actions List.
The third and fourth arguments are 1 and some semblance of the player's typed command (for queue display purposes).
And that's it! We've queued an action in response to the player typing wield <whatever>, and now the player will wield the item at his next convenience -- probably immediately, but quite possibly later on if he's already got actions going (say, in combat). It might never happen at all (say, if the player eats a grenade between the time he typed wield <whatever> and the time it would have executed). It may sound obvious, but this is very important to keep in mind: you can't assume when, if ever, your queued action will execute.
Action Queue Processing: Under the hood
What actually happens when an action executes?
Actions are executed by actor:process_queue(). This verb is initiated when an action is thrown onto an empty queue (or under certain other conditions we'll see later), and it doesn't stop until the queue is empty. Let's walk through its logic:
- It pulls the next action off the .queue. This is just an action spec, as we described above.
- It calls :forbid_action(action-spec) on everything in the room, including the room itself. If any of those verbs exist and return 1, the action is skipped entirely; it never had a chance to get started. This forbid_action call is your opportunity as a coder to stop actions from happening under certain circumstances. For instance, it could happen if you're coding a force field that, when active, prevents players from standing up.
- The action's spec is copied into actor.executing, so we know what action we're in the middle of. You can test this property on creatures to see if they're doing something.
- action:_start(OBJ actor, LIST arguments) is called on the action object, where 'actor' is the actor performing the action, and 'arguments' is the list of arguments the action object expects -- the arguments originally given to us when :queue_action() was called. action:_start() does what it needs to do -- test for sanity, print a start message, etc -- and returns back a two item list of the form {FLOAT duration, ANY pass-to-finish} The 'duration' parameter is simply a duration (in seconds) to wait before the action finishes. The 'pass-to-finish' parameter can be anything -- it's used for any data that needs to be given to the :_finish() verb about the results of starting this actions. For many actions this is nothing. You may be tempted to decide whether the action succeeded in the :_start verb, but if that decision depends on a skill check you should not do this. Save the skill check for the :_finish verb, otherwise you could be creating an exploit that allows people to improve quickly by repeatedly starting and stopping the action (and never having to wait for it to finish).
- :process_queue() suspends for the number of seconds given back by the :_start() verb in 'duration'. While this task is suspended, any additional :queue_action() calls on the actor will simply push actions onto the queue, because the actor knows it's already doing something.
- action:_finish(OBJ actor, LIST arguments, ANY pass-to-finish is called. For most actions, the meat of the work happens here. The appropriate time has elapsed and the action can actually do whatever it is it wanted to do, possibly based on the results forwarded to us by 'pass-to-finish'.
- Now that the action is completely done, .executing is cleared, and we move on to the next action, or die quietly if there are no more actions to process.
Creating your own actions
Generally, anything that would take more than a second or two to do in the real world should be an action. Anything that you shouldn't be able to do instantly in combat should be an action. In fact, almost anything that interacts with or alters the virtual reality in any way should probably be an action. As an example, let's create a rape action.
First, @create a child of $action. Give it a nice short one-word name -- in this case, 'rape'.
Next, write action:_start(). As noted above, this takes the arguments (OBJ actor, LIST arguments) -- you'll need to decide for yourself what arguments your action needs to know about. In this case, we only need one argument -- the rape victim.
$actions.rape:_start this none this rapist = args[1];
This is the actor performing the action.
victim = args[2][1];
This is our one argument -- the victim. Since 'arguments' is a list, we have to pull off the first (and only) element of it.
if (rapist.location == victim.location) rapist:aat(rapist:dnamec(), " begins raping the hell out of ", victim:dname(), "!");
Notice we have to sanity-check the locations of victim and rapist, because someone might have moved since the time this action was queued. Never underestimate the importance of this sort of checking!
return {3.0, 1};
Now we return two elements -- the duration of the action, and a pass-to-finish value to pass to :_finish(). In this case we're passing the success of the rape.
else return 0; endif
For the failure case, we're returning 0. Note that :_finish() will still get called.
Now we write the :_finish() verb. Same arguments as before, except we also get a third argument -- the pass-to-finish value returned from :_start().
$actions.rape:_finish this none this rapist = args[1]; victim = args[2][1]; passtofinish = args[3]; if (passtofinish)
We test passtofinish to see if the :_start() succeeded okay.
rapist:aat(rapist:dnamec(), " pulls up ", rapist.pp, " pants and smiles at ", victim:dname(), "."); endif
And we're done! A brand new action.
Simplifying Actions
Not every action that gets performed needs to have its own action object. There are a couple of ways to simplify things if the action you want to perform is something fairly trivial.
Integrated Actions
Suppose there is some action that is only and always associated with some particular object, like doing math on a calculator for instance. There's no need for a child of $action, since this action will never happen unless someone has a calculator anyway. We can simply add :_start and :_finish verbs to the calculator, and queue the calculator as the action object. Any object that implements :_start and :_finish properly can become a queued action.
$actions.verb
If your action doesn't need to do anything except delay the execution of some existing verb, you can use 'verb' action (#721). This method works by passing the verb to call and its arguments as the action arguments. For instance:
who:queue_action($actions.verb, {#335361, "_open", {who, 1}, 5.0}, 1, "open west");
This will queue an action that calls the :_open verb on #335361 with the arguments {who, 1}, and that action will take five seconds to complete. Note that if you want the verb to run when the action starts instead of when it finishes, you can pass an additional argument in the action args, like this:
who:queue_action($actions.verb, {#335361, "_open", {who, 1}, 5.0, 1}, 1, "open west");
This will cause the same thing to happen, but the :_open verb will run at the beginning of the action instead of the end. Probably not what you want in this example, but there are times when that might be appropriate.