views:

1629

answers:

2

Here's a really frustrating Actionscript 2 problem I have with movie clips associated with classes that are created on the timeline and need to be referenced in code right away:

-- I have a movie clip "C" in my library associated with a class "C".

-- Class "C" extends MovieClip.

-- I have a movie clip on the stage with frame labels "off" and "on". This movie clip has an instance name of "mc".

-- On the "on" frame has an instance of the class "C" with instance name "inst".

-- The constructor for class "C" includes a trace statement to output "C constructor!" to let me know when the instance on the stage is created.

Now let's say I run this code:

mc.gotoAndPlay("on");
var inst_mc:MovieClip = mc.inst;

if (inst_mc){
    trace("inst_mc found!");
}else{
    trace("inst_mc NOT FOUND!");
}

var inst_c:C = C(mc.inst);

if (inst_c){
    trace("inst_c found!");
}else{
    trace("inst_c NOT FOUND!");
}

It seems that the creation of any object under a class such as C will not happen until after all code for the current frame has finished executing, because the output will be this:

inst_mc found!
inst_c NOT FOUND!
C constructor!

What the heck is going on here? I've explicitly told the Flash authoring environment that movie clip C is associated with class C, and that class C is a MovieClip derivative. So in my code, the gotoAndPlay("on") will create the movie clip "inst" which is on the "on" frame. It is able to find the instance alright, but when I treat it as type C, it fails. And then the constructor happens AFTER all of this. How do I fix this? I would hope that once you change something on the timeline, the corresponding objects would be created immediately -- and they are, except not as their explicit class types. I can reference my instance, but only as a MovieClip. How the heck do I fix this? It should output:

C constructor!
inst_mc found!
inst_c found!

Thanks for any help!

* UPDATE * Thanks for the responses! It is unfortunate that there's no easy fix for my project, which is large now and can't be easily restructured (it is also too large to be converted to AS3). I thought about keeping the MCs on frame 1 and hiding them, but I figure that needlessly adds overhead. Even if _visible is set to false, isn't it still going to use up resources? (That's a different but related issue -- is performance any different if you have a complex, stationary MC that is not _visible versus not having it there at all?)

My current strategy goes something like this:

mc.gotoAndPlay("on");
var inst_mc:MovieClip = mc.inst;
var inst_c:C = C(mc.inst);

if (inst_c){
    // Even though I moved to the "on" frame,
    // the object was already initialized/existed already
    // so i can use its class code now
    inst_c.do_something_now();
}else{
    // The class is not accessible, so set a boolean flag
    // which will get dynamically assigned to the *movie clip*.
    // The constructor in class C will look to see if the flag
    // has already been set.  If so, it calls do_something_now()
    // within C's constructor.
    // In class C, trigger_do_something_now is a defined as a 
    // Boolean with no default value.
    // It is not set in the constructor.
    inst_mc.trigger_do_something_now = true;
}

This approach bothers me. It is messy and confusing. But, I think it is a reasonable workaround. What do you guys think? Thanks!

+2  A: 

Unfortunately this is how "object oriented" code in AS2 works. This is how you should visualize it: when the timeline playhead is on frame A, and you call gotoAndStop(B), the playhead is advanced immediately, and any necessary objects are created, but any timeline scripts you have on frame B are not yet executed, because Flash is still completing the execution of frame A. It's not until the next iteration of Flash's internal loop that frame B is processed.

That's how things had been ever since the days when classes were just a glint in the proverbial milkman's eye. And as you can guess, with class-attached clips everything works the same - when you move the playhead to frame B Flash creates everything on frame B, but it doesn't run any scripts that reside on frame B - and this includes the constructors of clips that were just created - until the following frame.

The easiest workaround is to rework your timeline so that the MC in question is created in frame 1, but kept hidden (or inactive, offstage, etc.) until the gotoAndStop command. You could do this by changing its alpha (etc.) in the IDE on the given frame, or by just calling an init method at the same time you do the gotoAndStop. The best workaround is to use AS3, if that's an option. There are other ways you can go, but I'd recommend one of those two.

update for edit: Yeah, one of the "other ways you can go" is to put parameters in the clip or its parent, and have the clip look for them when it initializes itself, but anyone who winds up maintaining your code won't thank you for it. But I've certainly done it. ;)

