Unityに関連する記事です

ボスバトル


 ボスに限らずですが、ある一定の行動パターンがあって、その中からランダムな行動を行う分岐処理があります。

 具体的な行動内容以外についての、分岐処理の実装例を提示します。行動内容は自分で考えて書きましょう

 下記サンプル動画では、白い Cube がボス役です。


<実装動画>
動画ファイルへのリンク



 この挙動が正常に動作することが確認できたら、実際に自分で3Dモデルを配置し、アニメーションなどの設定と
行動内容の処理を追加することで、分岐に応じた行動を実行させることが出来ます。


<完成例>
動画ファイルへのリンク



ゲームオブジェクトの作成


 任意のアセットなどから、ボス役となるゲームオブジェクトをシーンに配置し、自分のゲームにおいて必要な設定を行います(コライダーなど)
この教材では仮に Cube ゲームオブジェクトをボスとして見立てて利用しています。

 また、ボスの移動先として、Create Empty を行って空のゲームオブジェクトを複数作成し、それをさらに、フォルダ役のゲームオブジェクトにまとめて置きます。
この地点内のいずれかの位置へランダムへ移動するように想定しています。
Transform コンポーネントしか持たないゲームオブジェクトには、ラベルを設定することで Sceneビューで可視化出来るようになります。


ヒエラルキー画像



Scene ビュー画像(白い Cube がボス役。他のラベルのあるものが移動先)




 フォルダ役のゲームオブジェクトの位置については、ボスの位置と同じにしておくと、他の位置を決める際に相対的に考えることができて便利です。


インスペクター画像(フォルダ役)




サンプルコード


 ボスのランダム行動と分岐処理の実装例です。

 実際の行動内容については、それぞれのゲームに適した内容で実装をおこなってください。
ここでは処理の確認のため、Debug.Log メソッドのみ記述しています。


using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Linq;
using DG.Tweening;

/// <summary>
/// ボスの行動パターン
/// </summary>
public enum BossActions 
{
    MoveToPosition,
    AttackA,
    AttackB,
    Idle,
    MoveAndAttack
}

/// <summary>
/// ボスの行動パターンごとの重み付け
/// </summary>
[System.Serializable]
public class BossActionWeight
{
    public BossActions Action;
    public int Weight;
}

public class BossBattle : MonoBehaviour
{
    [SerializeField, Header("移動速度")] 
    private float moveSpeed = 10f;

    [SerializeField, Header("次回の行動までの待機時間")] 
    private  float coolDownTime = 5f;
    
    [SerializeField, Header("移動先となる地点")] 
    private Transform[] targetTrans;
    
    [SerializeField, Header("体力")] 
    private  float health;
    
    [SerializeField, Header("移動時のアニメ設定")] 
    private Ease moveEase = Ease.Linear;
    
    [SerializeField, Header("行動パターンごとの重み付け")]
    private List<BossActionWeight> ActionWeights;
    
    [SerializeField, Header("通常の行動パターン")]
    private BossActions[] NormalActionPattern;
    
    [SerializeField, Header("ヘルスが半分以下になった時の行動パターン")]
    private BossActions[] HalfHealthActionPattern;
    
    private bool inCoolDown = false;
    private float initialHealth;
    
    private BossActions currentAction;  // 現在の行動
    public BossActions CurrentAction => currentAction;  // プロパティ
    

    void Start()
    {
        // ActionWeights 変数がインスペクターから初期化されていない(登録されていない)場合、List を初期化する
        if(ActionWeights == null || ActionWeights.Count == 0)
        {
            ActionWeights = new List<BossActionWeight>
            {
                new BossActionWeight {Action = BossActions.MoveToPosition, Weight = 1},
                new BossActionWeight {Action = BossActions.AttackA, Weight = 1},
                new BossActionWeight {Action = BossActions.AttackB, Weight = 1},
                new BossActionWeight {Action = BossActions.Idle, Weight = 1},
                new BossActionWeight {Action = BossActions.MoveAndAttack, Weight = 0}
            };
        }

        initialHealth = health;
        
        // ボスの行動開始
        StartCoroutine(BossBehavior());
    }
    
    /// <summary>
    /// ボスの行動
    /// </summary>
    /// <returns></returns>
    private IEnumerator BossBehavior()
    {
        while(true) // Keep this loop running as long as the boss is alive.
        {
      // ヘルスが残っていない場合、行動を終了
            if(health <= 0) 
            {
                break; // Stop the loop if the boss is defeated.
            }

      // ランダムな行動を重み付けしている中から1つ選択
            currentAction = GetRandomActionByWeight();

      // 選択された行動を実行
            switch(currentAction)
            {
                case BossActions.MoveToPosition:
                    // 登録されている位置の中で、ランダムな位置に移動
                    MoveToPosition(targetTrans[Random.Range(0, targetTrans.Length)].position);
                    break;

                case BossActions.AttackA:
                    AttackA();
                    break;

                case BossActions.AttackB:
                    AttackB();
                    break;

                case BossActions.Idle:
                    Idle();
                    break;

                case BossActions.MoveAndAttack:
                    MoveAndAttack(targetTrans[Random.Range(0, targetTrans.Length)].position);
                    break;
            }

            yield return new WaitForSeconds(coolDownTime);
        }
    }
    
