タワーディフェンスタイプのゲームを作ってみようと思う 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 パーマリンク

タワーディフェンスタイプのゲームを作ってみようと思う 3日目 への2件のフィードバック

  1. 光次 のコメント:

    結局これ以上つくらないんですか?

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です