タワーディフェンスタイプのゲームを作ってみようと思う 3日目

敵軍ユニットを進軍させる準備ができた。
早速実装に入りたいところだが、まずその前に、前回のコードに重複している部分があるのでリファクタリングする事にする。地形ビュー(GraphView)と地形ビューモデル(GraphMapper)がノードの座標を決定している部分が重複個所だ。

GraphView.as

        private function createNodeView(node : GraphNode) : void
        {
            var nodeView : NodeView = addChild(NodeView.getView(node.id)) as NodeView;
            nodeView.x = node.depth * 100 + 60;
            nodeView.y = node.childIndex * 60;
        }

GraphMapper.as

public function GraphMapper(model : DirectedGraph)
{
    this.model = model;
            
    // とりあえずノードの座標は簡易ビューのノード座標をハードコーディングする.
    // あとでノードの座標を指定するインターフェイスを考える
    model.crawl(function(node : GraphNode) : void
    {
        position[node] = Vector.([node.depth * 100 + 60, node.childIndex * 60]);
    });
}

 
地形ビューモデルの当該個所をメソッドとして抽出し、地形ビューはこのメソッドを利用する。

GraphMapper.as

public function GraphMapper(model : DirectedGraph)
{
    this.model = model;
        
    model.crawl(decideNodePosition);
}

// グラフノードの空間上の位置を返す
public function decideNodePosition(node : GraphNode) : Vector.
{
    // とりあえずノードの座標は簡易ビューのノード座標をハードコーディングする.
    // あとでノードの座標を指定するインターフェイスを考える
    return position[node] = Vector.([node.depth * 100 + 60, node.childIndex * 60]);
}

GraphView.as

private function createNodeView(node : GraphNode) : void
{
    var nodeView : NodeView = addChild(NodeView.getView(node.id)) as NodeView;
    var pos:Vector. = mapper.decideNodePosition(node);
    nodeView.x = pos[0];
    nodeView.y = pos[1];
}

 
このdecideNodePositionメソッドは将来抽象メソッドになるだろう。そうすればGraphMapperのサブクラスはこのメソッドを実装し自由にノードを空間に配置できるようになる。
いまのところ地形ビューは直接地形モデル・ビューモデルを参照している。本来ビューはモデル・ビューモデルを参照するべきではないが、後で設計に手を加える予定なので今の時点ではこれでよしとする。ついでに地形の形を縦長に変更した。

実行結果
ビューのレイアウトを少し変えた

では敵軍ユニットを進軍させる事を考える。敵軍ユニットはノード0からスタートし、ノード5に達する。道程に分岐があるが敵軍ユニットによってどちらに進むかは変わる。ノードには進軍コストが設定されているのでコストに応じて敵軍ユニットが移動するスピードも変わる。

さて、敵軍ユニットビューが進軍するためには進軍ルートのゲーム画面上の座標を知っていなければならない。敵軍ユニットビューはどのようにしてそれを知るのだろう?いや、敵軍ユニットビューはその事に関知してはいけない。知るためにはモデル・ビューモデル(以下モデル層)にアクセスしなければならないからだ。

地形モデルの空間表現をビューに持たせる事を避けるためにビューモデルを導入した。ここでも同じように、敵軍ユニットを空間上どこに配置するかを知っているオブジェクトをビューモデル層に置こう。

Marchクラスを作ることにする。このオブジェクトは敵軍ユニットが現在グラフ上のどのノードからどのノードへ移動しているのかを知っている。またGraphMapperにアクセスし敵軍ユニットの座標を取得する。さらにその座標へ敵軍ユニットビューを移動させる。

設計はこんな感じ。
(GraphViewクラスはTerrainクラスと名前変更した。)

以下、ソースコード

EnemyUnit.as

package com.imajuk.tdf
{
    import flash.display.Graphics;
    import flash.display.Sprite;

    /**
     * @author imajuk
     */
    public class EnemyUnit extends Sprite
    {
        public var speed : Number;
        private var color : uint;

        public function EnemyUnit(speed : Number = 1.0, color : uint = 0)
        {
            this.color = color;
            this.speed = speed;
            
            draw();
        }

        private function draw() : void
        {
            //とりあえず正方形を描画.いまはテストできればいい
            var g:Graphics = graphics;
            g.beginFill(color, .5);
            g.drawRect(-15, -15, 30, 30);
            g.endFill();
        }
    }
}

March.as

package com.imajuk.tdf
{
    import flash.utils.Dictionary;

    /**
     * @author imajuk
     */
    public class March
    {
        private var mapper : GraphMapper;
        private var enemyUnits : Vector. = new Vector.();
        private var unitFrom : Dictionary = new Dictionary(true);
        private var unitTo   : Dictionary = new Dictionary(true);
        private var unitTime : Dictionary = new Dictionary(true);

        public function March(mapper : GraphMapper)
        {
            this.mapper = mapper;
        }

        public function addEnemyUnit(enemy : EnemyUnit) : void
        {
            enemyUnits.push(enemy);
            unitTime[enemy] = 0.0;
            resetEnemyFrom(enemy);
        }
        
        public function update() : void
        {
            var enemy : EnemyUnit,  // 敵軍ユニット
                from  : GraphNode,  // 敵軍ユニットの所属ノード
                to    : GraphNode,  // 敵軍ユニットの行き先ノード
                time  : Number,     // 敵軍ユニットの進軍時間
                i : int , l : int = enemyUnits.length;
                
            for (i = 0; i < l; i++)
            {
                enemy = enemyUnits[i];
                from  = unitFrom[enemy];
                to    = unitTo[enemy];
                time  = unitTime[enemy];

                // まだ敵軍ユニットの行き先が決まってなければ決定する
                if (!to) 
                    to = decideDestination(enemy, from);
                    
                // 敵軍ユニットの座標を取得してビューの位置を変更
                var position:Vector. = mapper.getPosition(from, to, time);
                enemy.x = position[0];
                enemy.y = position[1];
                
                // 敵軍ユニットが行き先ノードに達したら所属ノードを更新
                if (time >= 1.0)
                {
                    updateEnemyFrom(enemy, to);
                    unitTime[enemy] = 0.0;
                }
                unitTime[enemy] += .05 / to.cost * enemy.speed;
            }
        }

        private function resetEnemyFrom(enemy : EnemyUnit) : void
        {
            updateEnemyFrom(enemy, mapper.model.begin);
        }

        private function updateEnemyFrom(enemy : EnemyUnit, from : GraphNode) : void
        {
            unitFrom[enemy] = from;
            unitTo[enemy] = null;
        }

        private function decideDestination(enemy : EnemyUnit, node : GraphNode) : GraphNode
        {
            var prospectiveDestinations : Array = node.to,
            posibility : int = prospectiveDestinations.length;
            
            switch (posibility)
            {
                // 敵軍ユニットが終点に達した
                case 0:
                    // ゲームオーバーなど何らかの処理が必要になるが、いまはとりあえず始点に戻す
                    resetEnemyFrom(enemy);
                    return decideDestination(enemy, unitFrom[enemy]);
                    break;
                // 進路に分岐なし
                case 1:
                    return unitTo[enemy] = prospectiveDestinations[0];
                    break;
                // 進路に複数の分岐
                default :
                    // まだ分岐した進路を決定する仕組みを考えてないので複数進攻候補がある場合にはランダムに決定する
                    return unitTo[enemy] = prospectiveDestinations[int(Math.random() * posibility)];
            }
        }
    }
}

 
テストとして、敵軍ユニットを2つ進軍させてみる。1つはスピード1、カラー青。もう1つはスピード1.5、カラー赤。

Main.as

package com.imajuk.tdf
{
    import flash.display.Sprite;
    import flash.events.Event;

    public class Main extends Sprite
    {
        private var model : DirectedGraph;

        public function Main()
        {
            // テスト用の地形作成
            model = new DirectedGraph();
            // 敵軍進軍始点(ノードA)
            var node_A : GraphNode = model.begin;
            // ノードAにノードBを追加
            var node_B : GraphNode = node_A.add(2);
            // ノードBにノードCを追加
            var node_C : GraphNode = node_B.add(5);
            // ノードBにノードDを追加
            var node_D : GraphNode = node_B.add(2);
            // ノードCにノードEを追加
            var node_E : GraphNode = node_C.add(8);
            // ノードEに敵軍終点(ノードF)を追加
            var node_F : GraphNode = node_E.add(2);
            // ノードDとノードFを連結
            node_D.connect(node_F);
            
            // グラフを空間にマップするオブジェクト
            var mapper:GraphMapper = new GraphMapper(model);

            // テスト用の簡易ビュー
            var view:Sprite = addChild(new Terrain(model, mapper)) as Sprite;
            
            // 敵軍ユニット(スピード1、カラー青)
            var enemy1:EnemyUnit = view.addChild(new EnemyUnit(1.0, 0x0000ff)) as EnemyUnit;
            // 敵軍ユニット(スピード1.5、カラー赤)
            var enemy2:EnemyUnit = view.addChild(new EnemyUnit(1.5, 0xff6666)) as EnemyUnit;
            
            // 進軍クラス
            var march:March = new March(mapper);
            march.addEnemyUnit(enemy1);
            march.addEnemyUnit(enemy2);
            addEventListener(Event.ENTER_FRAME, function() : void
            {
                march.update();
            });
        }
    }
}

 
実行結果
進軍コストを分かりやすくするためコストにメリハリを付けた。また、線の太さで進軍コストを表現している。
Sorry, either Adobe flash is not installed or you do not have it enabled

