Zippy-Egoboo Home EgoWiki > Documentation > MakingZippyObjects > ZippyScriptingGuide > ZippyActionGuide EgoWiki webs:
Main | TWiki | Know | Sandbox
Documentation . { Changes | Index | Search | Go }
If you're hailing from the 2.x crowd, you may have been puzzled by all of the alerts that are labeled "no longer used". Further, you may notice that particles can't do damage anymore, and that there's nothing about shops anywhere in the documentation, or doors. Even the dropped and grabbed alerts are going away; what's going on?

And you may just be wondering how objects get to do stuff, or how other objects find out about it.

So you may find answers to your questions upon discovering that Zippy has formalized actions in to a system of Actions. Or you may be dismayed at having to learn another nuanced subsystem just to open doors. Oh, well!

Actions and Results

The action system has two parts: see, there's Actions, and there's Results. But actually, results are split up into Effects. There are lots of Effects, and lots of Actions, and you can see them all in basicdat/action_classes.lua. Also be aware that the full reference for Zippy's action system is documented in the ZippyActions topic. Let's look at a typical Action/Effect pair:

EffectAxis{
  name = "OpenEffect",
  namespace = Basic,
  valid = Range{0,100}
}

ActionClass{
  name = "OpenDoor",
  namespace = Basic,
  effects = {
    Basic.OpenEffect
  }
}

This looks like a lot more than it is. All it says is that one kind of action is the action of opening a door, and that the degree to which the door opens can be measured from 0 to 100.

By the way, EffectAxis? is just a fancy name for an effect. It comes from the fact that each Effect is measured on a numeric scale, and a bunch of Effects can be part of the same action, and then the result is a bunch of independent numeric scales. So we thought we'd be clever and call the result a result vector, and measure each effect on an EffectAxis?. The only important point in this discussion is that effects are measured on a numeric scale, and the "valid = ..." part of the example above shows what numbers are valid for the OpenEffect?.

But this just tells us something abstract about the game world that we already knew. It's nice to know that doors can be opened, but how do we do it?

Action Resolution

