【Unity】RPGを作るチュートリアルその52 敵キャラクターの行動選択を行うクラス

【Unity】RPGを作るチュートリアルその52 敵キャラクターの行動選択を行うクラス

シンプルなRPGをUnityで作るチュートリアルシリーズの52回目です。

第51回では敵キャラクターの行動を決定するにあたって、敵キャラクターの戦闘中のステータスを保持するクラスを作成しました。

今回は敵キャラクターの行動を決定するクラスを作成していきます。

 

 

制作環境

MacBook Pro 2023 Apple M2 Max

Unity6 (6000.0.30f1) Silicon

 

作業内容と順序

シンプルなRPGを作る上でどんな作業が必要か、どんな順番で作っていくと良さそうか、別ページで検討しました。基本的にこの流れに沿って進めていきます。

 

チュートリアルの一覧

このシリーズ全体の一覧は以下のページにまとめています。

 

前回の内容

前回は敵キャラクターの行動を決定するにあたって、敵キャラクターの戦闘中のステータスを保持するクラスを作成しました。

 

敵キャラクターの行動選択を行うクラス

キャラクターと同様に、敵キャラクターについても戦闘中の行動を選択するようにしたいと思います。敵キャラクターの場合は、定義データで決めた行動の中から、優先度に応じて選択するようにします。

優先度や条件など、それぞれ計算を行なって、ターン内に選択されるものを決めていきたいと思います。

まず最初に計算するのは優先度で、優先度の高い行動順にリストを並べ替えてループを回します。そこからそれぞれの項目で条件を確認して、合致するものを選択していきます。合致するものがなければ通常攻撃を選択するようにしておきます。

今回のチュートリアルでは条件としてターン数、敵キャラクターの現在HPを実装していますが、他にも色々と条件にできるものがあるので、拡張していく際にはRPGの雰囲気や敵キャラクターの性格などを加味して行動を決めていくのも良いかと思います。

Projectウィンドウから「Assets/Scripts/Battle」のフォルダに移動し、MonoBehaviourのスクリプトファイルを作成します。名前は [EnemyCommandSelector] にしました。

スクリプトの作成
スクリプトの作成

 

作成した「EnemyCommandSelector」の中身は以下のように記載しました。

