Offstage event dispatching

My rendering engines rely on capturing events from display objects not on the display list. This enables a controller fully decoupled from the display list. Monitoring timing and managing lifecycle, there’s no root entry point or attachment to the display list.

Use case aside, one difference between OpenFL and Flash is that display objects off the display list don’t receive events.

For example, in Flash runtime you can create a display object and receive events even though it’s not on the display list:

ActionScript

package {
    import flash.display.Shape;
    import flash.display.Sprite;
    import flash.events.Event;

    public class Main extends Sprite {

        private var shape:Shape;

        public function Main() {
            super();

            shape = new Shape();
            shape.addEventListener(Event.ENTER_FRAME, frameHandler);
        }

        protected function frameHandler(event:Event):void {
            trace("frame handler");
        }
    }
}

Above, frame handler is called.

However, translated to Haxe below, the handler will not be called:

Haxe

package;
import openfl.display.Shape;
import openfl.display.Sprite;
import openfl.events.Event;

class Main extends Sprite {

    private var shape:Shape;

    public function new() {
        super();

        shape = new Shape();
        shape.addEventListener(Event.ENTER_FRAME, frameHandler);
    }

    private function frameHandler(event:Event):Void {
        trace("frame handler");
    }

}

Interesting, I wonder what order the Flash runtime uses for objects off the display list. Do they occur before, after, or at random, compared to those that are on the list?

My understanding was order listeners were added.

In this example, offstage sprite was added last, and events are handled last:

MAIN: Entering frame 2
Root node: 2571 - Enter Frame
	Child 1: 2571 - Enter Frame
		Child 2: 2571 - Enter Frame
Offstage child: 2571 - Enter Frame
Root node: 2574 - Frame Constructed
	Child 1: 2574 - Frame Constructed
		Child 2: 2574 - Frame Constructed
Offstage child: 2574 - Frame Constructed
Root node: 2574 - Exit Frame
	Child 1: 2574 - Exit Frame
		Child 2: 2574 - Exit Frame
Offstage child: 2574 - Exit Frame
Root node: 2574 - Render
	Child 1: 2574 - Render
		Child 2: 2574 - Render

In this example, offstage sprite was added first, and events are handled first:

MAIN: Entering frame 2
Root node: 2334 - Enter Frame
Offstage child: 2334 - Enter Frame
	Child 1: 2334 - Enter Frame
		Child 2: 2334 - Enter Frame
Root node: 2336 - Frame Constructed
Offstage child: 2336 - Frame Constructed
	Child 1: 2336 - Frame Constructed
		Child 2: 2336 - Frame Constructed
Root node: 2336 - Exit Frame
Offstage child: 2336 - Exit Frame
	Child 1: 2336 - Exit Frame
		Child 2: 2336 - Exit Frame
Root node: 2336 - Render
	Child 1: 2336 - Render
		Child 2: 2336 - Render

Example program to isolate behavior:

Main Application

package {
import flash.display.Sprite;
import flash.events.Event;

public class Main extends DisplayNotifier {

    protected var frameNumber:uint = 0;

    public function Main() {
        super("Root node", 0, true);

        stage.frameRate = 1;

        // add a child to the display list
        addChild(new DisplayNotifier("Child 1", 1, true));

        // add a child to that child
        (getChildAt(0) as Sprite).addChild(new DisplayNotifier("Child 2", 2, true));

        // add a child off the diplay list
        var offstage:Sprite = new DisplayNotifier("Offstage child", 0, false);
    }

    override protected function enterFrameHandler(event:Event):void {
        trace("MAIN: Entering frame " + ++frameNumber);
        super.enterFrameHandler(event);

        stage.invalidate();
    }

}
}

DisplayNotifier - Utility display object to trace events

package {
import flash.display.Sprite;
import flash.events.Event;
import flash.utils.getTimer;

public class DisplayNotifier extends Sprite {

    protected var displayName:String;
    protected var depth:uint = 0;
    protected var onStage:Boolean = false;

    public function DisplayNotifier(displayName:String, 
                                    depth:uint, 
                                    onStage:Boolean) {
        super();

        this.displayName = displayName;
        this.depth = depth;
        this.onStage = onStage;

        addEventListener(Event.ENTER_FRAME, enterFrameHandler);
        addEventListener(Event.FRAME_CONSTRUCTED, frameConstructedHandler);
        addEventListener(Event.EXIT_FRAME, exitFrameHandler);
        addEventListener(Event.RENDER, renderHandler);
    }

    protected function enterFrameHandler(event:Event):void {
        trace(toString() + " - Enter Frame");
    }

    protected function frameConstructedHandler(event:Event):void {
        trace(toString() + " - Frame Constructed");
    }

    protected function renderHandler(event:Event):void {
        trace(toString() + " - Render");
    }

    protected function exitFrameHandler(event:Event):void {
        trace(toString() + " - Exit Frame");
    }

    override public function toString():String {
        return (DisplayNotifier.indent(depth) + displayName + ": " + getTimer());
    }

    protected static function indent(depth:uint):String {
        var n:uint = depth;
        var s:String = "";
        while (n-- > 0) {
            s += '\t';
        }
        return s;
    }

}
}

