LibGDX: acting lessons
In my last post When Actors Act Up, I posed a question about a practical issue I ran across while doing some LibGDX development. How do you sequence actions when they're spread out across different actors? To play with this question, come up with some solutions, and request your input, I wrote a simple simulation that illustrates the problem!
Jack and Jane Race
The game is quite simple. Two characters – Jack and Jane – race back and forth across the screen.
The requirements of the simulation are thus:
- Jack and Jane wait for 1 second before starting the race.
- The light turns green and the race starts.
- Each character randomly chooses their speed (really how long it takes to get to the finish line).
- When both characters have reached the finish line, the winner is declared, they both turn around, and the cycle starts again (with another 1 second wait).
The audience is really impatient and they don't want to wait any longer than 1 second to get to the action, so it is very important that races start exactly 1 second later regardless of whether the race took half a second or one-and-a-half seconds!
For the purposes of illustration I modeled this entirely with Scene2D and Actions. You can find all the code here, but the real meat of the game – the place where the action loop is created – is in RaceActionGenerator. This code is commented with numbers that correspond to this Actor and Action time line:
Jack and Jane are Actors that belong to a Group which belongs to the Stage. The action loop is run on the Group so that the actions can be sequenced. The phases are as follows:
- 1 second race delay
- A RunnableAction sets the lights, picks the running speed for each actor, and adds an appropriate MoveBy action to Jack and Jane. After the MoveBy action completes, a FireEventAction action sends a signal that that runner is done racing.
- A FinishLineListener listens for two RacerDoneEvents and fires a RaceWinEvent. While Jack and Jane are running, a GateAction (on the Group) waits until the RaceWinEvent has been witnessed. (RaceWinEvent is also caught by the score board actor.)
- The race over, a final RunnableAction resets the lights and turns Jack and Jane around so they can start a race in the other direction (following another 1 second delay).
If you're familiar with LibGDX, you might be saying, "FireEventAction? GateAction? What the heck are those?" Well these are the tools I promised at the start of the post! I created these special Actions to make Jack and Jane race to my exacting specifications: FireEventAction and GateAction. Why?
Normally when Actions execute, they happen in parallel completely independent of one another. You can set up Actions to run one after the other using, of course, a SequenceAction. But that only works if all of your Actions apply to the same Actor! Here, I need my sequence loop (running on the Group) to pause and wait for Actions on Jack and Jane to complete! You might also be saying, "why don't you just simply put in a delay? You know how long Jack and Jane are going to be running ahead of time." That's a very astute observation and entirely correct! However this is just a simplified example to capture the essence of a nuanced problem. There may be times when the thing you want to wait on isn't so cut and dry. Perhaps you need to stall a sequence until the user clicks on something. Perhaps the action sequence you need to pause isn't responsible for creating the other actions. So a DelayAction isn't always going to be an acceptable answer: we need something more sophisticated.
Events to the Rescue!
LibGDX's Events are perfect for connecting otherwise independent parts of a system without tightly coupling them. Tight coupling is of course fraught with potential problems, so we should avoid it when it's not too painful to do otherwise. The FireEventAction is entirely straight-forward: it allows us to quickly and easily thread the firing of events into a sequence of actions. We want to know when Jane is done running, so we set up an event to fire when she is. Piece of cake.
Events in Scene2D also have the convenient quality of bubbling through the actor hierarchy. An Event fired by Jack can be caught and handled by a listener on Jack, the Group, or the Stage. This "funneling" allows us to catch events at a single point – in this case I chose the Group. You can read more about events on the LibGDX wiki.
So we have our racers firing events to notify the system that they've reached the finish line. Now we need to listen for those events and, in the mean time, pause the main action loop until we see two of them (because there are two racers!) This is where GateAction comes in. I called it "Gate" because it blocks the progress of other actions (when used in a sequence). The key to a gate is the Events it observes. GateAction is simultaneously an event listener and an action. When the action is added to an actor, it registers itself as a listener.
Now I could have set up my GateAction to watch for two RacerDoneEvents, but it's actually more convenient to treat the end of the race as a single event from this point forward. To accomplish that, I used another listener on the Group that transforms two RacerDoneEvents into one RaceWinEvent.
So getting back to the action loop, I configure my GateAction to listen for a single RaceWinEvent before allowing the loop to continue! Simple as that. Now you may notice if you look at the code that I use a single instance of this GateAction and insert it twice into the action loop (once for the left-to-right race, once for the right-to-left race). I had to use a single instance here because of an implementation detail with how LibGDX handles sequences. When a sequence is added to an actor, all the actions that are a part of that sequence are simultaneously added to the actor. Since GateAction is also a listener if I have two different GateActions, I have two different listeners. However Actions don't know when they're "active", so my GateActions are always listening. This meant that my RaceWinEvent was triggering both gates and the races got all buggy. A single GateAction (which resets itself after it's complete) solves the issue without having to worry about deactivating or removing actions when they're not in use.
So those are my novel inventions that I am humbly offering to the LibGDX community as food for thought. If you
have alternative solutions to this problem, I would love to hear about them! I posted the code on GitHub so that
you can fork and code up your solutions if you have a mind to. Otherwise feel free to use
FireEventAction in your own code. Best of luck out there!