今日はここまで!リビジョンは3eef1d8a3b
三日目到達。つづく〜

カテゴリー: ActionScript3, towerDefense | 2件のコメント

タワーディフェンスタイプのゲームを作ってみようと思う 2日目

さて論理的な地形が出来た。この地形に敵軍ユニットを配置したいのだがゲーム画面上のどこにユニットを配置すればいいのだろうか?と、ここで地形の空間表現がまだ出来ていない事に気づく。

地形の空間表現のロジックはどこに記述するべきだろうか?今ある地形モデルだろうか、それとも地形ビューだろうか?
言うまでもなく地形の空間表現はビューに密接に関係する。MVCの原則からモデルはビューに関心を持つべきではない。従って地形の空間表現はビューに記述する事になる。しかしこれはなんだか嫌な感じがする。この考え方では敵軍ユニットビューが自身の配置される座標を決定する事になるからだ。これは出来れば避けたい。ビューはなるべく知性を持たない置物のようなオブジェクトであるべきだ。

そこで、以下の用にビューモデル層を導入する事にした。各レイヤーの責務は図に記述してある。

地形のアーキテクチャ

GraphMapperというクラスをビューモデル層に作り、このクラスがグラフの空間表現を行う事にする。

地形マッパーのクラス図

テストとして前回作った簡易地形ビューを使い各ノード間の経路をポイントしてみた。

以下ソースコード
GraphMapper.as

package com.imajuk.tdf
{
    import flash.utils.Dictionary;
    /**
     * @author imajuk
     */
    public class GraphMapper
    {
        private var model : DirectedGraph;
        private var position : Dictionary = new Dictionary(true);

        public function GraphMapper(model : DirectedGraph)
        {
            this.model = model;
            
            // とりあえずノードの座標は簡易ビューのノード座標をハードコーディングする.
            // あとでノードの座標を指定するインターフェイスを考える
            model.crawl(function(node : GraphNode) : void
            {
                position[node] = Vector.([node.depth * 100 + 60, node.childIndex * 60]);
            });
        }

        public function getPosition(begin : GraphNode, end : GraphNode, time : Number) : Vector.
        {
            var v1 : Vector. = position[begin],
                v2 : Vector. = position[end],
                bx : Number = v1[0], by : Number = v1[1],
                ex : Number = v2[0], ey : Number = v2[1];
                
            return Vector.([bx + (ex - bx) * time, by + (ey - by) * time]);
        }
    }
}

Main.as

package com.imajuk.tdf
{
    import flash.display.DisplayObject;
    import flash.display.Sprite;

    public class Main extends Sprite
    {
        private var terrin : DirectedGraph;

        public function Main()
        {
            // テスト用の地形作成
            terrin = new DirectedGraph();
            // 敵軍進軍始点(ノードA)
            var node_A : GraphNode = terrin.begin;
            // ノードAにノードBを追加
            var node_B : GraphNode = node_A.add(3);
            // ノードBにノードCを追加
            var node_C : GraphNode = node_B.add(5);
            // ノードBにノードDを追加
            var node_D : GraphNode = node_B.add(5);
            // ノードCにノードEを追加
            var node_E : GraphNode = node_C.add(5);
            // ノードEに敵軍終点(ノードF)を追加
            var node_F : GraphNode = node_E.add(5);
            // ノードDとノードFを連結
            node_D.connect(node_F);
            
            // テスト用の簡易ビュー
            var view:Sprite = addChild(new GraphView(terrin)) as Sprite;

            // グラフを空間にマップするオブジェクト
            var mapper:GraphMapper = new GraphMapper(terrin);
            
            // ビューの各ノード間の経路をポイント
            var tests:Array = [
                [node_A, node_B,  0], //ノードAからノードBまでの経路上で0%の位置
                [node_B, node_C, .2], //ノードBからノードCまでの経路上で20%の位置
                [node_B, node_D, .4], //ノードBからノードDまでの経路上で40%の位置
                [node_C, node_E, .6], //ノードCからノードEまでの経路上で60%の位置
                [node_D, node_F, .8], //ノードDからノードFまでの経路上で80%の位置
                [node_E, node_F,  1]  //ノードEからノードFまでの経路上で100%の位置
            ];
            tests.forEach(function(a:Array, ...rest) : void
            {
                var v : Vector. = mapper.getPosition(a[0], a[1], a[2]);
                var p : DisplayObject = view.addChild(new TestPoint());
                p.x = v[0];
                p.y = v[1];
            });
        }
    }
}
import flash.display.Shape;

class TestPoint extends Shape
{
    public function TestPoint() {
        graphics.beginFill(0x0000FF, .5);
        graphics.drawCircle(0, 0, 30);
        graphics.endFill();
    }
}

実行結果
経路にポイントをマップ

これで敵軍ユニットを地形に配置する事が出来そうだ。
今日はここまで! 
リビジョンは601fe26dd3

たぶん つづく。

カテゴリー: ActionScript3, towerDefense | コメントする

タワーディフェンスタイプのゲームを作ってみようと思う 1日目

このblog、3年以上更新されてなかったんですね。僕は生きてます。

