|
Page 1 of 6
The Scripted Event System has saved me more time than any other idea I've ever had as a game programmer. We use it everywhere. It controls cinemas, runs AIs, executes visual effects, creates complicated character animations, and much more. By itself, it is a powerful and flexible system, but its true strength comes from how it works in conjunction with the Behavior System and Robot Functions.
What are scripted events?
In the Scripted Event System, a script is essentially an ordered list of actions. The actions in the script could be anything: moving an object around on the screen, shifting an object's color from red to blue, commanding an enemy to move forward, etc. The point is that the actions are somewhat predetermined and execute in order, one after the other.
When the script is over (i.e., the queue of actions is empty), the script informs whatever object is listening for the end of the script. At that point, the listening object may add some more items to the script and have it begin running again, or it could start running another script, or take some completely different action. The system as a whole is very simple to implement and easy to use, once you understand how it works. That's what this article is for!
Important Note
The Scripted Event System relies heavily on concepts explained in the Behavior System and Robot Functions articles. I strongly recommend that you read these articles first if you haven't already. "Effects" are also part of the Scripted Event System, but for now, you can just think of Effects as objects that play an ongoing action, usually a visual effect. I'll probably write an article on Effects soon.
Origins of the Scripted Event System
Coding Cinemas
Early on in the development of Primate Panic, we knew we wanted to have some fairly complex cinematic scenes. As an inexperienced programmer, I had no idea how I was going to code them. I had never programmed a cinema before, and it seemed very difficult. So I tried to come up some ideas for how it could be done.
State Machine?
First, I thought maybe a large state machine could control each cinema. As events occur, they change the state of the cinema, and cinema would start running the new state. Each state could just be an integer, and the scene would determine which one is running in a switch statement. It seemed simple enough, but I knew it wouldn't work long before I tried to implement it.
Complicated, longer cinemas have dozens, even hundreds of individual actions that occur in order. Coding all of these tiny states by hand for each cinema seemed like a monumental task, and it also didn't seem very flexible. Each cinema scene would have to be an enormous class, have code for hundreds of tiny states, and would get unmanageable very quickly.
A natural extension of the idea would be to make each state an Object, and have it override an update() method. Then each cinema scene class would just have a reference to the currently running state and tell it to update. This would put all of the code in the individual state Objects, which sounded like a better idea. But this didn't solve the problem of each state being so tiny. Even if I put several actions in each state to make them bigger, I would still need at least dozens of classes per cinema, all of which would grow more complicated by being tiny state machines themselves. And what about the fact that a lot of the states would do similar things, like say, run an animation, or move an object around? No, states as Objects just wouldn't do either.
Starting With The Interface
Having spent a little while trying in vain to figure out how I could make a state machine work without each cinema taking weeks to program, I decided I needed to attack the problem from a different angle. So I asked myself the question, "If I had the perfect scripting system, what would my code look like when I was using it?"
This question instantly put the design in perspective. Instead of fumbling around with how the system might work, I started thinking about how I wanted to use it. I pretended that the system itself already existed, and started writing code with it in my head. One important thing I wanted to do was reuse my code for Effects and Behaviors in the cinema. Here is what I was thinking (code examples in Slag):
# Primate Panic cinema sequence
class AirplaneCinema
PROPERTIES
cinemaScript : Script
METHODS
method init():
cinemaScript = Script()
# Move the plane toward the middle of the screen
cinemaScript.enqueue(MoveBehavior(plane, 200, 50))
# Run a lightning effect that strikes the plane
cinemaScript.enqueue(LightningEffect(plane))
# Start thunder sound effect (instantaneous)
cinemaScript.enqueue(PlaySound(thunder))
# Start smoke generating from the plane's wing (instantaneous)
cinemaScript.enqueue(SmokeEffect(plane))
# The plane falls downward
cinemaScript.enqueue(MoveBehavior(plane, 200, 250))
method update():
cinemaScript.update()
endClass
The basic idea here is that the entire script is built in the constructor of the CimenaScene class, and all the scene does is run the script (by simply telling it to update() every frame). When a script element has finished executing, it is dequeued from the front of the script, and the next element runs automatically. Each command should only be one line long, even if it does something fairly complicated, like move an object around. Also, the script needs to be able to handle two kinds of members: 1. Members that run for several update cycles, leaving the script when they are done, and 2. Members that execute some arbitrary code and leave the script in the same update cycle. In this example, the script elements that move the plane around would operate over several updates, while starting the thunder sound effect would be instant.
Now I knew what I was shooting for, although it seemed impossible at first. The interface that I wanted sounded so great--it could handle really diverse actions, and they would be added to the script in just one line of code each. It would be very easy to change the order of the script, since all you'd need to do is change the order of the instructions. And because the script is implemented as a queue, it handles the control flow between script elements automatically.
And the best part is that it would work with the Behaviors and Effects that I have already written. Why write a new script action that plays a lightning effect when I already have a lightning Effect class? Or why write a script element to move an object to a specific point when I've already written the Behavior for this? (Actually, simple movement is accomplished more efficiently using a Robot Function than a Behavior, but when I first started the Scripted Event System, Robot Functions didn't exist yet).
But was this actually possible? Could I keep the code for the script as condensed as my example without sacrificing the flexibility of the scripts? It turns out that all of this is possible, and although it took some time to figure out the solution, it's very easy to implement.
|
Good job!
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.
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?
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.
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.