Tweens & Jugglers - An in-depth look at the Juggler

Daniel Sperl on September 16, 2010

This time, I want you to show how the juggler works and how you can use this simple tool to be the master of your animations. This topic comes up quite often in the forum, so I thought a detailed explanation might help a lot of you.

In reality, tweens and jugglers are very simple objects. If you understand how they work, you can handle all time-based stuff in your games very elegantly.

Tweens, inside out

Before we look at the juggler, however, we need to understand how tweens work internally. Let's look at the security door tween we've created the last time:

// create the door and display it
SPImage *door = [SPImage imageWithContentsOfFile:@"door.png"];
door.y = 0;
[self addChild:door];

// create a tween that moves the door down  
SPTween *tween = [SPTween tweenWithTarget:door time:4.0];
[tween animateProperty:@"y" targetValue:480];

Notice that I did not add the tween to the juggler - we want to manipulate the tween manually in this sample. Look at this:

// starting with: door.y == 0

[tween advanceTime:1.0]; // door.y = 120, tween.isComplete = NO
[tween advanceTime:1.0]; // door.y = 240, tween.isComplete = NO
[tween advanceTime:1.0]; // door.y = 360, tween.isComplete = NO
[tween advanceTime:1.0]; // door.y = 480, tween.isComplete = YES

As you can see, every time I call the method "advanceTime:" on the tween, it updates the door's y-value. There's no magic behind a tween - all you have to do is call the "advanceTime:" method regularly (read: once per frame).

I guess now you see where I'm going. In theory, you don't need a juggler at all - you could create an enter frame event listener and update your tween once per frame, like this:

- (void)onEnterFrame:(SPEnterFrameEvent *)event
{
  [tween advanceTime:event.passedTime];
}

In reality, however, that's burdensome. Work like this should be delegated to a cheap assistant that does not ask any dumb questions - and that's why you hire the juggler for that job! :-)

A custom juggler

If you use the stage juggler (accessible to any display object by calling "self.stage.juggler"), you just have to add the tween to the juggler, and it will carry out the animation for you. We demonstrated that in the last part of this series. However, we won't use the stage juggler now, but create our own juggler.

But why? I'll tell you a secret: I rarely use the stage juggler. In my games, I always create my own jugglers. On the long run, that makes life easier. You will see why that is at the end of this tutorial.

The juggler is a really small class. It does one thing, and one thing only: advancing all tweens you give it. Look at this:

// create a custom juggler
SPJuggler *juggler = [SPJuggler juggler];

// reset the door's position
door.y = 0;

// create another tween
SPTween *tween = [SPTween tweenWithTarget:door time:4.0];
[tween animateProperty:@"y" targetValue:480];

// now, give it to the juggler
[juggler addObject:tween];

However, the door won't move yet! Just like the tween, the juggler needs to be advanced:

// door.y == 0

[juggler advanceTime:1.0]; // door.y = 120, tween.isComplete = NO
[juggler advanceTime:1.0]; // door.y = 240, tween.isComplete = NO
[juggler advanceTime:1.0]; // door.y = 360, tween.isComplete = NO
[juggler advanceTime:1.0]; // door.y = 480, tween.isComplete = YES

Bummer. We have not gained much, have we? That's just as much work as before!

Well, in this simple example, the juggler really does not help much. But in a typical game, you have a lot of animations, and then it suddenly makes sense. We'll see that later.

BTW, have you noticed the "isComplete" property of the tween in the code above? That information is crucial to the juggler, because it tells the juggler when an animation is finished. As soon as that happens, the juggler can throw away the tween.

A real life example

Let's look at an example that comes closer to real life.

When I create some building block of my game that is bound to become more complex, I always start with a scaffold like this:

@interface PlayingField : SPSprite
{
  SPJuggler *mJuggler;
}
@end

@implementation PlayingField

- (id)init
{
  if (self = [super init])
  {        
    mJuggler = [[SPJuggler alloc] init];
  }  
  return self;
}

- (void)advanceTime:(double)seconds
{
  [mJuggler advanceTime:seconds]
}

- (void)dealloc
{
  [mJuggler release];
  [super dealloc];
}

@end

It's just a sprite with a juggler and a method that is supposed to be called once per frame. This class will become the playing field, a typical building block of a game. I might also have a class "Dashboard" (showing the health and number of lives of the player) and a class "MessageBox". All those major building blocks have some animations, so they all look start like the class above.

Whenever an animation is started from within those classes, I add the corresponding tweens to its juggler (mJuggler). Thus, the animations of each class are all separated.

Now somebody has got to handle those classes! That's the stage, normally. Look at this:

@interface Game : SPStage
{
  PlayingField *mPlayingField;
  Dashboard *mDashboard;
  MessageBox *mMessageBox;  
}

@end

@implementation Game

- (id)initWithWidth:(float)width height:(float)height
{
  if (self = [super initWithWidth:width height:height])
  {
    [self addEventListener:@selector(onEnterFrame:) atObject:self
                   forType:SP_EVENT_TYPE_ENTER_FRAME];
                   
    // create building blocks of the game
    mPlayingField = [[PlayingField alloc] init];
    [self addChild:mPlayingField];
    [mPlayingField release];
    
    mDashboard = [[Dashboard alloc] init];
    [self addChild:mDashboard];
    [mDashboard release];
    
    // that one will be created on demand - e.g. when the
    // player hits the pause button or is game over.
    mMessageBox = nil;    
  }
  return self;
}    

- (void)onEnterFrame:(SPEnterFrameEvent *)event
{
  double passedTime = event.passedTime;
  
  if (mMessageBox)
    [mMessageBox advanceTime:passedTime];
  else
    [mPlayingField advanceTime:passedTime];
    
  [mDashboard advanceTime:passedTime];  
}

@end

Now we get to the point where it all comes together. The reason that all those classes have their own juggler is that we want to control them separately! Look at the "onEnterFrame:" method. As long as a message box is active, the playing field will not move one pixel. It's, in effect, frozen where it is! As soon as the message box has disappeared (it's nil), it continues where it left of. The dashboard, however, is animated continuously.

To see why this is so useful, look at the alternatives. Say you had used one juggler (probably the stage juggler) for all animations. Now the user hits the pause button. How in hell do you stop all animations? You'd have to remember each and every tween and remove it from the stage juggler. Then, when the game continues, you'd have to add them again. That's a lot of work! Besides, I guarantee that you'll forget one tween sometime, perhaps creating a hard to find bug.

Best practices

Thus, my recommendation is to construct your games just like the sample above:

  • Create separate classes that constitute the main building blocks of your game.
  • Give each of those classes their own juggler.
  • Create an enter frame event listener in your stage class and let it be responsible for advancing the animations of your building block classes.

Another interesting trivia about the juggler is that it can handle more than just tweens. Every class that implements the "SPAnimatable" protocol can be added to a juggler. I already demonstrated that in this blog post.

That's it for today! Be sure to check back here soon, when we have more tips on how the juggler can make your life simpler.