あまりに暇なのでタワーディフェンスタイプのゲームを作ってみようと思う。制作状況はこのblogにアップし、都度ソースコードをgithub(https://github.com/imajuk/tdf)にコミットする。
実行環境はスマートフォン。Adobe AIRでのapp制作を試みる。
完成時にはタワーディフェンスタイプのゲームが作成できるフレームワークが構築されていることを目標とする。
グラフィックやサウンドをどうするかは未定。
モチベーションを保つためのblogでの公開だが完成するかどうかは分からない。

タワーディフェンスのルールの概略

  • リアルタイムで攻めてくる敵軍にたいして自軍ユニットを配置し自軍本拠地にあるタワーを守る
  • タワーが敵軍に占拠されるとゲームオーバー
  • 何らかの方法で資源を獲得する事が出来る。
  • 自軍ユニットの配置には資源を消費する。一般的に高い性能を持つユニットほど多くの資源を消費する。

地形の設計

 
まずは地形の設計から開始する事にする。
地形は柔軟性のある設計にしたい。複雑な地形を従軍してくる敵軍、沼地では速度が遅くなり平野では速度が増す、などなどに対応できるモデルにしたい。
地形のモデルとして有向グラフを採用する事にする。グラフは既存のライブラリは使わずスクラッチする。
以下の図の用に、グラフのエッジにコストを持たせる事で進軍コストを表現する。

地形のモデル

グラフの設計は以下の様にした。最小のプロパティとメソッドから始める。必要に応じて機能を追加してゆく。

グラフのクラス図

以下ソースコード

DirectedGraph.as

package com.imajuk.tdf
{
    /**
     * @author imajuk
     */
    public class DirectedGraph
    {
        private var _begin : GraphNode;
        public function get begin() : GraphNode
        {
            return _begin;
        }
        
        public function DirectedGraph()
        {
            _begin = new GraphNode();
        }        
    }
}

DirectedGraph.as

package com.imajuk.tdf
{
    /**
     * @author imajuk
     */
    public class GraphNode
    {
        private var _cost : Number;
        public function get cost() : Number
        {
            return _cost;
        }
        
        private var _to : Array = [];
        public function get to() : Array
        {
            return _to;
        }
        
        private var _from : Array = [];
        public function get from() : Array
        {
            return _from;
        }
        
        public function GraphNode(cost : Number = 0)
        {
            _cost = cost;
        }
        
        public function toString() : String
        {
            return "GraphNode[" + _id + "]";
        }

        public function add(cost:Number) : GraphNode
        {
            return connect(new GraphNode(cost));
        }

        public function connect(node : GraphNode) : GraphNode
        {
            _to.push(node);
            node._from.push(this);
            return node;
        }
    }
}

Main.as

package com.imajuk.tdf
{
    import flash.display.Sprite;

    public class Main extends Sprite
    {
        private var terrin : DirectedGraph;

        public function Main()
        {
            // テスト用の地形作成
            terrin = new DirectedGraph();
            // 敵軍進軍始点(ノードA)
            var node_A : GraphNode = terrin.begin;
            // ノードAにノードBを追加
            var node_B : GraphNode = node_A.add(3);
            // ノードBにノードCを追加
            var node_C : GraphNode = node_B.add(5);
            // ノードBにノードDを追加
            var node_D : GraphNode = node_B.add(5);
            // ノードCにノードEを追加
            var node_E : GraphNode = node_C.add(5);
            // ノードEに敵軍終点(ノードF)を追加
            var node_F : GraphNode = node_E.add(3);
            // ノードDとノードFを連結
            node_D.connect(node_F);
        }
    }
}

上記のメインクラスの様にコードでグラフを作るのは大変めんどくさいので将来的には地形作成ツールなども必要になるだろう。頭の片隅においておく事にする。

さて、地形のモデルが出来たのでビューを作る。
この時点ではモデルの確認用とし、ゲームとしてのグラフィックは考えない。必要最低限のものにとどめる。
ビューの作成のため、モデルにノードを始点から終点まで一度ずつ巡回するメソッドを追加した。またデバッグのためノードにユニークなIDをもたせた。さらにビュー上でのノードの位置を決定するためモデルにノードの深度とノードの兄弟関係に置けるインデックスを追加した。

グラフのクラス図2

以下ソースコード

DirectedGraph.as

package com.imajuk.tdf
{
    /**
     * @author imajuk
     */
    public class DirectedGraph
    {
        private var _begin : GraphNode;
        public function get begin() : GraphNode
        {
            return _begin;
        }
        
        public function DirectedGraph()
        {
            _begin = new GraphNode();
        }
        
        /**
         * 始点から終点に達するまでノードを巡回する。
         * 各ノードを引数に一度だけ与えられた関数を実行する。
         */
        public function crawl(f:Function):void
        {
            _crowl(_begin, f);
        }
        private function _crowl(node:GraphNode, f:Function) : void
        {
            f(node);
            
            if(node.hasNext)
            {
                var nexts : Array = node.to,
                    i : int = 0, 
                    l : int = nexts.length;
                for (i; i < l; i++)
                {
                    _crowl(nexts[i], f);
                }
            }
        }
        
    }
}

GraphNode.as

package com.imajuk.tdf
{
    /**
     * @author imajuk
     */
    public class GraphNode
    {
        private static var sid : int;
        private var _id : int;
        public function get id() : int
        {
            return _id;
        }
        
        private var _depth : int;
        public function get depth() : int
        {
            return _depth;
        }

        private var _childIndex : uint;
        public function get childIndex() : uint
        {
            return _childIndex;
        }

        private var _cost : Number;
        public function get cost() : Number
        {
            return _cost;
        }
        
        private var _to : Array = [];
        public function get to() : Array
        {
            return _to;
        }
        
        private var _from : Array = [];
        public function get from() : Array
        {
            return _from;
        }
        
        public function GraphNode(cost : Number = 0)
        {
            _cost = cost;
            _id = sid++;
        }
        
        public function toString() : String
        {
            return "GraphNode[" + _id + "] / depth:" + _depth;
        }

        public function add(cost:Number) : GraphNode
        {
            return connect(new GraphNode(cost));
        }

        public function connect(node : GraphNode) : GraphNode
        {
            node._childIndex = _to.length;
            _to.push(node);
            node._from.push(this);
            node._depth = Math.max(node._depth, _depth + 1);
            return node;
        }

        public function get hasNext() : Boolean
        {
            return _to.length > 0;
        }
    }
}

GraphView.as

package com.imajuk.tdf
{
    import flash.display.Graphics;
    import flash.display.Sprite;

    /**
     * @author imajuk
     */
    public class GraphView extends Sprite
    {
        public function GraphView(graph:DirectedGraph)
        {
            graph.crawl(createNodeView);
            graph.crawl(drawEdge);
        }

        private function createNodeView(begin : GraphNode) : void
        {
            var nodeView : NodeView = addChild(NodeView.getView(begin.id)) as NodeView;
            nodeView.x = begin.depth * 30 + 30;
            nodeView.y = begin.childIndex * 30;
        }
        
        public function drawEdge(begin : GraphNode) : void
        {
            var beginNode : NodeView = NodeView.getView(begin.id);
            var g : Graphics = graphics;
            g.lineStyle(0, 0);
            
            begin.to.forEach(function(next:GraphNode, ...rest) : void
            {
                var nextNode : NodeView = NodeView.getView(next.id);
                g.moveTo(beginNode.x, beginNode.y);
                g.lineTo(nextNode.x, nextNode.y);
            });
        }
    }
    
}
import flash.text.TextFieldAutoSize;
import flash.text.TextFormatAlign;
import flash.text.TextFormat;
import flash.display.Sprite;
import flash.text.TextField;
import flash.display.Graphics;

class NodeView extends Sprite
{
    private static var nodes : Array = [];
    private static var g : Graphics;

    public function NodeView(id : int)
    {
        g = graphics;
        g.clear();
        g.beginFill(0);
        g.drawCircle(0, 0, 7);
        g.endFill();
        
        var tf : TextField = addChild(new TextField()) as TextField,
            tfm : TextFormat = new TextFormat();
            
        tfm.size = 8;
        tfm.color = 0xFFFFFF;
        tfm.align = TextFormatAlign.CENTER;
        
        tf.text = id.toString();
        tf.setTextFormat(tfm);
        tf.autoSize = TextFieldAutoSize.CENTER;
        tf.x = -tf.width * .5;
        tf.y = -tf.height * .5;
    }

    public static function getView(id : int) : NodeView
    {
        if (!nodes[id])
            nodes[id] = new NodeView(id);
        return nodes[id];
    }   
}

Main.as

package com.imajuk.tdf
{
    import flash.display.Sprite;

    public class Main extends Sprite
    {
        private var terrin : DirectedGraph;

        public function Main()
        {
            // テスト用の地形作成
            terrin = new DirectedGraph();
            // 敵軍進軍始点(ノードA)
            var node_A : GraphNode = terrin.begin;
            // ノードAにノードBを追加
            var node_B : GraphNode = node_A.add(3);
            // ノードBにノードCを追加
            var node_C : GraphNode = node_B.add(5);
            // ノードBにノードDを追加
            var node_D : GraphNode = node_B.add(5);
            // ノードCにノードEを追加
            var node_E : GraphNode = node_C.add(5);
            // ノードEに敵軍終点(ノードF)を追加
            var node_F : GraphNode = node_E.add(3);
            // ノードDとノードFを連結
            node_D.connect(node_F);
            
            addChild(new GraphView(terrin));
        }
    }
}

実行結果
最初のビュー

今日はここまで。
リビジョンは83c72eb4cc

つづく。。かな?

カテゴリー: ActionScript3, towerDefense | コメントする

Threadの割り込みハンドラが失敗することがある(その3)

この問題は先日のアップデートでパッチがあたりました。

最新のtrunkのThreadではこの問題は起きないようです。


前回の続きです。。

前回のパッチをSparkProjectにコミットしたところ、nobuさんから割り込み後もイベントを受け取ってしまうという問題を指摘してもらえました。この事についてちょっと考えてみます。

おそらくThreadの仕様的には割り込み後の振る舞いというのはThreadのサブクラスにまかされていて、スーパークラスであるThreadでは特に定義されていないんだと思います。ところがこれまで見てきたように、割り込みとイベント配信が続けて行われた場合、サブクラスでは、タイミングによっては自分が割り込まれたかどうか知る事ができないという状況が発生します。

前回のパッチではこれをふまえ、Threadの仕様を「イベント待ちの時に割り込まれたThreadはイベントを処理しない(イベントハンドラを実行しない)」ように変更しています。ただ、2つあるテストのうち、後者のテストである[割り込み]〜[イベント配信]では割り込み後も1度イベントを受け取ってしまっていました。これは割り込み後実際に割り込みハンドラが割り当てられるまでに1サイクルのインターバルがあるためその隙に飛び込んできたイベントを受け取ってしまっていたためです。

まず、テストの、割り込まれた後のイベントチェック部分がおかしかったので修正します。

//300mSec後にテスト

setTimeout(

    async(

        function():void

        {

            //InterruptHandlerTestThreadの割り込みハンドラが実行されたかどうかテスト

            //assertEquals(“interrupt event interrutHandler finalize “, Static.log);

            assertEquals(“interrupt interrutHandler finalize “, Static.log);

テストを実行します。
予想通り割り込まれた後もイベントを受け取ってしまうためテストに失敗します。


..E
Time: 0.66
There was 1 failure:
1) inturruptHandlerExecution2(org.libspark.thread::InterruptHandlerTest)
Error: expected:<interrupt []interrutHandler fina...> but was:<interrupt [event ]interrutHandler fina...>

そこで、Threadのコードにもう一つだけ処理を加えこの問題を回避する事にします。
ここではイベントを受け取ったとき割り込みが予約されていれば何もしないようにしています。

private function eventHandler(e:Event, handler:EventHandler):void

{

    //割り込みが予約されていれば何もしない

    var me:Thread = this;

    if(

        _reserveInterrupt.some(

            function(thread : Thread, …param):Boolean

            {

                return thread == me;

            }

        )

    ) return;

                

    //実行関数が割り込みハンドラならイベントハンドラをリセットして何もしない

 

テストを実行すると無事にパスします。


CYCLE 0--------------
@ internalExecute (RUNNABLE
# run() (RUNNABLE
@ register event  (RUNNABLE
CYCLE 1--------------
@ internalExecute (WAITING
〜〜 省略 〜〜
CYCLE 8--------------
@ internalExecute (WAITING
[DISPATCH]
@ eventHandler  (WAITING
@ internalExecute (RUNNABLE
# received event (RUNNABLE
!!!!!!!!!!!!!!!!!!! CALL INTERRUPT !!!!!!!!!!!!!!!!!!!
@ interrupt  (RUNNABLE
CYCLE 9--------------
@ internalExecute (RUNNABLE
# run() (RUNNABLE
@ register event  (RUNNABLE
@ internalExecute (WAITING
[DISPATCH]
@ eventHandler  (RUNNABLE
CYCLE 10--------------
@ internalExecute (RUNNABLE
# INTERRUPTED
CYCLE 11--------------
@ internalExecute (TERMINATING
# finalize() (TERMINATING
CYCLE 0--------------
@ internalExecute (RUNNABLE
# run() (RUNNABLE
@ register event  (RUNNABLE
CYCLE 1--------------
@ internalExecute (WAITING
〜〜 省略 〜〜
CYCLE 6--------------
@ internalExecute (WAITING
!!!!!!!!!!!!!!!!!!! CALL INTERRUPT !!!!!!!!!!!!!!!!!!!
@ interrupt  (WAITING
[DISPATCH]
@ eventHandler  (WAITING
CYCLE 7--------------
@ internalExecute (WAITING
@ internalExecute (WAITING
[DISPATCH]
@ eventHandler  (RUNNABLE
CYCLE 8--------------
@ internalExecute (RUNNABLE
# INTERRUPTED
CYCLE 9--------------
@ internalExecute (TERMINATING
# finalize() (TERMINATING
..
Time: 0.654
OK (2 tests)

 

シーケンス図です。

fixed

今回の修正をもう一度SparkProjectにコミットしました。

カテゴリー: ActionScript3 | コメントする

Threadの割り込みハンドラが失敗することがある(その2)

この問題は先日のアップデートでパッチがあたりました。

最新のtrunkのThreadではこの問題は起きないようです。


前回の続きです。

問題をもう一度整理してみます。
問題点は2つあります。

1・ ステートがWAITINGの状態でイベントハンドラが実行され即座にRUNNABLEになる。
このステートが再びWAITINGになるのは次のサイクル。
つまりサイクルとサイクルの間でステータスがRUNNABLEになってしまう事があり、このとき割り込まれると割り込みハンドラは失敗する。

2・ WAITINGの状態で割り込まれると割り込みハンドラは次の実行関数として正しく設定される。
しかしこの実行関数が次のサイクルで実行される前にイベントを受け取ってしまうとイベントハンドラ内に書かれた処理内容によっては実行関数を上書きしてしまう。

まず2から取りかかります。

割り込みハンドラとイベントハンドラは排他的な関係のように思えます。
なぜならイベント待ちをしているThreadに割り込みをかけるという事は、もうイベントを受け取りたくないという事だからです。
これをヒントに次のような方針でパッチを当てます。

「イベントハンドラがイベントを受け取ったとき、実行関数が割り込みハンドラであれば(このThreadは割り込まれたので)、そのイベントは無視する。」

以下がイベントハンドラの部分のパッチコードです。(1026行目)

private function eventHandler(e:Event, handler:EventHandler):void

{

    //実行関数が割り込みハンドラならイベントハンドラをリセットして何もしない

    if (_interruptedHandler != null && _runHandler == _interruptedHandler) 

    {

        // イベントハンドラをリセット

        resetEventHandlers();

        return;

    }

    // 既にイベントが起こっていれば何もしない

 

つぎに1のほうですが以下のような方針でパッチを当てる事にします。

ステータスがRUNNABLEになる瞬間を避けて、WAITINGの時に確実に割り込みの内部処理を実行する

割り込み処理はinterrup()メソッド内に書かれています。interrupt()が呼ばれた時にすぐにこの処理を実行してしまわないで、ステータスがWAITINGの時にこの処理を行いたい。ではステータスが確実にWAITINGなのはどんなタイミングでしょうか?サイクルが終わった直後なら確実にWAITINGのはずです。サイクルが終わった直後に、イベントに先を越されないようすぐに割り込み処理を実行するようにします。

割り込み処理の遅延をするために、キュー用の変数を用意します。(138行目)

public class Thread extends Monitor

{

    private static var _executor:IThreadExecutor;

    private static var _threadIndex:uint = 0;

    private static var _currentThread:Thread = null;

    private static var _toplevelThreads:Array = [];

    private static var _uncaughtErrorHandler:Function = null;

    private static var _defaultErrorHandlers:Dictionary = null;

    private static var _reserveInterrupt : Array = [];

 

interrupt()が呼び出された時、呼び出されたThreadインスタンスをこのキューに格納します。
また実際の割り込み処理の部分を、新しいメソッドinternalInterrupt()を作って丸ごと移動します。
以下がinterrupt()部分のパッチコードです。(791行目)

public function interrupt():void

{

    //割り込みを予約し、次のサイクルの最後に実行する

    _reserveInterrupt.push(this);

}

internal function internalInterrupt():void

{

    if (_state == ThreadState.WAITING || _state == ThreadState.TIMED_WAITING) {

 

最後に、サイクルの最後に割り込み処理を実行するように、executeAllThreads()メソッドの最後に以下のコードを追加します。(350行目)

public static function executeAllThreads():void

{

    // 全てのトップレベルスレッドを呼び出す

    

〜〜 省略 〜〜

    

    //予約された割り込みハンドラがあれば実行する

    _reserveInterrupt.forEach(

        function(thread:Thread, …param):void

        {

            thread.internalInterrupt();

        }

    );

    _reserveInterrupt = [];

}

 

テストを実行すると無事にテストをパスします

CYCLE 0--------------
@ internalExecute (RUNNABLE
# run() (RUNNABLE
@ register event  (RUNNABLE
〜〜 省略 〜〜
CYCLE 8--------------
@ internalExecute (WAITING
[DISPATCH]
@ eventHandler  (WAITING
@ internalExecute (RUNNABLE
# received event (RUNNABLE
!!!!!!!!!!!!!!!!!!! CALL INTERRUPT !!!!!!!!!!!!!!!!!!!
@ interrupt  (RUNNABLE
CYCLE 9--------------
@ internalExecute (RUNNABLE
# run() (RUNNABLE
@ register event  (RUNNABLE
[DISPATCH]
@ eventHandler  (RUNNABLE
CYCLE 10--------------
@ internalExecute (RUNNABLE
# INTERRUPTED
CYCLE 11--------------
@ internalExecute (TERMINATING
# finalize() (TERMINATING
CYCLE 0--------------
@ internalExecute (RUNNABLE
# run() (RUNNABLE
@ register event  (RUNNABLE
〜〜 省略 〜〜
CYCLE 6--------------
@ internalExecute (WAITING
!!!!!!!!!!!!!!!!!!! CALL INTERRUPT !!!!!!!!!!!!!!!!!!!
@ interrupt  (WAITING
[DISPATCH]
@ eventHandler  (WAITING
@ internalExecute (RUNNABLE
# received event (RUNNABLE
CYCLE 7-------------- (※このサイクルで割り込みハンドラが割り当てられる)
@ internalExecute (RUNNABLE
# run() (RUNNABLE
@ register event  (RUNNABLE
[DISPATCH]
@ eventHandler  (RUNNABLE
CYCLE 8--------------
@ internalExecute (RUNNABLE
# INTERRUPTED
CYCLE 9--------------
@ internalExecute (TERMINATING
# finalize() (TERMINATING
..
Time: 0.659
OK (2 tests)

シーケンス図です

fixed the problem


今回のテストと合わせて既存のすべてのテストをパスしたので、このパッチを当てる事で大きな問題は多分起きないんじゃないかと思いますが、何か問題が起きるようであれば教えてください。オフィシャルなパッチじゃないのでクレームはSparkProjectではなくimajuk@mac.comまで

追記:sparkProjectのThreadのbranchesにcommitしました。http://www.libspark.org/svn/as3/Thread/branches/imajuk

カテゴリー: ActionScript3 | コメントする

Threadの割り込みハンドラが失敗することがある(その1)

[追記]この問題は先日のアップデートでパッチがあたりました。

最新のtrunkのThreadではこの問題は起きないようです。


Threadを使っているとある問題に気づく事があります。
それは「イベントをリスンするThreadでinterrupt()を呼び出した際に割り込みハンドラが実行されないことがある」問題です。
event()を実行してイベントのリスンを開始したThreadは待機状態となり、その状態でinterrupt()を実行すると割り当てられた割り込みハンドラが実行されます。が、場合によっては期待する割り込みハンドラが実行されない事があるのです。

上記の問題は、サイクルとサイクルの狭間、つまり一つのThreadの実行関数が終わり次のThreadの実行関数が始まるまでの間に、イベントハンドラとinterrupt()の両方が実行された時におこります。

(※ここでいうサイクルとはThread.initialize()で渡すIThreadExecutorの実装によって定義されるThreadの実行の単位のことです。例えばEnterFrameThreadExecutorはユーザが定義したフレームレート、30fpsなら1/30秒に一回Threadが実行され、IntervalThreadExecutorならIntervalThreadExecutorのコンストラクタに渡したインターバルで一回Threadが実行されます。)

この問題に気づく典型的な例は、マウスクリックに反応するようにイベントをリスンしているThreadをinterrupt()するケースやステージのリサイズを監視するThreadをinterrupt()するケースなど、短い間隔でイベントをキャッチするThreadに割り込むケースです。
(参考:Threadの割り込みがうまくいかないことがある

このエントリはここから普段ユーザが意識する必要のないThread内部の振る舞いを見ていきます。
もしこの問題の解決方法だけを知りたい場合は「Threadの割り込みハンドラが失敗することがある(その2)」を見てみてください。

問題が再現するテストを書く

さて、この問題が再現するテストを書いてみて何がおきているかを調べてみます。
以下がそのテストコードです。ThreadライブラリのリポジトリにあるtestディレクトリにTestSuiteが用意されているので、ここに今回のテストInterruptHandlerTest.asを追加しました。

package org.libspark.thread

{

    import flash.utils.setTimeout;

    import flash.display.Sprite;

    import org.libspark.as3unit.after;

    import org.libspark.as3unit.assert.*;

    import org.libspark.as3unit.before;

    import org.libspark.as3unit.test;

    

    use namespace test;

    use namespace before;

    use namespace after;

    /**

     * 割り込みハンドラのテスト

     *   イベントをリスンするThreadインスタンスがあり、

     *   Threadの実行タイミング間でイベントの配信と割り込みが両方行われた時に

     *   割り込みハンドラが正しく実行されるかテスト

     */

    internal class InterruptHandlerTest extends Sprite

    {

        private var dispatcher : Dispatcher;

        private var thread : InterruptHandlerTestThread;

        before function setup() : void

        {

            Thread.initialize(new EnterFrameThreadExecutor());

            Static.log = “”;

            

            //イベント配信オブジェクト

            dispatcher = new Dispatcher();

            

            //テスト用Thread

            thread

                new InterruptHandlerTestThread(dispatcher);

        }

        after function teardown() : void

        {

            Thread.initialize(null);

            Static.log = “”;

        }

        //イベント配信直後に割り込みするケースのテスト

        test function inturruptHandlerExecution() : void

        {

            //200mSec後に [イベント配信] -> [割り込み]

            setTimeout(

                function():void

                {

                    //Threadに向けてイベントを配信

                    dispatcher.dispatch();

                    

                    //Threadinterrupt()を呼び出し

                    trace(“¥n!!!!!!!!!!!!!!!!!!! CALL INTERRUPT !!!!!!!!!!!!!!!!!!!¥n”);

                    thread.interrupt();

                    

                }, 200

            );

            //250mSec後にもう一度イベント配信.このイベントはキャッチしないはず

            setTimeout(

                function():void

                {

                    dispatcher.dispatch();

                },

                250

            );

            

            //300mSec後にテスト

            setTimeout(

                async(

                    function():void

                    {

                        //InterruptHandlerTestThreadの割り込みハンドラが実行されたかどうかテスト

                        assertEquals(“event interrupt interrutHandler finalize “, Static.log);

                        //割り込み後のステータスをテスト

                        assertEquals(ThreadState.TERMINATED, thread.state);

                    }

                ), 

                300

            );

                

            //テスト用Thread開始

            thread.start();

        }

    }

}

import org.libspark.thread.Thread;

import flash.events.Event;

import flash.events.EventDispatcher;

//テスト用Thread

class InterruptHandlerTestThread extends Thread 

{

    private var dispatcher : Dispatcher;

    

    public function InterruptHandlerTestThread(dispatcher:Dispatcher)

    {

        super();

        this.dispatcher = dispatcher;

    }

    

    override public function interrupt():void

    {

        Static.log += “interrupt “;

        super.interrupt();

    }

    

    override protected function run() : void

    {

        trace(”    # run()”, printStatus(state));

        

         //割り込みハンドラを生成

        var interruptHandler : Function

            function():void

            {

                trace(”    # INTERRUPTED¥n”);

                Static.log += “interrutHandler “;

            };

        

        //割り込みハンドラを設定

        interrupted(interruptHandler);

        

        //イベントをキャッチしたら再びイベントをリスンして待機状態へ

        event(

            dispatcher,

            InternalEvent.COMPLETE,

            function():void

            {

                trace(”    # received event”, printStatus(state));

                Static.log += “event “;

                //イベントを受け取ったら再びイベントをリスン

                next(run); 

            }

        );

    }

    override protected function finalize() : void

    {

        Static.log += “finalize “;

        trace(”    # finalize()”, printStatus(state));

    }

    

}

//イベント配信オブジェクト

class Dispatcher extends EventDispatcher

{

    public function dispatch():void

    {

        trace(“¥n [DISPATCH]¥n”);

        dispatchEvent(new InternalEvent(InternalEvent.COMPLETE));

    }

}

//テスト用イベント

class InternalEvent extends Event

{

    public static const COMPLETE : String = “COMPLETE”;

    

    public function InternalEvent(type : String)     

    {

        super(type, false, false);

    }

}

class Static

{

    public static var log:String;

}

さらに、Thread内の動きを観察するために、org.libspark.thread.Threadとorg.libspark.thread.EnterFrameThreadExecutorにtrace()を仕込みます。

Threadにデバッグ用メソッドを追加

//Threadのステータスコードを文字列に変換するユーティリティ

public static function printStatus(s : int) : String

{

    switch(s)

    {

        case ThreadState.NEW:

            return “(NEW”;

        case ThreadState.RUNNABLE:

            return “(RUNNABLE”;

        case ThreadState.TERMINATED:

            return “(TERMINATED”;

        case ThreadState.TERMINATING:

            return “(TERMINATING”;

        case ThreadState.TIMED_WAITING:

            return “(TIMED_WAITING”;

        case ThreadState.WAITING:

            return “(WAITING”;

    }

    throw new Error(s);

}

Threadの以下のメソッドにtrace()を追加
event()メソッド

public static function event(dispatcher:IEventDispatcher, type:String, func:Function, useCapture:Boolean = false, priority:int = 0, useWeakReference:Boolean = false):void

        {

            trace(”  @ register event “, printStatus(getCurrentThread().state));

            getCurrentThread().addEventHandler(dispatcher, type, func, useCapture, priority, useWeakReference);

eventHandler()メソッド

private function eventHandler(e:Event, handler:EventHandler):void

        {

            trace(”  @ eventHandler “, printStatus(_state));

            //実行関数が割り込みハンドラならイベントハンドラをリセットして何もしない

interrupt()メソッド

public function interrupt():void

        {

            trace(”  @ interrupt “, printStatus(_state));

            //割り込みを予約し、次のサイクルの最後に実行する

internalExecute()メソッド

private function internalExecute(error:Object, errorThread:Thread):Boolean

        {

            trace(”  @ internalExecute “ + [printStatus(_state)]);

            if (_state == ThreadState.WAITING || _state == ThreadState.TIMED_WAITING) {

EnterFrameThreadExecutorのenterFrameHandlerにtrace()を仕込みます。(カウンタ用の変数を一つ追加しています)

private var time : int = 0;

private function enterFrameHandler(e:Event):void

{

trace(“¥nCYCLE “ + time++ + “————–“);

Thread.executeAllThreads();

}

これで準備完了。
このテストでは、テスト開始100mSec後にイベント配信とThreadの割り込みを行い、その50mSec後にもう一度イベント配信、最後に200mSec後に割り込みハンドラが実行されたか確認しています。


テスト

テストしてみます。
割り込みハンドラが実行されずにテストは失敗します。

以下はその出力結果です。

CYCLE 0--------------
@ internalExecute (RUNNABLE
# run() (RUNNABLE
@ register event  (RUNNABLE
〜〜 省略 〜〜
CYCLE 8--------------
@ internalExecute (WAITING
[DISPATCH]
@ eventHandler  (WAITING
@ internalExecute (RUNNABLE
# received event (RUNNABLE
!!!!!!!!!!!!!!!!!!! CALL INTERRUPT !!!!!!!!!!!!!!!!!!!
@ interrupt  (RUNNABLE
CYCLE 9--------------
@ internalExecute (RUNNABLE
# run() (RUNNABLE
@ register event  (RUNNABLE
[DISPATCH]
@ eventHandler  (WAITING
@ internalExecute (RUNNABLE
# received event (RUNNABLE
CYCLE 10--------------
@ internalExecute (RUNNABLE
# run() (RUNNABLE
@ register event  (RUNNABLE
CYCLE 11--------------
@ internalExecute (WAITING
.E
Time: 0.341
There was 1 failure:
1) inturruptHandlerExecution(org.libspark.thread::InterruptHandlerTest)
Error: expected:<event interrupt [interrutHandler finalize] > but was:<event interrupt [event] >

何が起きたか見ていきましょう。

@マークのある行はThread(スーパークラス)内部でおこった出来事、#マークのある行はThread(テスト用のサブクラス)で起こった出来事です。
マークの後に何が起こったか、そしてその時のThreadのステータスを出力しています。
例えば、

@ internalExecute (RUNNABLE

はThread.internalExecuteが実行され、その時のステータスはRUNNABLEだったという感じです。
(※このテストコードの出力結果は実行時のフレームレートやCPUの速度などに影響します。もしあなたがこのテストを実行した場合、テストの成否は同じはずですが出力結果は異なる可能性があります。今回は31FPSでこのテストを実行しました。)

まず、CYCLE0を見てみてください。

CYCLE 0--------------
@ internalExecute (RUNNABLE
# run (RUNNABLE
@ register event  (RUNNABLE

run()が実行されイベントハンドラが設定されています。
ここでこのサイクルは終了です。

次に、CYCLE8を見てみます。
このサイクルで割り込みハンドラが実行されない問題が起きています。

CYCLE 8--------------
@ internalExecute (WAITING
[DISPATCH]
@ eventHandler  (WAITING
@ internalExecute (RUNNABLE
# received event  (RUNNABLE

まずイベントが配信されイベントハンドラが実行されるという期待した動作がスムーズに展開していきます…

!!!!!!!!!!!!!!!!!!! CALL INTERRUPT !!!!!!!!!!!!!!!!!!!
@ interrupt  (RUNNABLE
CYCLE 9--------------
@ internalExecute (RUNNABLE
# run() (RUNNABLE
@ register event  (RUNNABLE
[DISPATCH]
@ eventHandler  (WAITING
@ internalExecute (RUNNABLE
# received event (RUNNABLE

その後、interrupt()が実行されますが、次のサイクル9で実行されるはずの割り込みハンドラは実行されていません。
また、割り込まれた後イベントのリスンをやめる事が期待されていますが、次のサイクルであるサイクル9でも何事もなかったようにイベントをリスンし続けています。

何が起きたのでしょうか?

もう一度割り込まれた時の出力結果を見てみます。

!!!!!!!!!!!!!!!!!!! CALL INTERRUPT !!!!!!!!!!!!!!!!!!!
@ interrupt (RUNNABLE

interrupt()が呼び出されたときThreadのステータスはRUNNABLEになっています。
これは、interrupt()される直前にイベントハンドラが実行され、ステータスが変更されたためです。


Threadの仕様によると、割り込みハンドラが実行される条件はイベントハンドラが設定されている事、つまりステータスがWAITINGであることです。しかし割り込み直前でイベントハンドラが実行されたためにステータスがRUNNABLEになってしまいました。イベントハンドラが再度設定されステータスが再びWAITINGになるのは次のサイクルです。次のサイクルが始まる前に割り込みが実行されると、この時点ではまだステータスがRUNNABLEであるため、このエントリで取り上げている問題が起こります。

テキストだけだとわかりにくいので図にしてみました。(前のエントリでも書いてみたシーケンス図です。見方がわからない場合は前のエントリを参照してください)

 

まず、割り込みハンドラが意図する通りに実行されるケースです。

successfuly interruption

つぎに失敗したケースです。

failed interruption

サイクルとサイクルの間でイベントハンドラの実行と割り込みの実行の両方が起きているのがわかります。
イベントのキャッチによりThreadのステータスが一時的にRUNNABLEとなり、そのタイミングで割り込みが実行されると割り込みハンドラは失敗します。

「割り込みハンドラが実行されるのは待機状態に割り込まれた時」とThreadは仕様としているので、これは正しい動作と言えなくもないです。が、ユーザは(このテストのようなケースでは)「イベントを受け取ったら再びイベント待機状態」つまり「割り込まれるまでは常に待機状態」のThreadを意図していると思うのでバグのようにも思えます。
この問題が仕様なのかバグなのか微妙なところです。

運用でカバーできないのか?

ステータスが一時的にRUNNABLEになってしまうのが問題です。しかしステータスがRUNNABLEの時に割り込みが実行されるとすれば割り込みフラグ(isInterrupted)たつはずです。このフラグを利用すれば手動で割り込みハンドラを呼び出す事ができるのではないでしょうか?
これを使って運用でカバーできないのでしょうか?

例えば以下のようなコードです。

override protected function run() : void

{

    trace(”    # run()”, printStatus(state));

    

     //割り込みハンドラを生成

    var interruptHandler : Function

        function():void

        {

            trace(”    # INTERRUPTED¥n”);

            Static.log += “interrutHandler “;

        };

        

    //割り込みフラグがたっていれば割り込みハンドラを実行して終了

    if (isInterrupted)

    {

        interruptHandler();

        return;

    }

        

    //割り込みハンドラを設定

    interrupted(interruptHandler);

        

    //イベントをキャッチしたら再びイベントをリスンして待機状態へ

    event(…

テストコードに上記の処理を追加すればテストは通ります。

  .
Time: 0.337
OK (1 test)

うまくいったように思えます。が、まだ2つの問題を抱えています。

1つ目は、待機状態(WAITING)でないのに割り込みハンドラを呼び出すという意味でThreadの仕様を変えてしまっている事です。
2つ目は、実はまだinterrupt()のタイミングによってはテストが失敗するという事です。

後者について詳しく見ていきます。

まず、現段階のテストでは、イベントをキャッチした後割り込みが実行されています。([イベント] →  [割り込み])。しかし、割込みを実行した後にイベントをキャッチするケース([割り込み] → [イベント])をまだテストしていません。

テストコードにテストを追加してみます。

//割り込み直後にイベント配信

test function inturruptHandlerExecution2() : void

{

    //200mSec後に [イベント配信] -> [割り込み]

    setTimeout(

        function():void

        {

            //Threadinterrupt()を呼び出し

            trace(“¥n!!!!!!!!!!!!!!!!!!! CALL INTERRUPT !!!!!!!!!!!!!!!!!!!¥n”);

            thread.interrupt();

            //Threadに向けてイベントを配信

            dispatcher.dispatch();

                    

        }, 200

    );

    //250mSec後にもう一度イベント配信.このイベントはキャッチしないはず

    setTimeout(

        function():void

        {

            dispatcher.dispatch();

        },

        250

    );

            

    //300mSec後にテスト

    setTimeout(

        async(

            function():void

            {

                //InterruptHandlerTestThreadの割り込みハンドラが実行されたかどうかテスト

                assertEquals(“interrupt event interrutHandler finalize “, Static.log);

                //割り込み後のステータスをテスト

                assertEquals(ThreadState.TERMINATED, thread.state);

                //割り込み後の割り込みフラグをチェック(待機状態での割り込みなのでfalseのはず)

                assertFalse(thread.isInterrupted);

            }

        ), 

        300

    );

                

    //テスト用Thread開始

    thread.start();

}

前は「イベント配信」「割り込み」という順番でしたが、今度のテストは「割り込み」「イベント配信」という順番です。前のテストとはこの呼び出しの順番だけが違います。

このテストを実行してみるとテストは失敗します。

.E.
Time: 0.658
There was 1 failure:
1) inturruptHandlerExecution2(org.libspark.thread::InterruptHandlerTest)
Error: expected:<interrupt event [interrutHandler] > but was:<interrupt event [event] >

これはwonderflでsoundkitchenさんが指摘していることと同じことが起こっています。

図にすると以下のような感じです。

another failure case

このケースでは割り込み後のイベントハンドラの動作で、割り込みフラグと割り込みハンドラが上書きされてしまいます。
こうなるともはやThreadは自分が割り込まれたかどうかを知る手段がありません。運用でカバーするのは無理そうです。
やはりこの問題を解決するにはThreadのソースコードに手を入れる必要があるかもしれません。

次回のエントリでは解決編として、Threadのソースにパッチを当てる方法を書いてみます。

つづく

カテゴリー: ActionScript3 | コメントする

Threadはええぞ〜

前に一度org.libspark.threadライブラリについてエントリーを書きましたが、このライブラリは本当に素晴らしいです。フレキシブルに使えるし、使っているだけで自然とOOPの作法が身に付いていくような気もします。
なんでこんなに使い心地がいいのかつらつら考えているのですが、いまだうまくエントリーにまとめられそうにありません。が、

とりあえず、今回は「Threadは新しいデザインパターンの実装なのではないか」という大胆な仮説を唱えてみようかと思いますw

まずは 同期処理、非同期処理のおさらい

さて、タスクAのあとタスクBを実行したいという状況があるとします。

以下のようなコードからスタートします。

package
{
    import flash.display.Sprite;

    public class AboutThread extends Sprite
    {
        public function AboutThread()
        {
            /**
            * タスクAが終わった後タスクBをすませたい
            */
            doTask();
        }

        //タスク実行
        private function doTask():void
        {
            //タスクAを生成
            var a:A = new A();
            //タスクを実行
            a.execute();
            //タスクBを生成
            var b:B = new B();
            //タスクを実行
            b.execute();
        }
    }
}

とってもシンプルで清々しいコードですね。何の問題もありません。平和な世界。

ただ現実はこのように甘くはないかもしれません。
タスクAは非同期なタスクかもしれないからです。
もし非同期の場合は、タスクの終了を通知してもらう手続きをして、さらに通知を受け取るイベントハンドラも必要になりますね。
あと、一度だけ通知してもらいたい場合(よくある!)は、ハンドラ内で通知解除の手続きも必要です。

コードは以下のような感じです。

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

    public class AboutThread extends Sprite
    {
        public function AboutThread()
        {
            /**
            * タスクAが終わった後タスクBをすませたい
            */
            doTaskA();
        }

        //タスクAを実行
        private function doTaskA():void
        {
            //タスクAを生成
            var a:A = new A();
            //タスク終了を通知してもらう
            a.addEventListener(Event.COMPLETE, taskACompleteHandler);
            //タスクを実行
            a.execute();
        }
        
        //タスクAが終了した
        private function taskACompleteHandler(event:Event):void
        {
            //リスナの登録を解除            
            A(event.target).removeEventListener(Event.COMPLETE, taskACompleteHandler);
            //タスクBスタート
            doTaskB();
        }

        //タスクBを実行
        private function doTaskB():void
        {
            //タスクBを生成
            var b:B = new B();
            //タスク終了を通知してもらう
            b.addEventListener(Event.COMPLETE, taskBCompleteHandler);
            //タスクを実行
            b.execute();
        }

        //タスクBが終了した
        private function taskBCompleteHandler(event:Event):void
        {
            //リスナの登録を解除            
            B(event.target).removeEventListener(Event.COMPLETE, taskACompleteHandler);
            //...続く
        }
    }
}

突然コード量が増えてしまいました。ざっと倍くらいでしょうか。

何が問題なのか?

単純にコードの量が増えたことを問題にしているわけではありません。
最近の気の利いたエディタを使っていれば半自動でリスナ登録やハンドラの記述をやってくれますもんね。
コードを書くときはそれほど負担にはならないんです。でも、コードを読むときはどうでしょうか?

本来書きたかった処理は、「タスクAのあとにタスクBを実行したい」という物だったはずです。
もちろん残念ながらコンパイラは自然言語で書かれた命令のような抽象的なことを理解することはできないので(死ぬまでにはそんなコンパイラが出てくることを望みますが)、
それなりの手続きを記述していく必要があります。
問題にしているのは手続きの細かさです。

上記のコードは、「タスクAのあとにタスクBを実行したい」ということを記述したいはずのコードなのに、その一段低レベルなイベントシステムの手続きという層がむき出しになっています。
また、これが、「Aのあと、Bのあと、Cのあと、Dのあと、Eしたい」の様なケースではイベントの手続きという低レベルなコードがほとんどを全体のほとんどをしめてしまうでしょう。

Threadはこれを解決する?

はい。解決します。
このように

package
{
    import org.libspark.thread.Thread;

    public class AboutThread extends Thread
    {
        public function AboutThread()
        {
            /**
            * タスクAが終わった後タスクBをすませたい
            */
            doTaskA();
        }
        
        //タスクAを実行
        private function doTaskA():void
        {
            //タスクAを生成
            var t:Thread = new DoAThread();
            //タスクを実行
            t.start();
            //タスクの終了を待つ
            t.join();
            //終わったらタスクB開始
            next(doTaskB);
        }

        //タスクBを実行
        private function doTaskB():void
        {
            //タスクBを生成
            var t:Thread = new DoBThread();
            //タスクを実行
            t.start();
            //タスクの終了を待つ
            t.join();
            //終わったら次の...
            next(doSomething);
        }
    }
}

コード量も減りましたが、何より嬉しいのは上から流れるように処理を記述できる点です。
イベントという仕組みは一見隠蔽され、一段抽象的なレベルで処理を記述できています。直感的とも言えますね。
start()やnext()などとても直感的な記述で、もし初めてThreadを使ったコードを見た人でも何をやっているか想像できるんじゃないでしょうか?
t.join()というちょっと違和感を感じるかもしれない記述がありますが、これは「tの終了を待つ」と読み替えてもらうといいです。


ちょっと脱線
個人的にはjoin()という名前に違和感を感じる。startやnextと意味の階層が違う気がしてて、うまく言えないけど利用者視点じゃなく設計者視点な名前という感じで。
これがもし、waitForFinishing()とか(ださいけど)だと意味がつながる気がする。さらに、join()とnext()をまとめたシンタックスを用意して、
さらにさらに流れるようなインターフェイスで、t.start().andThen(func)とか書けると最高だー。


非同期処理のコード大盛り問題を解決するだけの物ではない

本題です。
最近、自分はコントローラはすべてThreadで書いています。同期、非同期関係なくです。
たとえ同期なタスクでもjoin()してしまいます。そうすると何が嬉しいのかと言うと、
もし、コードを書き始めた当初同期だったあるタスクがあとから非同期になっても、クライアントコードはいっさい変更する必要がありません。
いいかえれば、クライアントはそのタスクが同期か非同期かを意識する必要なく処理を記述できるんです。

「AしてBしてCしてDして…」
それらのタスクが同期なタスクか非同期なタスクかいっさい気にかけることなく流れるように処理を記述でき、後から読むときも流れるようにコードを読めます。
エントリーの最初にあげた大胆な仮説、「Threadは新しいデザインパターンの実装なのではないか」というのはこれです。
「同期か非同期かを意識する必要なく処理を記述する」パターン。

ThreadはCommandパターンとCompositeパターンの応用だとは思うんですが、そのどちらにもない同期、非同期性の隠蔽という機能があるんですよね。
少なくともGoFの23のパターンにはないパターンだという気が。。
こういうパターンに既に名前がついているかどうか知っている人がいたら教えてください〜。

と書いては見たものの

うーん、新しいパターンはちょっと言い過ぎたかも。やっぱりw
ほとんどのCommand系のライブラリは非同期なタスクをサポートしてますもんね。
でも、いろいろあるCommand系のライブラリの中でも、クライアントコードの記述の簡潔さは一歩抜きん出ていると思います。

カテゴリー: ActionScript3 | 1件のコメント

最適化してます?

最適化(と言ってもいろいろあるけど)って、ひたすらコードに制限を加えていくことになりますよね。ドラスティックに効果がある最適化は特に。
融通の利かないコードと引き換えに速度を得られるんですね。わかります。
コードの資産化をいつも心がけている自分としては、いままで最適化にはあまり興味がなかったんですけど、城戸さんの公演のビデオを見て少し考え方が変わりました。
曰く、「浮いた分のパフォーマンスを別の表現にまわせる」
たしかに。

というわけで、今回は最適化のプラクティスです。
テーマは魚っぽいノードの群れ。


ノード数1000


ノード数3000

僕の環境では、ノード数1000だと60/60fpsキープできるのですが、
ノード数を3000まで増やすとで32/60fpsどまりでした。
にわかオプティマイザーだとこんなもんですかね。くやしい。
まだまだ早くできる余地がある気がします。

施した最適化は以下のとおり
・オブジェクト複製による残像効果は使わない。
・ノードはaddChild()しないでBitmapDataに描画
・ループ内でオンデマンドでやる必要のない計算は全て前もってやっておく
・ループ内で極力newしない
・全てビットマップ化
・flockポイ動きだけどいんちき
・その他、ちまたにあふれている重箱の隅をつつくような最適化Tipsいろいろ
CS4のVector使えばもうちょっと早くなるのかな。

カテゴリー: ActionScript3, flash | コメントする

絵で見るThread

Flasher各方面で話題のorg.libspark.thread.Threadだけど、最近ようやく使ってみた。
評判がいいのもうなづける素晴らしいライブラリだと思う。
もっと早く使ってみればよかった。。
ドキュメントがとてもよく出来ているので、使い方についてほとんど付け加えることはないんだけど、文章とコードだけだと、処理の実行単位とか実行順がわかりにくいかもしれないと思い(自分がそうだった)シーケンス図でまとめてみた。
ここまちがってるんじゃないの?みたいなつっこみ、絶賛募集してます。

シーケンス図の見方、超投げやりな説明


シーケンス図は、プログラムの実行時におけるインスタンスの処理の流れや、インスタンス間のコラボレーションの様子を表現できる。
インスタンスとインスタンスの寿命はこのように表現する。


時間は上から下へ流れる。
この図では、
1・ENTER_FRAMEイベントが起きた
2・誰かがnew A()を呼び出した
3・インスタンスAが生成された
事を表している。

Threadの実行単位は「実行関数」


Threadの実行単位は「実行関数」。
1フレーム※1に一度、Threadが持っている実行関数が1つだけ実行される。
2つは実行されない。※2
これ大事。
※1(1フレームと書いたが、フレームベースではなくintervalベースにすることもできるみたい)
※2(例外あり。event()参照)
runとfinalizeはデフォルトで用意されている実行関数。
runは必ず最初に呼び出される実行関数。
finalizeはThreadが終了する前に必ず呼び出される実行関数。


実行関数の中でnext()を呼ぶと、次の実行関数を予約できる。
予約された実行関数は、1フレーム1実行関数の原則に従い、次のフレームで実行される


実行関数の中でevent()を呼ぶと、任意のイベントが発生するまで待機状態になる。
イベントが発生すると予約した実行関数を実行する。
その後特に実行関数がなければ、finalize()が実行される
※(1フレーム1実行関数が基本だが、この場合は例外的に2つの実行関数(fとfinalize)が1フレームで実行される)


実行関数内でsleep()を呼ぶと、指定された時間のあいだ待機状態になる。
時間が過ぎると次の実行関数が実行される。

実行関数の定義まとめ

Threadの実行単位は実行関数なので、なにが実行関数なのかを意識した方が良い。
実行関数は、

  • run()
  • next()に渡された関数
  • event()に渡された関数
  • finalize()

上記に含まれないThread内の関数は実行関数ではないので注意。

Threadの親子関係


実行関数の中で新しいThreadを生成すると、Threadの親子関係が出来る。
AはBの親、BはCの親。
親子関係のあるスレッドは、それぞれ独立した時間軸を持ち実行関数を実行していく


子Threadの実行関数は必ず親Threadの実行関数より先に実行される。

Thread.graffle-19.jpg
子Threadがjoin()された時は、親Threadは子Threadのすべての実行関数が終了するまで次の実行関数の実行を待機する(待機状態になる)。
つまり、各Threadの独立した時間軸の一本化

Monitor


実行関数内で、Monitor::wait()を呼び出すと、そのThreadはMonitor::notify()が呼び出されるまで待機状態になる。
nonify()が呼び出された場合は即座に待機状態が解除され、notify()された同じフレームで実行関数が実行される。

待機状態まとめ

待機状態は、「時間指定あり待機状態」と「時間指定なし待機状態」に分けられる。

時間指定 あり なし
・join(timeout time)したとき
・sleep()したとき
・join()したとき(引数なし)
・event()したとき
・Monitor.wait()したとき

割り込み


interrupt()を呼び出すと、割り込みフラグが立つ。
割り込み処理はこのフラグを使って実装する。
ちなみにinterrupt()は実行関数ではないので注意。


待機状態のときに割り込まれると、指定した割り込みハンドラが実行される。
割り込みハンドラは、Thread.interrupted ( )をつかって各実行関数ごとに記述する。
待機状態で割り込まれ、割り込みハンドラがない場合はエラーとなる。


たとえ親子関係にあっても、割り込みは子Threadに伝播しない
もし伝播させたい場合はorg.libspark.thread.utils.ParallelExecutorまたはorg.libspark.thread.utils.SerialExecutorを使用する。

カテゴリー: ActionScript3 | コメントする

ConvolutionFilterで遊ぶ その4

ConvolutionFilterを使ったアニメーションの可能性

前回からの続きです。前回までを見てないと今回の話はわからないと思うので、もしまだであれば以下のリンクからど〜ぞ。

ここに黒背景に1pxの画像があります。
img1.jpg
この画像に以下のような行列でConvolutionFilterを適用すると…
k1.jpg
以下のようになります。前回までで見てきたように、ターゲットピクセルの50%の強さで、斜め4方向にピクセルを移動コピーしています。
img2.jpg


さて、このフィルタの適用結果に、さらにフィルタをかけ続けるといったいどうなるのか?とても気になったので、ツールに連続してフィルタを適用する機能を追加しました。
tool.jpg
三角形の再生ボタンを押すと、フィルタを連続して適用していきます。
スライダーを右に動かすと、フィルタをかけるインターバルが長くなっていきます。(注:上の画像はスナップショットなので、機能しませんよ〜)


再生ボタンを押すと、ドットで出来た円が拡大し、画面端で折り返した後、画像全体が白に収束します。
(下の画像どれかをクリックすると、アニメーションが再生されます。)
anim1.jpg
1・ドットで出来た円が拡大していく…
anim2.jpg
2・画面端で折り返す…
anim3.jpg
3・最後は白に収束する
円が拡大していくとこまではなんとなく予想してましたが、その後が想像を超えてました
では、シンプルなグレースケール画像ではなく、フルカラーの画像で同じ事をやると何が起こるのでしょうか?
下の画像に、同じConvolutionFilterを連続して適用してみると…
(下の画像をクリックすると、アニメーションが再生されます。)
anim0.jpg
anim4.jpg

結果は、一瞬で白に収束してしまいます。。
まぁ、さっきのような黒背景に1ピクセルの白があるだけの画像ではなく、
明るさを持ったピクセルが無数にあるフルカラー画像では当たり前の結果ですよね。
ということは、行列の値がまずかったと。
そこで、プリセットにあるガウスぼかしを連続で適用してみました。
(下の画像をクリックすると、アニメーションが再生されます。)
anim5.jpg
なんとなく味わいのあるフェードアウトで、最終的には黒に収束します。
ところで、ガウスぼかしの行列は以下のとおりで、全ての数値を足し合わせると「1」になります。
(0.33*4 + 0.11*4 + 0.44 = 1)
k2.jpg
いろいろやってわかったのですが、ConvolutionFilterを連続して適用する際、
行列の数値を足し合わせて1以下の場合、画像は黒(0)に収束する傾向があり、1より大きい場合は白(255)に収束していくようです。
(あとでみますが、どちらにも収束せずに発散することもあります。)
たとえば、ターゲットセルに対応する値をほんの少しだけ大きくしてみます。
k3.jpg
このように、列の数値を足し合わせた数を1より大きくすると、画像は白に収束していきます。RGBの各チャンネルが255方向へ収束するのでとてもきれいです。
anim6.jpg
anim7.jpg

さきほど、黒にも白にも収束しない場合があると言いましたが、
たとえば、以下のような行列の場合です。
k5.jpg
この行列で、ConvolutionFilterを適用し続けると、黒にも白にも収束せずに不思議なモアレ模様を描き続けます。
anim8.jpg
これがそのまま、何かの表現に使えるとは思いませんが、
このライフゲームのような不思議なアニメーションは面白いです。
ターゲットピクセルの値を少し変えてみると、
k6.jpg
黒に収束してしまいましたが、よりライフゲーム感が出てきました。シマシマの中を何かが泳いでいます(笑。
anim9.jpg


他にも面白いものがいくつか発見できたので、ならべておきます。
(画像をクリックすると、アニメーションが再生されます。)
ガウスぼかしのフェードアウトに似ていますが、終わりがけに一部カラーハーフトーンのような効果が見られます。
anim10.jpg
色面の色彩が荒くなっていき、最終的に点描のような状態で収束します。
anim11.jpg
画像が流れ落ちていく。。
anim12.jpg
コメントしにくいけど綺麗。。
anim14.jpg
これもコメントしにくい。ななめ。。
anim15.jpg

行列作成のこつ

行列の数値を色々変えながら遊んでいましたが、
コントロールしやすいのはピクセルの移動方向ぐらいで、
ほとんどの場合予想がつかない変化となります。。
それでも、いくつかコツと言えるようなものがあったので、あげてみます。

  1. 最終結果を暗くする、明るくする
    上でも書きましたが、行列の足し合わせた数が1を超えると白(255方向)へ収束し、1以下だと黒(0方向)へ収束します。ただしどちらにも収束しない場合もあります。
  2. 方向を変える例えば、以下のような行列は上側の数値よりも下側の数値の方が大きいです。このように下に重みをつけた場合、ピクセルは上に移動します。
    k8.jpg

    anim22.jpg

    下に移動させたい場合は、上下反転させて、上に重みをつければればOK。
    k9.jpg
    anim23.jpg

    同じ理屈で、45度回転させれば、斜め右上に重みがつくので斜め移動、さらに45度回転させれば、右の重みがつくので横移動になります。
    k10.jpg
    anim21.jpg

    k11.jpg
    anim20.jpg

    また、 縦横方向の重みは収束結果にピクセルが残りやすく、斜め方向の重みは収束結果にピクセルが残りにくいみたいです。

  3. 面白い行列を発見出来たら細かく微調整行列の合計が1以下の黒への収束と、1より大きい白への収束の境界付近が、収束するまでに長く時間がかかるので面白い効果が出来やすい気がします。
    面白い行列を発見出来たら、ターゲットピクセルの数値を増減させてこの境界を探してみましょう。
    0.01増減させただけで、見た目が大きく変わることもあります。

ConvolutionFilterを使ったアニメーションの利用の可能性

さて、ConvolutionFilterを使ったアニメーションの可能性を探ってみたわけですが、
「で、これなんかに使えるの?」という疑問が聞こえてくる気がします。
さすがに、クライアントワークなどにそのまま使えるとは思いません。ただ、表現の種(素材)として考えたときには一定のポテンシャルを感じます。
たとえば、最初の1pxを拡張していくグレースケールのアニメーションなんかは、
アルファチャンネルマスクとして使えば、スライドショーのトランジションとかに利用できるんじゃないかと思います。(綺麗かどうかはやってみてないのでわからんですが)
もしかしたら、白方向、黒方向へ収束するものは、そのままトランジションとして利用できるものもあるかもしれません。
他には、オリジナルの元画像を残しつつ、BlendMode.SREEN等で合成しながらアニメーションさせてみるとか、アニメーション中に別の画像を重ねて見るとか。。
個人的には、置き換えマップ(DisplacementMapFilter)のソースとして、ConvolutionFilterを使ったアニメーションを利用するとどんな事が可能になるか探ってみたいとこだったりします。
また何か発見があったらレポートします。
とりあえずこのシリーズは今回で終了です。

カテゴリー: flash | 2件のコメント