using System.Linq;
using UnityEngine;
namespace SimpleRpg
{
/// <summary>
/// 敵キャラクターのコマンドを選択するクラスです。
/// </summary>
public class EnemyCommandSelector : MonoBehaviour
{
/// <summary>
/// 戦闘に関する機能を管理するクラスへの参照です。
/// </summary>
BattleManager _battleManager;
/// <summary>
/// 戦闘中の敵キャラクターのデータを管理するクラスへの参照です。
/// </summary>
EnemyStatusManager _enemyStatusManager;
/// <summary>
/// 戦闘中のアクションを登録するクラスへの参照です。
/// </summary>
BattleActionRegister _battleActionRegister;
/// <summary>
/// 参照をセットします。
/// </summary>
/// <param name="battleManager">戦闘に関する機能を管理するクラスへの参照</param>
/// <param name="battleActionRegister">戦闘中のアクションを登録するクラスへの参照</param>
public void SetReferences(BattleManager battleManager, BattleActionRegister battleActionRegister)
{
_battleManager = battleManager;
_battleActionRegister = battleActionRegister;
_enemyStatusManager = _battleManager.GetEnemyStatusManager();
}
/// <summary>
/// 敵キャラクターのコマンドを選択します。
/// </summary>
public void SelectEnemyCommand()
{
foreach (var enemyStatus in _enemyStatusManager.GetEnemyStatusList())
{
if (enemyStatus.isDefeated || enemyStatus.isRunaway)
{
continue;
}
// 先頭のパーティキャラクターをターゲットにします。
int targetId = CharacterStatusManager.partyCharacter[0];
// 行動パターンに応じて敵キャラクターのコマンドを選択します。
EnemyActionRecord record = SelectActionFromRecords(enemyStatus.enemyData, enemyStatus.enemyBattleId);
if (record == null)
{
_battleActionRegister.SetEnemyAttackAction(enemyStatus.enemyBattleId, targetId, enemyStatus.enemyData);
continue;
}
switch (record.enemyActionCategory)
{
case EnemyActionCategory.Attack:
_battleActionRegister.SetEnemyAttackAction(enemyStatus.enemyBattleId, targetId, enemyStatus.enemyData);
break;
case EnemyActionCategory.Magic:
if (CanUseMagic(record, enemyStatus.enemyBattleId))
{
_battleActionRegister.SetEnemyMagicAction(enemyStatus.enemyBattleId, targetId, record.magicData.magicId, enemyStatus.enemyData);
}
else
{
_battleActionRegister.SetEnemyAttackAction(enemyStatus.enemyBattleId, targetId, enemyStatus.enemyData);
}
break;
default:
_battleActionRegister.SetEnemyAttackAction(enemyStatus.enemyBattleId, targetId, enemyStatus.enemyData);
break;
}
}
// 敵キャラクターのコマンド選択が完了したことを通知します。
_battleManager.OnEnemyCommandSelected();
}
/// <summary>
/// 行動パターンを選択します。
/// </summary>
/// <param name="enemyData">敵キャラクターのデータ</param>
/// <param name="enemyBattleId">敵キャラクターの戦闘中ID</param>
EnemyActionRecord SelectActionFromRecords(EnemyData enemyData, int enemyBattleId)
{
EnemyActionRecord record = null;
// 優先度の高い順に並び替えます。
var query = enemyData.enemyActionRecords.OrderByDescending(r => r.priority);
foreach (var actionRecord in query)
{
// 条件を確認し、合致している場合に行動パターンを選択します。
if (CheckCondition(actionRecord, enemyBattleId))
{
record = actionRecord;
break;
}
}
return record;
}
/// <summary>
/// 行動パターンの条件に合致しているか確認します。
/// Trueで合致しています。
/// </summary>
/// <param name="conditionRecords">条件のデータ</param>
/// <param name="enemyBattleId">敵キャラクターの戦闘中ID</param>
bool CheckCondition(EnemyActionRecord record, int enemyBattleId)
{
SimpleLogger.Instance.Log($"CheckConditionが呼ばれました。");
bool match = false;
foreach (var conditionRecord in record.enemyConditionRecords)
{
switch (conditionRecord.conditionCategory)
{
case ConditionCategory.Turn:
match = CheckTurnCondition(conditionRecord);
SimpleLogger.Instance.Log($"CheckTurnConditionの結果 : {match}");
break;
case ConditionCategory.HpRate:
match = CheckHpRateCondition(conditionRecord, enemyBattleId);
SimpleLogger.Instance.Log($"CheckHpRateConditionの結果 : {match}");
break;
}
}
return match;
}
/// <summary>
/// 行動パターンの条件に合致しているか確認します。
/// Trueで合致しています。
/// </summary>
/// <param name="conditionRecords">条件のデータ</param>
bool CheckTurnCondition(EnemyConditionRecord record)
{
return CompareValues(_battleManager.TurnCount, record.comparisonOperator, record.turnCriteria);
}
/// <summary>
/// HP残量の条件に合致しているか確認します。
/// Trueで合致しています。
/// </summary>
/// <param name="conditionRecords">条件のデータ</param>
/// <param name="enemyBattleId">敵キャラクターの戦闘中ID</param>
bool CheckHpRateCondition(EnemyConditionRecord record, int enemyBattleId)
{
var enemyStatus = _enemyStatusManager.GetEnemyStatusByBattleId(enemyBattleId);
int currentHp = enemyStatus.currentHp;
int maxHp = enemyStatus.enemyData.hp;
float hpRate = currentHp * 100f / maxHp;
SimpleLogger.Instance.Log($"HP残量の条件を確認します。 currentHp : {currentHp} || maxHp : {maxHp} || HP残量 : {hpRate}");
return CompareValues(hpRate, record.comparisonOperator, record.hpRateCriteria);
}
/// <summary>
/// 選択された魔法が使用可能か確認します。
/// Trueで使用可能です。
/// </summary>
/// <param name="record">行動パターンのデータ</param>
/// <param name="enemyBattleId">敵キャラクターの戦闘中ID</param>
bool CanUseMagic(EnemyActionRecord record, int enemyBattleId)
{
// 魔法データがnullの場合はfalseを返します。
if (record.magicData == null)
{
return false;
}
var enemyStatus = _enemyStatusManager.GetEnemyStatusByBattleId(enemyBattleId);
int currentMp = enemyStatus.currentMp;
int mpCost = record.magicData.cost;
bool canUse = currentMp >= mpCost;
SimpleLogger.Instance.Log($"{record.magicData.magicName} の魔法が使用できるか確認します。 currentMp : {currentMp} mpCost : {mpCost} canUse : {canUse}");
return canUse;
}
/// <summary>
/// 演算子に応じた条件を満たしているか確認します。
/// </summary>
/// <param name="targetValue">対象の値</param>
/// <param name="comparisonOperator">演算子</param>
/// <param name="criteria">条件の値</param>
bool CompareValues(int targetValue, ComparisonOperator comparisonOperator, int criteria)
{
switch (comparisonOperator)
{
case ComparisonOperator.Equals:
return targetValue == criteria;
case ComparisonOperator.NotEquals:
return targetValue != criteria;
case ComparisonOperator.GreaterThan:
return targetValue > criteria;
case ComparisonOperator.GreaterOrEqual:
return targetValue >= criteria;
case ComparisonOperator.LessThan:
return targetValue < criteria;
case ComparisonOperator.LessOrEqual:
return targetValue <= criteria;
default:
return false;
}
}
/// <summary>
/// 演算子に応じた条件を満たしているか確認します。
/// </summary>
/// <param name="targetValue">対象の値</param>
/// <param name="comparisonOperator">演算子</param>
/// <param name="criteria">条件の値</param>
bool CompareValues(float targetValue, ComparisonOperator comparisonOperator, float criteria)
{
switch (comparisonOperator)
{
case ComparisonOperator.Equals:
return targetValue == criteria;
case ComparisonOperator.NotEquals:
return targetValue != criteria;
case ComparisonOperator.GreaterThan:
return targetValue > criteria;
case ComparisonOperator.GreaterOrEqual:
return targetValue >= criteria;
case ComparisonOperator.LessThan:
return targetValue < criteria;
case ComparisonOperator.LessOrEqual:
return targetValue <= criteria;
default:
return false;
}
}
}
}

行動選択の完了を通知するために「BattleManager」への参照、現在のHP/MPや戦闘中IDを確認するために「EnemyStatusManager」への参照、行動を登録するために「BattleActionRegister」への参照をそれぞれ保持するようにして、SetReferences()のメソッドで渡しています。

敵キャラクターの行動選択が必要になったタイミングでSelectEnemyCommand()が外部から呼ばれ、「EnemyStatusManager」のリストに登録されている敵キャラクターのうち、撃破フラグ、逃走フラグが共にfalseのキャラクターについて行動を決めていきます。

どの行動を選択するかは、SelectActionFromRecords()のメソッドの中で決めていきます。優先度に応じて並べ替えた後、CheckCondition()のメソッドを呼んで条件を確認していきます。このメソッド内ではさらに条件の種類に応じて処理を分けています。

CheckTurnCondition()ではターン数の条件を比較し、CheckHpRateCondition()ではHP残量(割合)の条件を比較しています。また、行動選択で魔法が選ばれた時にも、CanUseMagic()のメソッドで消費MPと現在MPを比較して使用できるかどうかを確認しています。

CompareValues()のメソッドは比較用のメソッドです。int型での比較、float型での比較ができるようにオーバーロードを用意しています。

 

既存のクラスの修正

新しく作ったクラスと繋ぎ合わせるため、既存のクラスも修正していきます。対象は、

  • BattleManager

です。

 

BattleManagerの修正

「BattleManager」では参照用のフィールドを追加するのと、初期化処理、コールバック用メソッドの追加を行います。

/// <summary>
/// 戦闘中の敵キャラクターの管理を行うクラスへの参照です。
/// </summary>
[SerializeField]
EnemyStatusManager _enemyStatusManager;
/// <summary>
/// 敵キャラクターのコマンドを選択するクラスへの参照です。
/// </summary>
[SerializeField]
EnemyCommandSelector _enemyCommandSelector;
/// <summary>
/// 戦闘中のアクションを登録するクラスへの参照です。
/// </summary>
[SerializeField]
BattleActionRegister _battleActionRegister;