Do clips follow instantiation order, always? I had always assumed that ENTER_FRAME followed the hierarchy somehow, but does it instead seem to register on creation, and de-register on destruction, and totally disregard the render order?

I’ve always believed listeners were just ordered sets, in which callbacks are appended. As in, when an event listener is added it just += the function callback handler.

To expand on my previous example, if I add nodes out of order by inserting children at ordinal 0:

var offstage1:Sprite = new DisplayNotifier("Offstage 1", 0, false);

addChild(new DisplayNotifier("Child 1", 1, true));
(getChildAt(0) as Sprite).addChild(new DisplayNotifier("Child 1-1", 2, true));
(getChildAt(0) as Sprite).addChild(new DisplayNotifier("Child 1-2", 2, true));
addChild(new DisplayNotifier("Child 2", 1, true));
addChild(new DisplayNotifier("Child 3", 1, true));
(getChildAt(2) as Sprite).addChild(new DisplayNotifier("Child 3-2", 2, true));
(getChildAt(2) as Sprite).addChild(new DisplayNotifier("Child 3-3", 2, true));
(getChildAt(2) as Sprite).addChild(new DisplayNotifier("Child 3-4", 2, true));
(getChildAt(2) as Sprite).addChildAt(new DisplayNotifier("Child 3-1", 2, true), 0);
addChildAt(new DisplayNotifier("Child 0", 1, true), 0);

var offstage2:Sprite = new DisplayNotifier("Offstage 2", 0, false);

Trace results show exact same order as insertion, not order of the display list:

MAIN: Entering frame 2

Root node: 2603 - Enter Frame
Offstage 1: 2604 - Enter Frame
	Child 1: 2604 - Enter Frame
		Child 1-1: 2604 - Enter Frame
		Child 1-2: 2604 - Enter Frame
	Child 2: 2604 - Enter Frame
	Child 3: 2604 - Enter Frame
		Child 3-2: 2604 - Enter Frame
		Child 3-3: 2604 - Enter Frame
		Child 3-4: 2604 - Enter Frame
		Child 3-1: 2604 - Enter Frame
	Child 0: 2604 - Enter Frame
Offstage 2: 2604 - Enter Frame

Root node: 2605 - Frame Constructed
Offstage 1: 2605 - Frame Constructed
	Child 1: 2605 - Frame Constructed
		Child 1-1: 2606 - Frame Constructed
		Child 1-2: 2606 - Frame Constructed
	Child 2: 2606 - Frame Constructed
	Child 3: 2606 - Frame Constructed
		Child 3-2: 2606 - Frame Constructed
		Child 3-3: 2606 - Frame Constructed
		Child 3-4: 2606 - Frame Constructed
		Child 3-1: 2606 - Frame Constructed
	Child 0: 2606 - Frame Constructed
Offstage 2: 2606 - Frame Constructed

Root node: 2606 - Exit Frame
Offstage 1: 2606 - Exit Frame
	Child 1: 2606 - Exit Frame
		Child 1-1: 2606 - Exit Frame
		Child 1-2: 2606 - Exit Frame
	Child 2: 2606 - Exit Frame
	Child 3: 2606 - Exit Frame
		Child 3-2: 2606 - Exit Frame
		Child 3-3: 2606 - Exit Frame
		Child 3-4: 2606 - Exit Frame
		Child 3-1: 2606 - Exit Frame
	Child 0: 2606 - Exit Frame
Offstage 2: 2606 - Exit Frame

Root node: 2606 - Render
	Child 1: 2606 - Render
		Child 1-1: 2606 - Render
		Child 1-2: 2606 - Render
	Child 2: 2606 - Render
	Child 3: 2606 - Render
		Child 3-2: 2606 - Render
		Child 3-3: 2606 - Render
		Child 3-4: 2606 - Render
		Child 3-1: 2606 - Render
	Child 0: 2606 - Render

Great, this is helpful. I think we probably should do some sort of internal “event pump” within display objects, which dispatch frame events themselves, if they have any listeners.

I forget, do you know if frame events bubble, or do they simply dispatch directly within DisplayObjectContainer classes? (would be great if they don’t bubble)

No - ENTER_FRAME, FRAME_CONSTRUCTED, EXIT_FRAME, and RENDER events have neither a “capture phase” nor a “bubble phase” - event listeners must be added directly to targets, whether the target is on the display list or not.

Great, I think an implementation is forming in my mind, I have this on the roadmap now http://www.openfl.org/documentation/contributors/roadmap

Ah, I remember the issue here. Multiple targets have no support weak references. How do you keep track of every instance, that is not on the display list, I order to pump events, without preventing them from being garbage collected? Herein lies the problem

We may do a workaround where we use the display list plus hard references to objects that subscribe to these events, but then the listener would have to be removed manually for cleanup

Good point - I’m not sure about weak references, or how targets handle reference counting / mark and sweep garbage collection.

Hard reference to the object as a requirement is perfectly fine, in fact preferred.

Really appreciate your time, insights, and adding offstage and lifecycle event additions to the roadmap! Once I get more up to speed on internals of OpenFL, I look forward to contributing to the project. Thanks!