The file objects/door.lua defines a Door object that every door-like thing derives from (take a look at the ZippyInheritanceGuide if you don't know what this means). Now, imagine a character wants to open a door-like object, say it's a gate. In Egoboo, this means he'll bump into it. He'll get a passage_blocked alert for his trouble if the gate's closed. Now, he could give up in frustration, but luckily for him we have an Action that's good for opening doors. He can do it like this:

act(Basic.OpenDoor, self, passage.owner)

This says, "do the action OpenDoor? from the basic namespace." It also says that the one doing the opening is "self" and the one being opened is "passage.owner". "passage.owner" is going to be the door.

But how does this help us? How does the engine know that OpenDoor? means, "do the opening animation for the door model and unblock the passage"? All we've told it so far is how to measure the results.

The answer, of course, is that the engine knows nothing about doors. The OpenDoor? stuff is not really meant to describe to the engine how doors are opened. Rather, it is meant to describe to the engine how to determine whether the door gets opened or not. The engine then comes back and asks us to do the opening ourselves.

So what happens when we call act()? First, the engine figures out who's involved. This, obviously, is "self" and "passage.owner". "self" in this case is the origin, the initiator of the action, and "passage.owner" is the target, the object being acted on.

Next, the engine asks the participants to agree on the results of the action. The origin is asked first. Then the target is asked, and whatever the target says is the end result. The engine "asks" by calling a method on each object. In this case, that method is "poll_OpenDoor". Let's look at this method in the character who's opening the door:

poll_OpenDoor = function (self, action, result)
  if result then
    return result
  else
    return {OpenEffect = 100}
  end
end

As you can see, this is very simple. The character wants to open the door, and so it of course has no objection to the door opening. Of course, the door gets final say, and that's where most of the logic is going to happen. For example, the door might define a poll_OpenDoor() method that checks to see if the player has the appropriate key to open the door (this is what the Door class does). What's important, though, is that the poll_OpenDoor() method (and any other polling method) should return a table, mapping effect names to values. You'll notice that the poll_*() methods all take a parameter called "result"; this is the return value of the last poll function that was called.

We won't look at the actual code for the Door class's poll_OpenDoor() function, but we'll assume that it returns a table with "OpenEffect" mapped to zero if the opening fails, or 100 if it succeeds.

The next step the engine takes in resolving an action is to apply the results that were generated while polling the participant objects. The engine does this by calling an "apply" method, in this case apply_OpenDoor(). This time, however, the order is reversed. Here's the door's apply_OpenDoor method:

apply_OpenDoor = function (self, action, result)
  if result.OpenDoorEffect > 50 then
    self:open()
  end
end

Again, a very simple function. If the OpenDoor? effect had a result greater than 50, the door calls its own open() method, which is where all the tricky things with animations and passages happen. And then we're done.

But why, you might ask, is there no apply_OpenDoor() method for the character? There could be, of course, but there wouldn't be anything to do since the door is already open. As an example of what could be put in the apply_OpenDoor() method for the character, you might imagine that opening a locked door could cause the key to be destroyed. To make this happen, you might add a second effect to the OpenDoor? action, something like KeyLossEffect?, and the character's apply_OpenDoor() method might check to see if the key it used to open the door should be destroyed or not.

Where to Put Stuff

So by now we've seen the basics of the Action system. But we've also seen potentially four different places to put your code, just for a single Action class and Effect. Here's a quick guide for dividing your tasks between these functions. They're listed in the order they would be called during the resolution of an action.

Origin's poll_Action:

Since this is the first function called, it should return the "ideal" result for the origin character. Be careful that you only consider constraints based on the character itself or its items. For example, for an archer aiming at something, the polling function should take into account the quality of his bow, and his archery skill. For opening doors, unless the character is injured in some way that prevents him from opening doors, the result should be a perfect opening as returned by this function.

Target's poll_Action:

This function should adjust the result based on properties of the target. For a character being aimed at by an archer, considerations might include the target's agility, and the target's general visibility (for example, the probability of a perfect aim might be lowered if the target is invisible).

Target's apply_Action:

This should apply the results of the action, as far as the concern the target. Don't put anything in here that would change the origin, for example, or some other participant. They have their own apply functions.

Origin's apply_Action:

As with the target's function, this should apply results that concern the origin. If there's an attack going on, you must refrain from applying damage in this function -- it should go in the target's function instead.

Watches

So far we've looked at actions with only two participants. This is fine to start with, but what if you want to make a shield of 1/2 damage? The sheild isn't the target of an attack, and it isn't the origin of the attack either, so how can it affect the damage done to the character from an attack?

Let's start by getting some background on attacks. Let's propose the following set of actions and effects:

EffectAxis{
  name = "DamageEffect",
  namespace = Basic,
  valid = Range{min=0}
}

ActionClass{
  name = "Impact",
  namespace = Basic,
  effects = {
    Basic.DamageEffect
  }
}

OK, no surprises here. Impact is an action, and an effect of the action is damage. One thing we want to know for sure, though, is when the action takes place. You may be tempted to view the attack action as beginning from the swing of the sword and ending when the damage is done. But that's not how the game works; Actions start and finish on the same update, always. So there are actually two actions involved in any attack: the one that results in the attacker swinging his sword, and the one that takes place when the sword connects with something. Impact falls in the second category. It's origin is the weapon, and its target is the poor character getting cut in half.

Now, let's think about our sheild. It can't do anything about the damage done from an Impact unless it is party to the polling process. It'll have to have its own poll_Impact and apply_Impact methods. By now you should know what to put in which. But how do these methods get called?

The engine provides another important concept to supplement Actions, and that is the Watch. When a watch is in effect, some third-party object -- otherwise unrelated -- can become a participant in one action or a group of them. To create a watch, you need an object that defines the appropriate polling and application methods. You can then call the watch{} function to create the watch. Here's an example:

watch{
  owner = our_sheild,
  criteria = {
    { criterion = {our_sheild.attached_to}, role = "target", sufficient = 1 }
  }
}

Lots of parts here. First off, the owner of the watch (the object that gets polled) is the sheild. Fair enough. Next is this "criteria" thing. You may be wondering at this point how you can specify what actions get watched, and this is it. Every watch has a list of criteria (we have only one here). You specify criteria in tables, and each one has three fields (there are actually four, but we're only using three for now). First is "criterion", which is the thing we want the watch to pay attention to. In this case, it's a table of characters, actually just one character in a table here. That's the wearer of our sheild. So by now we've said that we only want to know about actions that involve the sheild's wearer as a participant. Next is the "role" field, which in this case is "target". This means that we further want to restrict our attention to include only actions that target the bearer of the sheild (as opposed to actions initiated by the bearer of the sheild). Finally, "sufficient = 1" means that if this criterion matches, the watch is relevant and the sheild gets polled. Now, if the sheild only defines poll_Impact, we're good to go as far as watching is concerned, because this watch will catch only the actions we're interested in: Impacts carried out against the sheild-bearer. We can then modify the DamageEffect? part of the result by dividing it in two, simple as that. Just for kicks, here's the sheild's poll_Impact method (in this case we don't need an apply_Impact method, because the damage is done in the target's apply_Impact() method):

poll_Impact(self, action, result)
  if result then
    result.DamageEffect = result.DamageEffect / 2
    return result
  else
    return {DamageEffect = 0}
  end
end

There you have it.

Now one question remains: where do we call this watch function, and how often? The watch is good forever, so technically we only need to call it once. But what happens when the sheild-bearer lays down his sheild? Then the watch will still be in effect, and the former sheild-bearer has 1/2 damage protection for life. Good deal for him, but not exactly what we had in mind. So what we need to do is make a new watch whenever the sheild gets picked up, and terminate the old one whenever it's dropped. But we haven't learned how to terminate watches yet, so let's do that.

First of all, the watch{} function actually returns a value, and you can save it like this:

self.sheild_watch = watch{...}

This value has a few useful methods in it. The one we're interested in right now is called "remove". Here it is in action:

self.sheild_watch:remove()

This makes the watch stop looking for actions to send to its owner. But the watch isn't dead yet! If you wanted, you could "add" it again:

self.sheild_watch:add()

And it'll start watching again, just like before. As with any value you save for an extended period of time, it's important to make sure you let go of it when it's no longer useful. Since the sheild doesn't know whether it will ever need this watch again after it is dropped, you should release the watch value like this:

self.sheild_watch = nil

Failing to do this won't cause any problems, except that the watch (which is now useless) sit around taking up memory until the game ends -- and if that happens a lot the game will slow down.

Final Notes

Make sure you read the ZippyActions reference documentation. There's more power built into the system than we've covered here (we've covered most of it, though). Also check out the basicdat/action_classes.lua file and try to figure out what all the actions do, and how they are handled by various objects.

-- ElminI - 05 Nov 2004

Topic ZippyActionGuide . { Edit | Attach | Ref-By | Printable | Diffs | r1.5 | > | r1.4 | > | r1.3 | More }
Revision r1.5 - 05 Nov 2004 - 23:15 GMT - ElminI
Parents: WebHome > MakingZippyObjects > ZippyScriptingGuide
Copyright © 1999-2003 by the contributing authors. All material on this collaboration platform is the property of the contributing authors.
Ideas, requests, problems regarding EgoWiki? Send feedback.