RiverMan Media

RiverMan Media 1

Newsletter

Keep up with info on our latest games, articles, and other projects. You can unsubscribe at any time.

Subscribe

XML RSS 2.0 RSS 2.0 

Object Oriented Game Programming: The Scripted Event System
Object Oriented Game Programming: The Scripted Event System - ActionEvents and Commands
Written by Paul Stevens   
Thursday, 02 October 2008 03:37
Article Index
Object Oriented Game Programming: The Scripted Event System
ActionEvents and Commands
ActionLists
Example 1: Cinemas
Example 2: Gameplay
More Examples
All Pages

Uniting Behavior, Effect, and RobotFunction
First I had to figure out how I would be able to put Behaviors, Effects, and RobotFunctions all into the same script. It seems like these three objects all work totally differently, right? Actually, they are more similar than I thought, at least in a few key ways. They all have an update() method and they all have listeners (of some sort). This is all the script needs to know, since all the script does is call their update() method and sets their listener to be itself. So to tie all of them together, we can have them all incorporate a simple aspect, ActionEvent (if you aren't familiar with Slag, just think of an aspect as a Java interface for this example).

underlying aspect ActionEvent
  METHODS
  method update():
  method setListener(ActionListener listener):
endAspect


underlying aspect ActionListener
  METHODS
  method callback(ActionEvent caller):
endAspect


# This code represents the new type declarations,
# which would show up elsewhere
abstract class Behavior : ActionEvent
abstract class Effect : ActionEvent
abstract class RobotFunction : ActionEvent

Okay, so now we've established that Behaviors, Effects, and RobotFunctions can all operate the same way in a script because they all incorporate ActionEvent (implementing the update() and setListener() methods). The ActionListener aspect, which our script incorporates, allows our script to listen to the event callbacks from the ActionEvents.

On to the next problem: our script doesn't just need to support ActionEvents, it also needs to be able to handle commands that execute instantly. These commands can be any arbitrary code. The lazy way to accomplish this would be to make a fourth kind of ActionEvent that only runs for 1 update cycle, and immediately notifies its listener that it is done. But that seems a hack, since if the command runs for an entire update cycle, then 10 commands in a row will take 10 updates, which isn't quite what we want. This would completely throw off the timing of the script.

Commands Are Different
Instead, why not make a new class, called a Command. Specific Commands override one method, execute(). When the script encounters a Command, it just calls the Command's execute() method and moves on. If the script encounters multiple Commands in a row, it processes them all at once in a while loop until the script is empty or it encounters an ActionEvent. Simple enough. Special note: in Java, you can declare, create, and add Command objects to the script as anonymous classes, keeping the code very compact and readable.

But how does a script hold both Commands and ActionEvents? How does it know which one to run? Well, since a script is implemented as a queue, it doesn't hold the Commands or ActionEvents directly, it actually holds ListNode Objects, which in turn knows the type of its data. But of course, that just passes the problem on to the ListNode class.

Two Node Types
All we have to do is make the ListNode a little bit more complicated than usual. ListNode becomes AbstractListNode, which has two children: EventNode and CommandNode. You can probably see where this is going. EventNodes hold data of type ActionEvent, and CommandNodes hold data of type Command. How does the script tell between the two kinds of Nodes? A simple int value called nodeType in the AbstractListNode class. For EventNodes, the nodeType is set to 0 in the node's constructor, and for CommandNodes, the nodeType is set to 1.

All of the pieces are in place. The script is a queue of AbstractNodes, and it can figure out which kind of data the Node holds. Here's is a simple version of what the script's update() cycle looks like:

method update():
  while (firstNode().nodeType == COMMAND_NODE)
    # cast operation
    local CommandNode c = firstNode().(CommandNode)
    c.data.exectue()
    dequeue()
  endWhile

  if (firstNode().nodeType == EVENT_NODE)
    # cast operation
    local EventNode e = firstNode().(EventNode)
    e.data.update()
  endIf

  # if the script is over, inform the listener
  if (firstNode() is null)
    listener.callback(this)
  endIf

method callback(ActionEvent e):
  dequeue()
  if (isEmpty() == true) listener.callback(this)

There are a few 'null' checks missing here, but this shows the basics. The update() call executes all of the Commands at the head of the queue first, then moves on to the ActionEvents, calling update() just once on the first one. If there aren't any commands at the head of the queue, it won't execute any Commands, and will just update() the ActionEvent at the head of the list (if one exists).

ActionEvents and callback()
In this code, we can see how the CommandNodes are removed from the queue during the while loop, but what about the ActionEvents? These are handled slightly differently, since we don't know when the ActionEvent will be done. The script, as an ActionListener, implements a callback(ActionEvent) method. When the Behavior/Effect/RobotFunction is finished, it invokes the method "listener.callback(this)." "this" is the caller (e.g., the behavior), and "listener" is the script.

ActionEvents and Commands are added to the script in the script's enqueue() method, which is also when the node is built to hold them. When an ActionEvent is added to the script, the script sets the ActionEvent's listener to be the script itself. When the script hears the callback() method from an ActionEvent, it invokes dequeue(), removing the calling ActionEvent from the head of the list. If the queue is now empty, the script in turn informs its own listener (which might be a menu, an enemy, a cinema, or whatever Object contains this particular script), by invoking listener.callback().



 
Comments (5)
Nice
1 Friday, 16 January 2009 17:32
Thanks for sharing your ideas! I just may use them in my projects, although I'm coding in Flash AS3, i think it could work just as well.

Good job!
How to access intances of objects?
2 Wednesday, 20 May 2009 12:02
HexDump
Hi,

Thanks for sharing this nice iformation. I have implemented the ideo on c# but there's something I can´t find out to do it in a clean way. Imagine this script (Each step is one EventAction):

1) Play Anim
2) Create Particle system
3) Animate with a robot the Particle system position.