In answer to your side question, a clip with _visible=false incurs a very small overhead if it's within the bounds of the stage. It is not rendered, but there is a very small hit, which I think has to do with Flash calculating its bounds or somesuch. (I ran a test once, and you really need thousands of them for it to be measurable.) Even if this is performance hit is a concern, the fix is to move the invisible clips outside the bounds of the stage. Then they incur no processor hit whatsoever, that I've found, they just use script memory (and if you delay their initialization until they're ready to be used, very little memory at that).

Just remember that event handlers fire regardless of whether a clip is visible, so don't have your clip register for events until you initialize it. What I'd do is make an init() method with everything that belongs in the constructor, and call it where you would otherwise have constructed the clip.

fenomas
AS3 doesn't make this easier. In fact, I think it makes it harder to work with the timeline (for no good reason, IMO), because you can't even access the instance as a MovieClip (or Sprite or whatever) just after you issued a gotoAndPlay or a gotoAndStop. If you run the posted code in AS3, you'll get:inst_mc NOT FOUND!inst_c NOT FOUND!C constructor!This doesn't have to do with the object model of the language, but rather with how the flash runtime works.
Juan Pablo Califano
Juan, that's what AS3 *should* do, since Flash executes one frame at a time, and moving the playhead does not yet change which frame is being executed. Nobody ever said you can port from AS2 to AS3 without changing anything. But the questioner's issue is that AS2 is giving him an accessible class object that has not had its constructor called yet, which is very misleading in a platform aspiring to be OO. And such silliness doesn't arise in AS3.
fenomas
fenomas, I see your point. I've worked both with AS2 and AS3, and I'd choose AS3 any time. Not only the language, but the player's API is much nicer and cleaner (the display list, no more silent errors, etc, etc). However, I think AS3 makes simple timeline stuff more complicated. And even though I don't do much timeline stuff these days, I believe it has its place, and "downgrading" the player in that particular aspect is a mistake. If I'm not wrong, I think Adobe put that behavior back in the AVM2/FP10 or at least planned to do it (haven't checked it really, but I remember reading about it).
Juan Pablo Califano
PS: It seems that Adobe did put that behavior back in FP10. http://stackoverflow.com/questions/794750/in-flash-gotoandstop-and-nested-movieclip-issues
Juan Pablo Califano
Thanks guys for the thorough responses! I have updated the post with my current workaround. Let me know what you think. Unfortunately, moving this large project to AS3 is out of the question right now...
ZenBlender
A: 

First, let me apologize for focusing just on a side aspect of this issue (in my comments to fenomas' answer); though I already made my point and I even agree that object orientation, or at least, a formal class syntax was added to AS1 as an afterthought, called AS2 -- and at some points, it shows --, I neglected answering your actual question.

Re-reading this thread, I think it all really boils down to "waiting one frame". That's a workaround I had to use many many times in AS2, so it seems odd I neglected to see that was the real problem here.

Anyway, your workaround will probably work, but as you say, it can become a maintenance nightmare really quick. Fenomas' option is another valid workaround. In this case, I wouldn't go that way if I could avoid it, though, but not because of performance; rather, because re arranging a lot of stuff when you already have lay out things could be a lot of work.

So, perhaps you could try something really simple like:

mc.gotoAndPlay("on");
this.onEnterFrame = function():Void {
    trace(mc.inst instanceof C);
    delete this.onEnterFrame;
};

And I think it should work. The enterFrame event will be caught by your handler one frame after you register to it. At that point, you now the frame you've moved to is ready, so you just have to clean up the handler and do what you'd normally do just after issuing the gotoAndPlay.

I remember I once had to do something very similar (it was not exactly the same scenario, but it boiled down to waiting one frame), so at the time I wrote a very simple class to centralize this code. It was something along these lines:

class FrameDelay {

    function FrameDelay(scope:Object,callback:Function,args:Array) {
            // get a reference to the current enterFrame handler
            // (if any), so we can restore it back when we're done   
     var oldEnterFrameHandler:Function = _root.onEnterFrame;

 _root.onEnterFrame = function():Void {
  oldEnterFrameHandler();
  callback.apply(scope,args);
  _root.onEnterFrame = oldEnterFrameHandler;
 }
    }

}

And you'd use it like this:

mc.gotoAndPlay("on");

new FrameDelay(this,onFrameReady,["a","1"]);

function onFrameReady():Void {
    // arguments (if any), will be available in the arguments array 
    trace(arguments.length);
    trace(mc.inst instanceof C);
    trace(this); 

}

You pass a scope, a function and optionally an array of arguments. Most likely, you will not need them, and it could be also possible to just have the class constructor take a callback. But in some circumstances, you could have scope problems in your callback (this is an AS2 issue), so passing the scope explicitly is safer.

Also, note that I'm using _root.onEnterFrame. Though I'm making a "backup" of the original handler and restoring it back when I'm done, that doesn't necessarily mean that other parts of the code are that polite (!). So, the handler could be overwritten. If you think that could be a problem, maybe you can dinamically create a movieClip in the root at some depth you know it's not used, and replace _root.onEnterFrame with _root.dummy_mc.onEnterFrame.

Anyway, keep in mind that a simple inline onEnterFrame will do the job, so maybe you don't even need using a class for this.

Juan Pablo Califano