    /// <summary>
    /// 重み付けを利用した行動の決定
    /// </summary>
    /// <returns></returns>
    private BossActions GetRandomActionByWeight()
    {
        // 現在のライフの残数に基づいて、適切な行動パターンを選択
        BossActions[] actionPattern = health <= initialHealth / 2 ? HalfHealthActionPattern : NormalActionPattern;

        // その行動パターンに含まれる行動のみの重みを合計
        int totalWeight = ActionWeights.Where(a => actionPattern.Contains(a.Action)).Sum(a => a.Weight);

        // ランダムな行動を選ぶための乱数を取得(対象となる選択肢は選択した行動パターンの行動だけに限定)
        int randomValue = Random.Range(0, totalWeight);
        int weightSum = 0;

        // 乱数から今回の行動を確定(対象となる選択肢は選択した行動パターンの行動だけに限定)
        for (int i = 0; i < ActionWeights.Count; i++) {
            if (!actionPattern.Contains(ActionWeights[i].Action)) {
                continue;
            }

            weightSum += ActionWeights[i].Weight;

            if (randomValue < weightSum) {
                return ActionWeights[i].Action;
            }
        }

        return BossActions.Idle; // Default return value, in case something goes wrong.
    }
    
    private void MoveToPosition(Vector3 target)
    {
        transform.DOMove(target, moveSpeed).SetEase(moveEase).SetSpeedBased();

        Debug.Log("MoveToPosition");
    }

    private void AttackA()
    {
        // TODO Implement AttackA here
        
        Debug.Log("AttackA");
    }

    private void AttackB()
    {
        // TODO Implement AttackB here
        
        Debug.Log("AttackB");
    }

    private void Idle()
    {
        // TODO Idle behavior, do nothing
        
        Debug.Log("Idle");
    }

    private void MoveAndAttack(Vector3 target)
    {
        MoveToPosition(target);
        AttackA();
        
        // TODO Implement Attack here
        
        Debug.Log("MoveAndAttack");
    }
}



処理の解説


 ボスの行動パターンをEnumで BossActions と定義し、行動パターンの種類を列挙子として登録します。

/// <summary>
/// ボスの行動パターン
/// </summary>
public enum BossActions 
{
    MoveToPosition,
    AttackA,
    AttackB,
    Idle,
    MoveAndAttack
}

 他にも行動の種類を増減したい場合には、まずはこちらに情報を登録します。

 この BossActions のうち1つの値を、変数として保持します。
これがボスの挙動制御のベースとなります。

public BossActions currentAction;  // 現在の行動



 次に、各アクションに対応する重みを保持するクラスを作成し、そのクラスのインスタンスをリストで保持します。

/// <summary>
/// ボスの行動パターンごとの重み付け
/// </summary>
[System.Serializable]
public class BossActionWeight
{
    public BossActions Action;
    public int Weight;
}


// 保持するための変数
[SerializeField, Header("行動パターンごとの重み付け")]
private List<BossActionWeight> ActionWeights;

 変数には SerializeField属性を付与していますので、この List はインスペクターから設定が可能です。
また BossActionWeight クラスにも System.Serializable属性が付与されているため、クラス内部の情報もインスペクターから設定が可能です。

 下記のサンプル画像のように利用します。
BossActions(Enum) の登録順は列挙子の順番通りでなくても問題ありませんが、見やすさを考慮して、同じ並び順にしています。
同じ列挙子を指定しないようにしてください。


インスペクター画像



 この ActionWeights(List) を利用することで、LINQを使用して特定の条件を満たす重みだけを合計したり、特定の行動を抽出することができます。



 最後にボスの行動をコントロールするためのクラスを定義します。

 各種の変数は、任意に増減してください。
今回は、通常の行動パターンと、ヘルスが半分以下になった時の行動パターンを、
それぞれNormalActionPatternとHalfHealthActionPattern配列で保持しています。
これにより、Unityのインスペクターから直接、どの行動がどの状態で可能かを設定することができます。

[SerializeField, Header("通常の行動パターン")]
private BossActions[] NormalActionPattern;
    
[SerializeField, Header("ヘルスが半分以下になった時の行動パターン")]
private BossActions[] HalfHealthActionPattern;

 下記のサンプル画像のように、インスペクターから登録して利用します。
登録する数や順番などは任意です。ここでも同じ列挙子は登録しないようにしてください。