How do you handle the third point? You need the particle system instance the robot funcion needs to animate?

Thanks in advance,
HexDump.
RE: How to access intances of objects?
3 Wednesday, 20 May 2009 17:45
RiverMan_Paul
Thanks, I'm glad you liked the article! That's a very good question.

The easiest way to do this is basically what you said: instantiate the particle system while you are creating the script and pass it to the command (or behavior, or whatever) that is responsible for the animation. You can organize the particle system's code so that it doesn't load any data just for instantiating it. That way, it isn't taking up much memory until it is supposed to actually run, at which point some item in the script can call particleSystem.load(), and start things running. I almost always use this approach because it is the simplest.

If you really don't want to instantiate the particle system before hand, there are a few options. One is to set up a "global" variable somewhere and pass a reference of it to the parts of the scripts in step 2 and 3. That way, when step 2 creates the system, it's already ready for step 3, which knows to look at the same object. The particle system could be placed into any kind of data structure, as long as both parts of the script know how to access the particle system once its created.

Option 3 would be to create a special command that encompasses steps 2 and 3, generating a particle system and passing to the animation. Again there are quite a few choices for how to do this, but probably the easiest way would be to create a step 2.5 command that facilitates communication between steps 2 and 3, making sure that both have the same data. You could make the input parameters for this communication command general enough so that you can reuse it in other situations (it doesn't have to be completely specific to this problem).

Would any of those solutions work in your case?
More problems....
4 Wednesday, 20 May 2009 21:51
HexDump
Hi, and thanks for the answers.

Well, I solved the problem more ore less, I have to rethink about it, thanks for the options you offered.

I´m noticing that things are getting complicated. I for example waht to create an effect that is like a meteor rain. This leads me to think that I will need something to do loops, and if you add to this that you want to listen to collisions in the script things get more and more complicated.

For the collision thing I managed to create a WatColliison action that could do the job, but for the loop thing (because you need to create lota of balls that are particle system and do same for all, it is like having 1 script thread per particle system) I can´t get a good solution.

My game is something like Robot Wars where cinematics are really important.

Thanks in advance,
HexDump.
RE: More problems....
5 Friday, 22 May 2009 05:58
RiverMan_Paul
If you wanted to create something like a meteor rain, would it be necessary to use a complicated script setup, or would it work to have MeteorRain be it's own class that runs alongside the script?

Usually for complicated cinematics, I have an ActionList that encompasses and runs the main script (because scripts can be added to ActionLists, and vice-versa). The script adds ongoing effects (such as a meteor rain) to the list when necessary, and then the script tells it to stop when it is supposed to be done.

Add your comment

Your name:
Your website:
Subject:
Comment: