Task Sequencer or how I was lazy to learn Lua

About half-way through coding of Bobo Explores Light, it became apparent that the book needed scripted sequences.  For example, on the Glow in the Dark page, Bobo pops up next to a fridge with a bunch of magnets on it.  He reaches out and arranges the magnets in such a way as to create a robot face with which he then proceeds to have a conversation.  The Reflection page has a similar need.  It shows a laser gizmo and four mirrors that the user can position and rotate, such that the laser light reflects and bounces all over.  The first time most people see this page, they don’t know what to do.  So, we script Bobo to demonstrate how the mirrors can be used.

From what I understand, Lua is the perfect language to achieve exactly that.  It is a simple and flexible script that gets interpreted at run-time and that can be hooked into objects and methods in the actual Objective-C code.  Most importantly, it’s already written.  Sadly for me, I have little patience learning yet another syntax for yet another language that I’ll most likely only use once.  So, I looked elsewhere.

Cocos2D comes with a really neat concept of “actions”.  Basically, every node in the scene (sprites, particles, the page itself, etc.) can execute an action.  Most commonly, actions allow you to modify a property of an object over time.  As an example, there is a CCRotateBy action which, when run on an object, rotates that object by a specified number of degrees over a specified amount time.  The following code would rotate mySprite by 90 degrees over the period of 3 seconds:

[mySprite runAction:[CCRotateBy actionWithDuration:3 angle:90]];

This is a really handy mechanism, because you can run an action on a node and forget about it.  Also, there are a slew of actions already defined, anything from actions that tween a property over time (such as an angle) to actions that execute callbacks or batch a bunch of other actions into a sequence.  The batching of actions into a sequence is especially useful, because you can, for example, move a sprite 100px to the right, rotate it by 180 degrees, move it 100px to the left, rotate it again by 180 degrees, and you have yourself a patrol loop for an enemy soldier that you can then repeat forever.

In the case of Bobo, however, I still needed a little more control.  Specifically, I needed to string together a sequence of actions for Bobo, some of which would be run concurrently, others of which would block until previous actions got finished, and so on.  On the Reflection page, for example, I needed the code to accomplish the following:

  1. Find Bobo’s unused arm and move mirror 1 to predefined position
  2. Find Bobo’s unused arm and move mirror 2 …
  3. Find Bobo’s unused arm and move mirror 3 …
  4. Find Bobo’s unused arm and move mirror 4 …
  5. Find Bobo’s unused arm and move the laser to predefined position
  6. Find Bobo’s unused arm and turn the laser on
  7. Say “Tada!”
  8. Wait 1 sec
  9. Find Bobo’s unused arm and turn off the laser

Since Bobo has two arms, actions 1 and 2 can be executed simultaneously.  The same can be said for actions 3 and 4 and actions 5 and 6.  However, action 7 needs to wait until action 6 (and all actions prior to it) have finished executing.  Finally, actions 8 and 9 need to happen in succession, not concurrently.

One solution would be to create a bunch of actions that execute callbacks that poll whether arms are available or not and that, depending on the result, execute other actions and their respective callbacks.  Hello spaghetti code!

Another solution, and the one that I ultimately ended up using, was writing a sequencer.

Imagine an object, a sequencer, that is a fancy wrapper around a FIFO list of other objects, tasks.  You can add tasks into a sequencer at any point and then periodically call the “execute” method that walks through all the tasks in the list and, given some simple rules, calls “execute” on them in turn.  Once a task signals that it has completed, it is removed from the list until, eventually, the sequencer ends up being empty.

To support multiple tasks being run concurrently, each task exposes a variable that defines its blocking behavior.  There are three possibilities:

  • BlockSelf – When sequencer encounters this task, it should stop executing any following tasks until the current task signals that it has finished.
  • BlockAll – When sequencer encounters this task, it should stop executing any following tasks until the current task and all previous tasks signal that they have finished.
  • PassThrough – When sequencer encounters this task, it should continue executing following tasks regardless of whether the current task is still in progress

Using a combination of these behaviors, the Reflection page script translates into the following sequencer calls:

[boboSequencer addTask:[MoveBodyTask taskWithBody:mirror1 position:pos1 rotation:rot1]
               block:kPassThrough];
[boboSequencer addTask:[MoveBodyTask taskWithBody:mirror2 position:pos2 rotation:rot2]
               block:kPassThrough];
[boboSequencer addTask:[MoveBodyTask taskWithBody:mirror3 position:pos3 rotation:rot3]
               block:kPassThrough];
[boboSequencer addTask:[MoveBodyTask taskWithBody:mirror4 position:pos4 rotation:rot4]
               block:kPassThrough];
[boboSequencer addTask:[MoveBodyTask taskWithBody:laser position:laserPos rotation:laserRot]
               block:kBlockAll];
[boboSequencer addTask:[SwitchLaserTask taskWithLaserOn:YES] block:kBlockSelf];
[boboSequencer addTask:[SpeakTask taskWithSound:SOUND_BOBO_TADA] block:kBlockSelf];
[boboSequencer addTask:[WaitTask taskWithDelay:1] block:kBlockAll];
[boboSequencer addTask:[SwitchLaserTask taskWithLaserOn:NO] block:kBlockSelf];

The execute method on MoveBodyTask first checks to see if Bobo has a free arm available.  If so, it grabs it, marks it as unavailable, and starts using it however needed.  If there is no free arm, the execute method simply turns into a noop and keeps waiting in the sequencer’s task list until one of the arms becomes available.  Hence, the first four tasks move the mirrors in sequence and the laser doesn’t get switched on until all the mirrors as well as the laser get positioned correctly.

This made it very simple for me to see what’s going on in code, as well as to modify the sequence however necessary without too much trouble or fear of introducing obscure bugs.

Later I discovered that this simple sequencer / task combo was effective all over the place.  For example, each time the user navigates to the next (or previous) page, a set of tasks needs to be performed: pre-load assets for the next page, slide the UI over, unload data from the previous page, clean up UI, etc.  At first, I hard-coded this sequence into the code manually.  However, then I switched to using the sequencer which allowed me to quickly and easily reshuffle some of these tasks around to compare performance and responsiveness of the app during page turns and come up with the most optimal arrangement.  Butter!

Anyway, enough of me talking – take a look (and feel free to use) the code itself:

Sequencer.h Sequencer.m
DemoProject.xcode


For the demo project, open up the Console window in Xcode and watch the logs (nothing actually happens on screen).  Let me know if it comes in handy!

, , ,

6 Responses to “Task Sequencer or how I was lazy to learn Lua”

  1. SuperSuRaccoon November 8, 2011 at 7:24 pm #

    Thanks for sharing the code, I am sure it’ll come to handy in my next game 🙂

  2. Antoine November 10, 2011 at 8:56 am #

    Awesome, thanks for sharing this !
    I loved Three Little Pigs, and definitely need to download Bobo ! (furthermore, I’ve created a mini-game in the book Magic Of Reality involving lasers and prisms) 🙂

  3. Antoine November 10, 2011 at 9:38 am #

    I’ve just got it and the book is excellent, really excellent. I’ll take a little more time tonight and this week end to read it entirely.
    Very good job !

  4. Aris November 11, 2011 at 2:48 am #

    The DemoProject doesn’t work as expected. It stops to log message “Executing: Task 4”. No repeat tasks get executed.

  5. Aris November 11, 2011 at 3:09 am #

    update: It doesn’t run in Xcode 3.2.6. It runs ok in Xcode 4… strange behavior!

  6. juraj November 11, 2011 at 9:48 am #

    @Aris: The project uses Cocos2D templates for Xcode 4, so that’s probably why. Sadly, when I upgraded Xcode on my machine, I managed to overwrite my old Xcode 3.2, which means that Xcode 4 is all I have currently installed. That said, the code in question should run on any Xcode version. If you need to run the demo project on 3.2, create a blank Cocos2D project, replace HelloWorldLayer code with the code from SequencerDemoLayer, add Sequencer.h and Sequencer.m to the project, and you should be good to go.

    @Antoine: Thanks for your support! I’m glad you liked the app. I haven’t seen Magic of Reality yet, but it’s definitely on list to download. It has received tremendous reviews.

    @SuperSuRaccoon: I hope the code will work out for you!