Help us understand the problem. What is going on with this article?

Unityでタワーディフェンスを作ったのでアルゴリズムや手順を公開

More than 3 years have passed since last update.

Unityでタワーディフェンスを作りました。実行サンプルとソースコードは以下のURLから取得できます。

TD.png

採用したルール

最近ではいろんなジャンルが混ざり合ったタワーディフェンスが多いですが、今回作ったのは古典的(と思われる)タワーディフェンスのルールを採用しました。

  • 砲台を好きな位置に配置できる
    • ただし敵の経路には配置できない
  • Wave単位で敵が出現する
  • 敵は決まった経路で移動する
  • 敵が拠点にたどりついたらダメージを受ける
    • 3回ダメージを受けたらゲームオーバー
  • 配置した砲台をアップグレードできる

タワーディフェンスにおける砲台(タワー)の配置方法は、大きく分けて自由配置型と固定型(敵の移動ルートが決まっている)の2パターンあるのですが、実装が簡単な「固定型」となっています。

登場人物

このタワーディフェンスにおける登場人物(ゲームオブジェクト)は以下のとおりです。

  • 砲台(Towerクラス)
  • 敵(Enemyクラス)
  • 砲台が撃つ弾(Shotクラス)
  • ゲーム管理(GameMgrクラス)
  • 敵生成管理(EnemyGeneratorクラス)

本当はもっとクラスはありますが、大きなところとしてはこの5つです。
メニュー周りは、GuiクラスがuGUIオブジェクトを管理して、GameMgrクラスがuGUIの動作制御を行います。

作成手順

使うツールや好みによって手順は変わりそうですが、今回のタワーディフェンスは、おおよそ以下の手順で作りました。

  1. マップデータの作成(Tiled Map Editor)
  2. 敵の移動経路の作成(Tiled Map Editor)
  3. マップの読み込みを実装(TmxLoaderクラス / Layer2Dクラス)
  4. 敵(Enemyクラス)の実装。移動経路データから敵の移動リストを実装
  5. 砲台(Towerクラス)の実装。一番近くにいる敵を攻撃するようにする
  6. ショット(Shotクラス)を当てることで敵を倒せるようにした
  7. 生産コストの実装(Costクラス)
  8. 操作モード(通常モード/購入モード/アップグレードモード)の切り替えを実装
  9. 敵を倒すと所持金が増えるようにした
  10. ゲーム状態(Wave開始待ち/メインゲーム/ゲームオーバーを実装)
  11. Waveパラメータの実装
  12. 砲台のアップブレードを実装

これらを簡単に説明していきます。

1. マップデータの作成(Tiled Map Editor)

マップデータの作成には、フリーのマップエディタである「Tiled Map Editor」を使用しました。
Tile Map Editorはこんな感じでマップを作れる、とてもナイスなツールです。
tiled.png

今回はマップが1つだけなので、マップは1枚の画像ファイルとして出力して使うことにしました(backオブジェクト)。もし色々なマップで遊ばせたいならマップデータを読み込んでチップを配置する処理の実装が必要になると思います。

2. 敵の移動経路の作成(Tiled Map Editor)

敵の移動経路もマップデータに配置しました。こんな感じです。
path.png

左端にある「S」が開始地点となるチップです。ここを基点として行き止まりまで進み、終端にたどり着いたらライフを減らすようにしています。

3. マップの読み込みを実装(TmxLoaderクラス / Layer2Dクラス)

Tiled Map Editorのマップデータ(*.tmx)は単なるXMLのファイルなので、XmlDocumentを使って読み込みを行います。

