Workflow and Animation – Code

Go here to get previous and subsequent posts in this series.


The two classes with primary responsibility for rendering and managing the visual game,GameBoard and GameBoardPresenter,provide a main entry function (SetupBoard() in GameBoard) and five continuation functions (also in GameBoard), that handles the initial setup of the gameboard prior to starting play. This logic implements  a mechanism that allows us to sequence a series of function calls without blocking the main UI thread, nor requiring another thread.  In order of their use, the specific functions in GameBoard are;

 

STEP

Function

Description

0

SetupBoard

Create the four walls, one in front of each player

1

formbox_continuation

Move walls towards center to form a closed box

2

open_continuation

Remove two tiles from wall to open the box, placing the two removed tiles on the wall

3

deal_continuation

Deal initial tiles to each player

4

fs_continuation

Remove any flowers and season tiles from the players hand and place them face up in front of them.

5

kong_continuation

Remove any Kongs (four of a kind) from the players hand and place them face up in front of them.

6

redeal_continuation

If the player holds less than 13 tiles, deal new tiles to bring their hand back to 13 tiles and restart removal of flowers and seasons (Step # 4). If the player needs no new tiles, the entire animation is complete.

Tiles and Animations

Each tile, or piece, is associated with an individual animation that moves that tile. Tiles are all instances of the Tile class and expose three key dependency properties used in animations;

 

  • Position            Sets the absolute position of a tile
  • Translate          Changes the position of a tile, relative to its current location
  • QRotation        Changes the absolute rotation of a tile by using a quaternion.

Every individual tile can be associated with a unique animation.  The purpose of any animation is to change the position and attitude of a tile smoothly between a starting and ending location and attitude.  This is done by manipulating these three dependency properties using a keyframed animation, where the initial, middle, and end positions are specified when the animation is defined, and then the standard WPF functionality is used to animate the motion of the piece.

If you aren’t familiar with dependency properties, simply look on them for the moment as standard properties that support late binding, i.e. they can be manipulated by code that doesn’t contain direct references to them.  Likewise, if you aren’t familiar with quaternions,  treat them as mathematical structures that define rotation, in the same way that a structure containing roll, pitch, and yaw would define rotation.  I will be blogging on both of these subjects in upcoming articles, in fact I will probably spend considerable time on quaternions.  And finally, if you aren’t familiar with keyframed animations, they are nothing more than an animation that passes through a series of locations and or attitudes you define (the keyframes) at specific points in time.  Keyframes make it possible for you to define animations that move along complex paths and are capable of changing velocity during the animation.  More information on keyframes in general is available here, and information on keyframes in WPF is available here.

 

Continuation Handlers

If you examine any of these routines, you will see a line such as this, taken from the SetupBoard() function.

 

if (i == _pieces.Length - 1)

t.ContinuationHandler += new TileTrajectory.TrajectoryContinuation(formbox_continuation);

 

This is binding a continuation to an individual tile; in this case it is testing to see if the current tile is the last in the list of tiles (_pieces) and if so, is binding the formbox_continuation() function to it.  If you examine SetupBoard() and the continuation functions, you will see they are connected together in a logical sequence by the continuation handlers, they do not make explicit calls from one function to the next.  As discussed in the previous posting, in order for a subsequent animation to start, the previous animation must have completed.  The reason for this is that, given an initial animation A and a subsequent animation B, the animation for B depends on the location animation A moved the piece to.  Therefore, it can’t start until animation A completes.

 

A key issue under the covers is the fact that any visual component, such as a Tile, can only be altered on the thread on which they were originally constructed.  Since the tiles interact with a number of other UI elements, it makes sense to create them on the main UI thread, but this does mean that they can only be altered on this thread.

 

What adds significant complexity here is that the animations themselves run on the UI thread, i.e. the animations will only work if the thread is idle.  If you programmatically start an animation and then immediately begin a time consuming operation on the UI thread, then the animations will not run until the operation completes.  If the operation were monitoring the position of an animated object, that object would never move..  As expected, such an action would also make the user interface non responsive; however the converse is not true.  If there are a number of animations running on the UI thread, the user interface will still respond, albeit sluggishly at times.

 

The issue presented is that we have animation stages which are dependent on the completion of preceding stages, and we have a situation where we must be careful not to lock up the main UI thread, so that the animations can actually run.  This is the central theme of this series of posts, as the problem is exactly the same as encountered in standard business data processing, where further steps in a business process are dependent on previous steps, which have unknown duration and results.  The concept of workflow programming was specifically developed to deal with exactly these kinds of issues.

 

One additional concept from enterprise architecture used here is the separation of concerns, specifically the separation of what is being worked on (an instance of a Mah-Jongg tile) from the work that is being done (an animation, represented by a TileTrajectory).  The information about how to perform the work is carried in the TileTrajectory, not in the tile itself, and the TileTrajectory contains a reference to the tile which it will affect.

 

The first step taken is to break down the entire sequence of animating the setup of a Mah-Jongg board into a series of atomic animations, where given that each atomic animation can examine the final results of a previous animation, it can then generate the exact subsequent animation.  Consider the simple act of pushing the four individual walls together to form the square.  Once the individual unconnected walls have been formed in front of each player, this is a very simple operation.  Prior to assembly of those walls, the operation is far more complex, since it is unknown which pieces will be in which wall.  Conversely, assembly of the initial walls is much simpler if the act of moving them to form a box doesn’t have to be addressed.  This is a central benefit to this approach, allowing the development of very specific task focused animations that can then be sequenced together into more complex animations.

How Continuation Handlers work

Each time an animation sequence completes, a Completed event for that animation is fired if a completion event handler has been attached to that animation.  The following code, from the GameBoard.xaml.cs file, handles attaching a completed event to any tile trajectory which has a defined continuation handler. (m is an instance variable containing an instance of TileTrajectory)

 

if (m.HasContinuation)

{

// attach a completion event handler

m.PositionPath.Name = String.Format("pp{0}",

_singleton._nameCounter++);

      _singleton._pendingCompletions[m.PositionPath.Name] = m;

m.PositionPath.Completed += new

                        EventHandler(PositionPath_Completed);

}

 

The boldface text shows the relevant logic.  Note that any tile being animated has two active animations, one driving changes in its position and one driving changes in its attitude.  It is certainly possible to combine these two animations into a single animation that affects both position and attitude, but the math can be significantly more complex and would muddy up the goals of these tutorials.  Readers familiar with matrix algebra should be able to integrate position and attitude with little effort, readers without such familiarity will benefit from keeping position and attitude manipulations separate.

 

When an individual position animation with an attached completion handler finishes the individual animation, the PositionPath_Completed function of TileTrajectory is automatically invoked.  That function has the following definition;

       

static void PositionPath_Completed(object sender, EventArgs e)

{

string key = ((AnimationClock) sender).Timeline.Name;

TileTrajectory tt =

_singleton._pendingCompletions[((AnimationClock) sender).Timeline.Name];

      _singleton._pendingCompletions.Remove(key);

      if (tt.HasContinuation)

tt.FollowContinuation();

}

 

There is a very important issue to be aware of with animations, namely that the animation you start with isn’t the one you end with.  Look at the logic used to attach the completion event to the animation, and note the lines of code that generate a name for the animation and then stash an instance reference to the associated TileTrajectory instance in a dictionary, keyed by the name. (m is an instance of TileTrajectory).

 

m.PositionPath.Name = String.Format("pp{0}",_singleton._nameCounter++);

_singleton._pendingCompletions[m.PositionPath.Name] = m;

 

Now note the complementary code in the completion event handler, where the name is recovered from the animation and used to locate the original animation instance.

 

TileTrajectory tt =

_singleton._pendingCompletions[((AnimationClock) ender).Timeline.Name];

 

The Timeline property is an animation equivalent to the original animation we started with, but it is not the same instance.  If it were, we could use the instance hashcode as a key, but since it isn’t, we need to set and use the name property to ensure that we match up the correct tile trajectory when the animation  completes.

 

Once this is done, we simply check to see if the TileTrajectory has a continuation bound to it, and if so, we transfer control to the continuation.

Conclusion

Consider the overall structure of the logical flow.  We are calling the SetupBoard() function in response to a click on the “New Game” button in the UI.  SetupBoard() in turn will set up the first animation step and link the next animation step (formbox_continuation) in through a continuation.  It will then trigger the animations and complete, allowing the UI thread idle time to actually begin running the animations.  In this particular case, once the last tile animation completes, the continuation handler for thast tile is triggered by the Completed event on the animation, and the next animation step starts.

In the next two blog entries I’ll discuss how continuations can be sequenced, and dig deeper into the details of the animation driver logic.

Published Tuesday, July 17, 2007 7:11 AM by MarkMMullin
Filed Under: , , ,

Comments

# Animation Article Overview @ Tuesday, July 17, 2007 4:25 AM

 
These articles illustrate a mechanism you can use to perform sophisticated animations using WPF...

Mark Mullin's Professional Blog