🤼‍♂️

【Unity】敵と味方とオブジェクト指向プログラミング

2020/11/19に公開1

はじめに

友人のデザイナーがUnityで自作ゲームの制作を始めました。
で、プログラミングで分からない所があるので教えてほしいという相談を受けたのですが
なるほど初心者はこういう所で躓くのかと思うところがあったのでこれをネタにして記事にしようと思います。

今回はプログラム未経験者へ向けての内容であるため専門用語はできるだけ使わずに簡単に説明することを心がけています。
本職プログラマーの方はツッコミたくなる内容があるかもしれませんがご了承ください。

要件

相談を受けた内容は以下のような感じです。

  • ゲームのジャンル
     タワーディフェンス
  • どこまで作ったか
     「目的地に向かって移動して敵と接触したらダメージを受けて死ぬ」ところまで
  • 何に困っているのか
    1. 敵のパラメータを持ってきてダメージ計算をしたい
    2. ↑の処理を敵でも同じことがしたい
    3. 目的地を操作で指定してから動かすには?
    4. これらの処理を汎用性高くまとめるには?

敵と味方

まずは1と2から見てみましょう。
敵のパラメーターを持ってきたい、敵でも同じ処理をしたい、という事から「味方のユニットの挙動は作ったけど敵側も作るの面倒くさい」という状況である事が予想されます。
これに関して答えは簡単です。

敵と味方は分けない

将棋を例にとるとわかりやすいと思いますが、将棋というゲームは倒した駒を自分の駒として使うことができますね。
敵・味方というものは「駒」という機能の一要素でしかないのです。
つまり、実装すべきは「味方」ではなく「駒」になります。

駒の実装

既に味方は出来ているというのですから考え方を少し変えるだけですぐに実装可能です。
味方は「自軍に所属する駒」ですから所属するグループを駒の機能に持たせればよいです。

Unit.cs
public class Unit : MonoBehaviour
{
    // 所属グループ
    public enum GroupType
    {
        Player,
        Enemy
    }
    [SerializeField]GroupType group;

    // 敵かどうか調べる
    public bool IsEnemy(Unit targetUnit)
    {
        return group != targetUnit.group;
    }
}

これでGroupTypeをPlayerに設定した駒は味方に、Enemyに設定した駒は敵になります。

敵のパラメータを持ってきてダメージ計算をしたい

既に敵・味方の垣根はなくなっているので後はダメージ計算だけになります。
ゲーム仕様によって必要なパラメーターは変わってきますが最低限HP、攻撃力、防御力があればいいでしょう。

Unit.cs
[SerializeField]int maxHp;            // 最大HP
[SerializeField]int attack;           // 攻撃力
[SerializeField]int defense;          // 防御力

int currentHp;        // 現在のHP

ダメージ計算は単純に攻撃力から防御力を引いたものにしました。
ゲーム仕様によって変えてください。
ダメージは0以下にならない、HPは0以下にならないようにしています。
敵でも味方でもAttackメソッドを呼べばいいようになっているので2.の 「↑の処理を敵でも同じことがしたい」という要望は既に満たされています。

Unit.cs
// targetのユニットに対して攻撃する
void Attack(Unit target)
{
    if(!IsEnemy(target))
    {   // 敵じゃないユニットには攻撃しない
        return;
    }
    // 自分の攻撃力から相手の防御力を引いたものをダメージとする(0未満にはならない)
    int damage = Mathf.Max(attack - target.defense, 0);
    // 攻撃相手はダメージを受ける
    target.GetDamage(damage);
}

// ダメージを受ける
void GetDamage(int damage)
{
    // HPが0未満にならないようにダメージを受ける
    currentHp = Mathf.Max(currentHp - damage, 0);
    if(currentHp == 0)
    {
        Debug.Log("撃破されました");
    }
}

将来出てくるであろう問題