TmxLoader.cs
    // XML解析開始.
    XmlDocument xmlDoc = new XmlDocument();
    xmlDoc.LoadXml(tmx.text);
    XmlNodeList mapList = xmlDoc.GetElementsByTagName("map");
    foreach (XmlNode map in mapList)
    {
      XmlNodeList childList = map.ChildNodes;
      foreach (XmlNode child in childList)
      {
        if (child.Name != "layer") { continue; } // layerノード以外は見ない.
        ……(以下省略)

ただ、Unityは拡張子でテキストかどうかを判定しているため、そのままの拡張子(tmx)ではロードできません。そのため、MyAssetPostProcessorクラスで、.tmxを.xmlとしてリネームコピーする処理を行っています。

MyAssetPostProcessor.cs
    private static void OnPostprocessAllAssets(
        string[] importedAssets, 
        string[] deletedAssets, 
        string[] movedAssets, 
        string[] movedFromPath)
    {
        foreach (var importedAsset in importedAssets)
        {
            if(IsTmxFile(importedAsset))
            {
                // TMXファイルなので拡張子を*.xmlにしてコピー
                var newAsset = importedAsset.Replace(".tmx", ".xml");
                // 古いXMLは削除
                AssetDatabase.DeleteAsset(newAsset);
                if(AssetDatabase.CopyAsset(importedAsset, newAsset))
                {
                    // コピー実行
                    Debug.Log ("Copy: " + importedAsset + " -> " + newAsset);
                }
            }
        }
    }

そして、マップデータはレイヤーが複数重なった構造となっています。

layer.png

このレイヤーは単なる2次元配列の数値データなので、それをLayer2Dクラスに変換して、経路情報を取得しています。

4. 敵(Enemyクラス)の実装。移動経路データから敵の移動リストを実装

敵の移動経路は経路レイヤーに配置した「S(スタート地点)」から移動可能な経路をたどり、終端にたどりついたらライフを減らすようにしています。
移動経路は、Layer2Dクラスを元にList<Vec2D>(座標リスト)へ変換しています。この座標リストを先頭から見ることで敵を動かします。
そして、移動の実装には、Mathf.Leap を使っています。これは2つの値を線形補完する関数です。

leap.png

Enemy.cs
    // 速度タイマーに対応する位置に線形補間で移動する
    X = Mathf.Lerp(_prev.x, _next.x, _tSpeed / 100.0f);
    Y = Mathf.Lerp(_prev.y, _next.y, _tSpeed / 100.0f);

なお座標は、マップ座標系からUnityのワールド座標系に変換した上で補完します。

5. 砲台(Towerクラス)の実装。一番近くにいる敵を攻撃するようにする

配置した砲台は、敵を自動的に攻撃します。Mathf.DeltaAngle を使うと、簡単に角度差を求められます。なので、これを使って敵のいる方向に旋回するようにしました。
tower.png

Tower.cs
    // 敵への角度を取得
    float targetAngle = Util.AngleBetween(this, e);
    // 現在向いている角度との差を求める
    float dAngle = Mathf.DeltaAngle(Angle, targetAngle);
    // 差の0.2だけ回転する
    Angle += dAngle * 0.2f;

6. ショット(Shotクラス)を当てることで敵を倒せるようにした

次に、砲台が一定間隔でショットを撃ち、ショットを敵に当てると倒せるようにしました。
なお、ショットは撃つ間隔を制御するインターバル用のタイマーがないと、以下のように激しく連射してしまうので要注意です。

shot.png

7. 生産コストの実装(Costクラス)

生産コストはCostクラスに実装しています。

Cost.cs
  /// タワーを買うために必要なコストを取得する
  public static int TowerProduction()
  {
    // 生存数を取得
    int num = Tower.parent.Count();

    // タワー生産コスト= 8 * (1.3 ^ タワーの存在数)
    int basic = 8;
    float ratio = Mathf.Pow(1.3f, num);

    // 小数点は切り捨て
    return (int)(basic * ratio);
  }

基本コストを8として、1台増えるごとに、1.3倍ずつコストを上昇させるようにしています。Mathf.Powを使うと倍々となる計算が簡単にできるので便利です。

8. 操作モード(通常モード/購入モード/アップグレードモード)の切り替えを実装

操作モードは以下の3つとしました。

  • 初期状態(GameMgr.eSelNone)
  • 購入モード(GameMgr.eSelBuy) …… Buyボタンを押した
  • アップグレードモード(GameMgr.eSelUpgrade) …… 砲台をクリックした

Buyボタンを押すと購入モードとなり、砲台を配置すると所持金が減ります。配置済みの砲台をクリックするとアップグレードモードとなり、アップグレードする項目を選ぶモードとなります。

9. 敵を倒すと所持金が増えるようにした

Wave1〜4までは1体倒すと所持金が2増えるようにして、Wave5からは1ずつ増えるようにしています。

10. ゲーム状態(Wave開始待ち/メインゲーム/ゲームオーバーを実装)

ゲームを制御する状態は以下の3つとしています。

  • Wave開始待ち(GameMgr.eState.Wait)
  • メインゲーム(GameMgr.eState.Main)……Wave内の敵をすべて倒すと次のWaveに進む
  • ゲームオーバー(GameMgr.eState.Gameover)……ライフが0になったらこの状態になる

ゲーム開始直後はWave開始待ち状態となり、2秒経過でメインゲームへ遷移します。そして敵をすべて倒したら次のWaveに進みます。もしライフが0になったらゲームオーバー状態となります。

11. Waveパラメータの実装

WaveのパラメータはEnemyParamクラスに実装しています。

  • 敵のHP(EnemyParam.Hp)
  • 敵の移動速度(EnemyParam.Speed)
  • 敵の所持金(EnemyParam.Money)
  • 敵の出現数(EnemyParam.GenerationNumber)
  • 敵の出現間隔(EnemyParam.GenerationInterval)

Wave数が先に進むほど各パラメータが上昇します。例えば、HPはWaveが3つ上昇すると1つ増えます。

EnemyParam.cs
  /// 敵のHPを取得する
  public static int Hp()
  {
    // 1 + (Wave数 / 3)
    return 1 + (Global.Wave / 3);
  }

そして、Wave数に対応する敵のパラメータは以下のとおりです。

Wave数 HP 速度 所持金 出現数 出現間隔
1 1 3.1 2 6 1.5
2 1 3.2 2 7 1.5
3 2 3.3 2 8 1.5
4 2 3.4 2 9 1.5
5 2 3.5 1 19 1.5
6 3 3.6 1 11 1.5
7 3 3.7 1 12 1.5

タワーディフェンスに限りませんが、ゲームのパラメータを調整する場合、遊びながら調整することも大切ですが、一覧の表にして数値を確かめることも大切です。

12. 砲台のアップブレードを実装

砲台のアップグレードは以下の手順で実装しました。

  1. 砲台にレベルパラメータを追加
  2. レベルに対応する各パラメータの計算(TowerParamクラス)
  3. アップグレードコストの計算(Costクラス)

なにげに、この実装が一番大変かもしれません。アップグレード用のボタンを追加したりとか、選択した砲台のパラメータを表示したりとか、UI周りの制御や整合性を取るために試行錯誤が必要となります。

そして、砲台のレベルごとのパラメータとコストは以下のとおりです。

■射程範囲(Range)

Lv コスト 射程範囲(単位は1チップ=1)
1 10 1.5
2 15 2.0
3 22 2.5
4 33 3.0
5 50 3.5

■連射速度(Firerate)

Lv コスト 連射速度(単位は秒)
1 15 2.0
2 22 1.8
3 33 1.62
4 50 1.458
5 75 1.3122

■攻撃威力(Power)

Lv コスト 威力(単位はHP)
1 20 1
2 30 2
3 45 3
4 67 4
5 101 5

今回のタワーディフェンスは、序盤では攻撃威力(Power)パラメータをかなり優遇しています。パラメータが有効であるかを判定するためには、DPS(Damage Per Second)という指標を使うとよいです。これは「1秒あたりのダメージ量」を表す値です。例えば2秒間に1ダメージを与えることができれば、DPSは 1 / 2 = 0.5 となります。
Powerパラメータは、Lv1からLv2に上げるとDPSは2倍となりますが、他のパラメータをアップグレードしても2倍にはなりません。よって、Powerパラメータは有利なのです(まあ普通にゲームプレイすれば感覚的にわかると思いますが)。ただし、攻撃力をLv2からLv3にするとDPSは1.5倍、というようにレベルが上がるごとに効果が低減していくので、他のパラメータも上げたほうがDPS効率がよくなる可能性があります。そのあたりのコスト効率を試行錯誤するのが今回のゲームシステムの狙いとなります。またPowerについてはアップグレードコストを高めにしているので、そのあたりの計算も必要となりますね。

プロジェクトデータについて

プロジェクトデータですが、プロジェクトファイルやスクリプトについては、自由に使っていただいて構いません。ただし、画像データはマップチップ画像を「ぴぽや様」よりお借りしているので、そのまま流用することは禁止します。

宣伝 (2015/6/14 追記)

ここで簡単に紹介したタワーディフェンスの作り方を、より詳細に解説するKindle本の配信を開始しました。
unitytd.jpg

この本についての詳しい内容は、以下のページで紹介しているので、もし興味があればのぞいてみてください

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした