views:

53

answers:

2

When someone clicks in my flash activity, sometimes it triggers a lot of computation. If the user clicks again (e.g. a double click), this event gets queued and dispatched after the heavy computation is complete. If I clicked many times, the problem is just compounded -- if clicked fast enough, the queued heavy computation finishes up to ten seconds later, with each clickEvent slowly dribbling out the next task.

I have two questions.

First: how can I get the accurate time for when the click took place? In the example below, I am getting the dispatch of rapid click events long after the click was clacked (sp?).

Second: what is a good design pattern for collecting every click? Off the top of my head I suppose I should

  • defer any computation until the next EnterFrame event, but if someone clicks during the computation on the EnterFrame event... well then, I've got the same problem!

  • I suppose breaking down the heavy computation into a psuedo-thread is another solution, but depending on the speed of the processor, finding the granularity is difficult.

  • Adding a flag after the first click to disregard the next clicks... but this solution doesn't let me track what the user was trying to do while he was locked out. A solution to my first question is what I need here.

Thanks for any advice. Here is some sample code to demonstrate the issue:

package
{
    import flash.display.Sprite;
    import flash.display.StageAlign;
    import flash.display.StageScaleMode;
    import flash.events.MouseEvent;
    import flash.geom.Rectangle;

    public class clicky extends Sprite
    {
        private static var _lastTraceTime:Number = new Date().getTime();

        private var _sp:Sprite;
        private var _state1:Boolean;

        public function clicky( ):void
        {   super( );

            stage.align = StageAlign.TOP_LEFT;
            stage.scaleMode = StageScaleMode.NO_SCALE;

            _state1 = true;

            _sp = new Sprite( );
            addChild( _sp );
            _sp.graphics.beginFill( 0xFF00AA, 1 );
            _sp.graphics.drawRect( 10, 10, 100, 100 );
            _sp.graphics.endFill( );
            _sp.addEventListener(MouseEvent.MOUSE_DOWN, mDnCb, false, 0, true);
        }

        private function mDnCb( evt:MouseEvent ):void
        {   traceTime( "click" );
            _state1 = !_state1;
            var c:uint = 0xFF0000;
            if (_state1)
            {   c = 0x00FFAA;
            }
            paintThatRect( c );

            killTime( );
        }

        private function paintThatRect( c:uint ):void
        {
            _sp.graphics.beginFill( c, 1 );
            _sp.graphics.drawRect( 10, 10, 100, 100 );
            _sp.graphics.endFill( );
        }

        private function killTime( ):void
        {   var r:Rectangle = new Rectangle( 0, 0, 100, 100 );
            for (var i:uint = 0; i < 500000; i++)
            {
                var t:Rectangle = new Rectangle( i, i, i, i );
                if (t.intersects(r) || r.containsRect(t) || t.containsRect(r))
                {   r = t.union(r);
                }
            }
        }

        public static function traceTime( note:String ):Number
        {   var nowTime:Number = new Date().getTime();
            var diff:Number = (nowTime-_lastTraceTime);
            trace( "[t" + diff + "] " + note );
            _lastTraceTime = nowTime;
            return diff;
        }
    }
}
+1  A: 

One of my past projects involved rapid button pressing, so I did a survey to see how fast I could typically expect players to tap. The fastest clicker I could find couldn't get past 10 times per second.

The consequence of this is you can assume that the frame rate will always be faster than the click rate. A frame rate of 10 fps or less is unacceptable in almost all cases. Set your program up so any detected fire events are added to a queue. Each frame, process only one fire event from the queue.

sometimes it triggers a lot of computation.

Don't do that then.

If a process takes more than a tenth of a second to complete, it's simply impossible to execute it more than 10 times per second. (in AS3 at least) Any and all processing that you do must be designed so it will not critically delay the next frame. Not only will it look really choppy, but you'll begin to have starvation issues.

how can I get the accurate time for when the click took place?

By maintaining a decent frame rate.