敵と味方は分けないと言いましたが、「プレイヤーのユニットはレベルや装備など様々な要因からパラメーターが算出されるから同じものにできないじゃないか!」と未来のあなたはきっとそう言うでしょう。
それを解決するのが「継承」になります。
継承を深堀りすると本が一冊書けるので今回説明は避けますがその時が来たらまた聞いてくれれば解説します。

バトルマネージャー

次に3と4を見てみましょう。
プレイヤーが今何を選択しているのか、何ができるのか、ゲームは今どういう状況なのか。
それらをまとめて管理するマネージャーを作ることをお勧めします。
マネージャーはゲームの中に一つだけ存在し、どこからでもアクセスでき、ゲームのことを何でも知っている、そんな存在です。

シングルトン

プログラム中一つしか存在しない(できない)ものを専門用語でシングルトンと言います。
1つしか作らなければいい話ですが、不具合防止のため2つ以上作れないようにするのが定番となっています。

BattleManager.cs
public class BattleManager : MonoBehaviour
{
    static BattleManager instance = null;

    private void Awake()
    {
        if(instance == null)
        {
            instance = this;
        }
        else
        {
            Debug.LogError("BattleManagerはシングルトンです。シーンに複数配置できません。");
        }
    }

    private void OnDestroy()
    {
        instance = null;
    }

    public static BattleManager Get()
    {
        return instance;
    }

シングルトンの書き方は色々ありますが、プログラム未経験者でもわかりやすいのはこんな感じでしょうか。
Awakeでstaticな自分自身へのメンバーへ保存し、既に存在している場合は2つ目ということなのでエラーとします。
これでBattleManager.Get() を介することでどこからでもBattleManagerへアクセスすることができます。

staticとは

簡単に言うと全てのインスタンスで共有するものに対して付ける修飾子です。
趣味で日曜プログラミング程度の人は「そういうもの」でいいと思いますが
将来プログラミングで仕事をしたい人は必ず理解してください。
https://docs.microsoft.com/ja-jp/dotnet/csharp/language-reference/keywords/static

目的地を操作で指定してから動かすには

BattleManagerに自軍の基地と目的地のパラメーターを追加し、自軍の基地に目的地が設定されたユニットを配置するメソッドを作ります。

BattleManager.cs
public GameObject playerBase;   // 自軍の基地
public Vector2 targetPosition;  // 目的地

// プレイヤーのユニットを配置するメソッド
public void PlacePlayerUnit(GameObject unitPrefab)
{
    // 駒を生成する
    GameObject go = Instantiate(unitPrefab, transform);
    // プレイヤーの基地から出発
    go.transform.position = playerBase.transform.position;
    // ユニットの目標を設定する
    Unit unit = go.GetComponent<Unit>();
    unit.SetTargetPosition(targetPosition);
}

目的地を変更するにはBattleManagerのtargetPositionを変更してあげます。

BattleManager.Get().targetPosition = new Vector2(100, 200);

ユニットを配置するにはあらかじめプレハブ化しておいたユニットを読み込んでPlacePlayerUnitメソッドに渡してあげます。
歩兵、騎兵、槍兵などのバリエーションを作っておけば読み込むプレハブを切り替えるだけで配置する兵種を変えることができます。

GameObject go = Resources.Load<GameObject>("PlayerUnit");
BattleManager.Get().PlacePlayerUnit(go);

最後に

プログラム未経験者への説明という事でどこまで書くかかなり悩みました。
説明したいことは山ほどあるけど全部なんて書いてられないし焦点がボケるので要件のみに絞って最低限の説明に抑えたつもりです。
移動に関する部分など省いている箇所もあるのでこの記事のコードがそのまま動くわけではないのでご注意ください。
この記事が少しでも自作ゲーム制作の役に立ってくれれば幸いです。

Discussion

neko7neko7

タワーディフェンスを作ろうを購入したものです。
にゃんこ風サンプルで敵のエネミーが槍兵しか出現しないのですが原因をご教示いただけますとありがたいです。何卒よろしくお願い申し上げます。