Tuesday, 13 July 2010

Using actionscript 3 to inject code into the timeline

Two undocumented handy features from AS3 for working with the MovieClip timeline:

frame1();

Which runs the code on frame 1 of the timeline, with corresponding functions for any frame that contains code - frame99(), frame354() etc.

addFrameScript(frameNumber:int, newFrameActions:Function):void

Which adds the code specified in newFrameActions at the frameNumber. Note that this is a zero-based frame number, yet frame1() is one-based. To add code to the first frame of an MC and then call it, you would do:

mc.addFrameScript(0, firstFrameActions);
mc.frame1();

addFrameScript will overwrite any existing actions, so if you want to augment and not just replace the code that is there, you'll need to first grab the existing frame code and then include that in your new function like this:
var oldFrameActions:Function = function():void {};

if((mc['frame23'] != null) && (mc['frame23'] is Function))
{
oldFrameActions = mc['frame23'];
}

var newFrameActionsFrame23:Function = function():void {
oldFrameActions();
// add new code here...
}
Of course all of that is much better wrapped up in some portability that doesn't care whether it's frame 23 or frame 99 etc. I'm using a class for that - http://gist.github.com/474159

You feed it a MovieClip and you'd probably want to hold a reference inside that MovieClip as well because one of the other uses is in overriding gotoAndPlay() so that it will rerun the code in the current frame without having to move the playhead (assuming frameScriptManager is a reference to an instance of the class in the gist):

override public function gotoAndPlay(frame:Object, scene:String = null):void
{
var frameNo:int = this.currentFrame;

super.gotoAndPlay(frame, scene);

if(this.currentFrame == frameNo)
{
var frameFunction:Function = frameScriptManager.getFrameScriptAtFrame(frameNo);
try {
frameFunction();
}
catch(e:Error)
{
trace(e.getStackTrace());
}

}

}

The class, and in particular the getFrameScriptAtFrame() function also deals with the major gotcha: even once you've added additional code to the timeline at frame 1, the frame1() function continues to only run the actions that were present in the MovieClip (or Fla) timeline when it was published.

An alternative to rerun the current frame actions is to call Stage.invalidate() before the gotoAndPlay() - which is OK unless your content, like ours, is loaded into a sandbox that doesn't have access to Stage.

--- ADDENDUM ---

I have now also discovered that the visibility of the frame1() function is internal - so unless the FrameScriptManager is compiled into the same package as the SWF / MovieClip base class, these will show up as undefined (annoyingly no more helpful error is thrown).

So - the workaround is that your MC to be injected should implement the following function:
    public function getFrameScriptAt(frameNumber:int):Function
{
return this['frame'+frameNumber];
}

This is also specified in the IInjectableTimeline interface I've added to the Gist file. However, the FrameScriptManager class itself doesn't require an IInjectableTimeline, but will accept a vanilla MovieClip because the complications of ApplicationDomains for content loaded at runtime, plus variation in where the FrameScriptManager is complied (in the parent swf or in the child swf), make it tricky to roll a one-size-fits-all solution with strong typing.

So - the interface is there to help you keep yourself honest. Unfortunately there's no compiler enforcement in this case.

--------


What's the point of all this? Well, in our case the swfs being targeted for timeline code injection are animated lessons. These lessons also need some code in them to address the application they run in - so that they can ask the main message window to show a title, or offer a download, or some instructions, or set the status of the play / pause button at the end of a section. Previously we did this using frame code, but this was hard to test and maintain. Instead we now specify most titles, instructions, download paths etc in an external xml file which is quick to edit.

At runtime the xml file for each lesson is processed and the relevant actions are added to the timeline of the lesson swfs, meaning that as far as the application is concerned these new, easier to manage (for us and our client) lessons behave exactly like the old ones.


1 comment:

Jack said...

Thanks for the info! That was exactly what I needed. As I only needed a subset of the functionality, I made a simplified version of FrameScriptManager.

package
{
import flash.display.MovieClip;

public class MCScripter
{
public static function addScriptAtFrame(mc:MovieClip, frame:Number, newFrameActions:Function):void
{
var oldFrameActions:Function = function():void {};
if((mc['frame'+frame] != null) && (mc['frame'+frame] is Function))
{
oldFrameActions = mc['frame'+frame];
}
mc.addFrameScript(frame, function():void {
oldFrameActions();
newFrameActions();
});
}
}
}

Interestingly, this one works fine for me but I couldn't get FrameScriptManager to retain the old actions. I didn't dive into it deep enough to figure out what the issue was but this meets my needs so it's all good.