インスペクター画像




 Start メソッドでは、ActionWeights(List) がもしインスペクタから初期化されていない場合、各行動ごとにデフォルトの重みを設定します。
他にも設定があれば Start メソッド内で設定をします。
それらが終了してから、コルーチンメソッドである BossBehavior メソッドを実行します。

 BossBehavior メソッドは while 文によってループ処理が行われており、ボスのヘルスが 0 になるまで、このループ処理が繰り返されます。
そして、一定時間ごとに、ボスが行動するロジックが組まれています。

 GetRandomActionByWeight()メソッドでは、まず現在のヘルスに基づいて適切な行動パターンを選択します。
次に、その行動パターンに含まれる行動のみの重みを合計します。そして、ランダムな行動を選びますが、その際に選択肢は選択した行動パターンの行動だけに限定します。

 その後、switch 文により、選択された行動に応じたメソッドを実行することで、ボスの行動を行います。
移動の処理については、DOTween を利用した処理を仮実装しています。targetTrans 配列変数に登録してあるいずれか1つの位置をランダムで選択して移動します。

[SerializeField, Header("移動先となる地点")] 
private Transform[] targetTrans;

// 選択された行動を実行
switch(currentAction)
{
  case BossActions.MoveToPosition:
        // 登録されている位置の中で、ランダムな位置に移動
        MoveToPosition(targetTrans[Random.Range(0, targetTrans.Length)].position);
        break;

 どの変数がどの処理に対して、どのように利用されているかを把握しておくことが大切です。



 移動処理には DOMove メソッドに加えて、SetEase メソッドと SetSpeedBase メソッドをオプションで付与しています。
SetEase メソッドの引数には変数を指定することで、インスペクターから設定が可能です。ゲーム画面を見ながら、移動のアニメを調整しましょう。
また、SetSpeedBase メソッドを付与することで、DOMove メソッドの第2引数の指定が「時間」ではなく「速度」として利用されるようになります。
そのため、移動先となる目標地点の遠近による速度の変化を排除することが出来ます。

[SerializeField, Header("移動速度")] 
private float moveSpeed = 10f;

[SerializeField, Header("移動時のアニメ設定")] 
private Ease moveEase = Ease.Linear;


private void MoveToPosition(Vector3 target)
{
    transform.DOMove(target, moveSpeed).SetEase(moveEase).SetSpeedBased();

    Debug.Log("MoveToPosition");
}


参考サイト
Qiita @BEATnonanka 様
DOTween完全に理解するその7 オプション編

ハルシオンブログ 様
Dotweenでオブジェクトを移動したりする際に、時間でなく速度で動かす方法



 最後に指定された待機時間に入ってから、while 文が最初に戻り、再度、ボスの行動をランダムで決定し、行動を繰り返します。


設定


 ボスのゲームオブジェクトにスクリプトをアタッチします。


インスペクター画像(設定前)





 各配列と List を初期化し、任意の情報を設定します。

 ボスの移動先については、この例では3箇所を指定しています。

 BossActionWeight の Weight の値も任意です。各 Enum より重複しないように1つを選択し、値を設定します。
この値はパーセント表示ではありません。各 Weight の合計値を 100 としてみて重み付けで計算するため、それぞれの値は大きいほど選択されやすくなります。


インスペクター画像(設定後)




ゲームを実行して動作を確認する


 ソースコードの処理を見直して、どういう処理が書かれているのか、また、ゲームを実行した際には
どういった挙動が想定されるのかを理解してから、ゲームを実行するようにしましょう。

 デバッグを行う際には事前の理解が重要です。ただゲームが動けばいい、というものではありません。


<確認動画>
動画ファイルへのリンク




 以上になります。
このサンプルコードをベースに改良して、自分だけのデザインを行ってみましょう。



 以上でこの手順は終了です。

 次は 応用2 ーカットシーンの実装例  です。

コメントをかく


「http://」を含む投稿は禁止されています。

利用規約をご確認のうえご記入下さい

Menu



技術/知識(実装例)

2Dおはじきゲーム(発展編)

2D強制横スクロールアクション(発展編)

3Dダイビングアクション(発展編)

2Dタップシューティング(拡張編)

レースゲーム(抜粋)

2D放置ゲーム(発展編)

3Dレールガンシューティング(応用編)

3D脱出ゲーム(抜粋)

2Dリアルタイムストラテジー

2Dトップビューアドベンチャー(宴アセット使用)

3Dタップアクション(NavMeshAgent 使用)

2Dトップビューアクション(カエルの為に〜、ボコスカウォーズ風)

VideoPlayer イベント連動の実装例

VideoPlayer リスト内からムービー再生の実装例(発展)

AR 画像付きオブジェクト生成の実装例

AR リスト内から生成の実装例(発展)

private



このサイト内の作品はユニティちゃんライセンス条項の元に提供されています。

管理人/副管理人のみ編集できます