前回追加した「_enemyStatusManager」の下に追加しましょう。また、行動を登録するためのクラスへの参照もここで追加しちゃいましょう。

 

/// <summary>
/// 戦闘の開始処理を行います。
/// </summary>
public void StartBattle()
{
SimpleLogger.Instance.Log("戦闘を開始します。");
GameStateManager.ChangeToBattle();
SetBattlePhase(BattlePhase.ShowEnemy);
_battleWindowManager.SetUpWindowControllers(this);
var messageWindowController = _battleWindowManager.GetMessageWindowController();
messageWindowController.HidePager();
_enemyCommandSelector.SetReferences(this, _battleActionRegister);
_characterMoverManager.StopCharacterMover();
_battleStarter.StartBattle(this);
}

次にStartBattle()の中で初期化処理を追加します。参照をここでアサインするようにしましょう。

 

/// <summary>
/// メッセージウィンドウでメッセージの表示が完了した時のコールバックです。
/// </summary>
public void OnFinishedShowMessage()
{
switch (BattlePhase)
{
case BattlePhase.ShowEnemy:
SimpleLogger.Instance.Log("敵の表示が完了しました。");
StartInputCommandPhase();
break;
}
}
/// <summary>
/// コマンド選択が完了した後の処理です。
/// </summary>
void PostCommandSelect()
{
SimpleLogger.Instance.Log("敵のコマンド入力を行います。");
_enemyCommandSelector.SelectEnemyCommand();
}
/// <summary>
/// 敵キャラクターのコマンドが選択された時のコールバックです。
/// </summary>
public void OnEnemyCommandSelected()
{
SimpleLogger.Instance.Log("敵味方の行動が決まったので実際に行動させます。");
}

ファイルの末尾、以前作成したOnFinishedShowMessage()の下に、PostCommandSelect()とOnEnemyCommandSelected()の2つのメソッドを追加します。

PostCommandSelect()は味方キャラクターの行動が選択された後に呼ばれる想定のメソッドです。味方の行動を登録する部分は、行動処理のクラスが実装できてから作っていきましょう。

OnEnemyCommandSelected()のメソッドは、今回作成した「EnemyCommandSelector」から完了を通知してもらうためのメソッドです。敵味方の行動が決まったら、実際に処理していく流れになります。

 

スクリプトのアタッチ

スクリプトを保存したら、ゲームオブジェクトの作成してスクリプトをアタッチしましょう。

 

EnemyCommandSelectorのアタッチ

Hierarchyウィンドウから「BattleParent」の下にある「Managers」のゲームオブジェクトを選択します。「Managers」の下に、空のゲームオブジェクトを作成しましょう。名前は [EnemyCommandSelector] にしました。

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

 

Inspectorウィンドウで今回作成した「EnemyCommandSelector」をアタッチします。

スクリプトのアタッチ
スクリプトのアタッチ

 

BattleManagerへのアサイン

続いて、「BattleManager」に「EnemyCommandSelector」と「BattleActionRegister」への参照をアサインします。

参照のアサイン
参照のアサイン

 

動作確認

今回も戦闘を開始してエラーがないことだけ確かめておきましょう。

 

今回のブランチ

 

まとめ

今回は敵キャラクターの行動を決定するクラスを作成しました。今回のチュートリアルでは行動の条件を少なくしていますが、チュートリアル後に作りたいRPGに応じて拡張していくのも良いかと思います。

次回は行動選択を行うクラスを実装していきます。全体の処理を管理するクラスについては作成済みのため、登録以外の処理について実装していきます。その後、個別の行動を処理するクラスの作成に進みたいと思います。

     

ゲーム開発の攻略チャートを作りました!

CTA-IMAGE

「ゲームを作ってみたいけど、何から手を付けていいか分からない!」


そんなお悩みをお持ちの方向けに、todoがアプリをリリースした経験を中心に、ゲーム作りの手順や考慮すべき点をまとめたe-bookを作成しました。ゲーム作りはそれ自体がゲームのように楽しいプロセスなので、「攻略チャート」と名付けています。


ゲームを作り始めた時にぶつかる壁である「何をしたら良いのか分からない」という悩みを吹き飛ばしましょう!