Gunslinger47
Also, [MouseEvent.clickCount](http://www.adobe.com/livedocs/flash/9.0/ActionScriptLangRefV3/flash/events/MouseEvent.html#clickCount) could possibly help.
Gunslinger47
A: 

You will not get "accurate" timestamps for you click events if the player is busy running your killTime method. These events will not get processed timely (or rather, your handlers will be called) while your method is blocking the actionscript code execution.

The only way to do what you want (or what I think you are trying to do anyway) is breaking the heavy processing part into smaller pieces, sort of like green threads, as you suggested. There are a good number of examples on how to implement this if you google actionscript + green threads. Some add more structure to the problem, others are more simple minded, but they all boil down to the same basic idea. Do your processing in chunks, checking that you don't exceed some threshold; when / if you do, return from your function and wait to be called again to pick up from where you left. You can use a Timer or subscribe to EnterFrame for this.

Depending on your game, this might solve your problem or just move it somewhere else. If this point made by Gunslinger47 applies to your game, this approach won't really work:

If a process takes more than a tenth of a second to complete, it's simply impossible to execute it more than 10 times per second

However, here's a sketch of a possible implementation, assuming that's not the case. I removed some stuff from your sample code and added other. I think the code is easy to follow, but anyway I'll just explain a bit.

I'm using an inner class Context to keep track of progress, and I'm also using it to save the timestamp of the click that caused the process to run. You could use it to store other data related to each process. Each time the user clicks the button, one of these context objects is created and pushed onto the queue. If there's only one item in the queue (the one we just added), we start the process right away and set the timer / interval. If there's more stuff in the queue, it means there's a process running already, so we don't do anything for now.

Each time the timer mechanism executes, it picks up from where it left in the previous iteration. The previous state is stored in the first item of the queue. I'm storing a counter, but you could potentially need to save other data there. In this function I'm checking if the time threshold has been exceeded. This involves calling getTimer() which is more lightweight than using a Date object, but you'd probably don't want to call it for every iteration. You could instead check the time only every N loops, but that's up to you. Also, this max time is somewhat arbitraty. You should tune it up a bit, though 20 ms seems reasonable for a 20 FPS swf (assuming a theorical 50 ms per frame for code and rendering).

When the process finishes, the queue is shifted. Then we check if there are items left to be processed. If there are, we just let the thing run again. Otherwise, we stop it removing the EnterFrame.

Please note there's some overhead in using this "green threading" approach, as running the same code in just one go would be noticeably faster. So it's not perfect, but it's often the only way to keep your app usable while doing the processing.

package {


    import flash.display.Sprite;
    import flash.display.StageAlign;
    import flash.display.StageScaleMode;
    import flash.events.MouseEvent;
    import flash.geom.Rectangle;
    import flash.events.Event;
    import flash.utils.getTimer;

    public class clicky extends Sprite {

        private static var _lastTraceTime:Number = new Date().getTime();

        private var _sp:Sprite;

        private var _queue:Array;
        private const MAX_TIME:int = 20;

        public function clicky( ):void {
            super( );

            stage.align=StageAlign.TOP_LEFT;
            stage.scaleMode=StageScaleMode.NO_SCALE;

            _queue = [];

            _sp = new Sprite( );
            addChild( _sp );
            _sp.graphics.beginFill( 0xFF00AA, 1 );
            _sp.graphics.drawRect( 10, 10, 100, 100 );
            _sp.graphics.endFill( );
            _sp.addEventListener( MouseEvent.MOUSE_DOWN, mDnCb, false, 0, true );
        }

        private function mDnCb( evt:MouseEvent ):void {
            _queue.push(new Context(new Date()));
            if(_queue.length == 1) {
                initProcess();
            }
        }

        private function initProcess():void {
            trace("initProcess");
            killTime();
            addEventListener(Event.ENTER_FRAME,run);
        }

        private function processDone():void {
            trace("processDone, " + _queue[0].clickTime);
            _queue.shift();
            if(_queue.length == 0) {
                removeEventListener(Event.ENTER_FRAME,run);         
            }
        }

        private function run(e:Event):void {
            killTime();
        }

        private function paintThatRect( c:uint ):void {
            _sp.graphics.beginFill( c, 1 );
            _sp.graphics.drawRect( 10, 10, 100, 100 );
            _sp.graphics.endFill( );
        }

        private function killTime():void {
            var r:Rectangle=new Rectangle(0,0,100,100);
            var initTime:int = getTimer();
            var runningTime:int = 0;
            var loops:int = 500000;
            var ctx:Context = _queue[0];
            for(var i:int = ctx.i; i < loops; i++) {
                var t:Rectangle=new Rectangle(i,i,i,i);
                if (t.intersects(r)||r.containsRect(t)||t.containsRect(r)) {
                    r=t.union(r);
                }
                runningTime = getTimer() - initTime;
                if(runningTime >= MAX_TIME) {
                    break;
                }
            }
            ctx.i = i;
            if(i == loops) {
                trace(i);
                processDone();
            }

        }

    }
}

class Context {
    public var i:int = 0;
    public var clickTime:Date;

    public function Context(clickTime:Date) {
        this.clickTime = clickTime;
    }

    public function reset():void {
        i = 0;
    }
}
Juan Pablo Califano