今回から戦闘でのキャラクターの行動処理を作成していきます。
キャラクターの行動処理は最低限の機能に絞って作成していますが、アイテムコマンドを作った時と同じような面倒さと大変さがあります。(^_^;)
覚悟しておいてください。(‘◇’)ゞ
今回はユニティちゃんのRPGの戦闘シーンの敵の攻撃ターンを作成していきたいと思います。
前回は戦闘シーンで使用するキャラクターのスキルデータを作成しました。

ユニティちゃんのRPGを作ってみようの他の記事は

から見ることが出来ます。
コマンド式の戦闘では行動をするキャラクターを順番に選び、敵であれば何らかのアルゴリズムに従って行動をし、味方であればそのキャラクターが持っているスキルに応じて選択肢をユーザーが選んで行動をする必要があります。
敵のターンでのアルゴリズム、味方のターンでの選択肢の表示や実行する処理を一遍に作成すると大変なので、今回は敵のターンでのアルゴリズムを作成していきます。
アルゴリズムと言うと難しく感じますが、要は敵にどのように行動させるか?という方法を考えます。
最初の数ターンは直接攻撃のみ→その後、数ターンは魔法を使って行動→敵の仲間の体力が減っていたら回復魔法を使う
というように行動パターンを作成する必要があります。
細かく攻撃パターンを作成すると大変なので、今回は敵が持っているスキルの中からランダムに一つを選び、その行動を行うようにします。
持っているスキルの中からランダムな一つを選ぶだけなので非常に頭の悪い行動になりますね・・・・(^_^;)
戦闘管理をするBattleManagerスクリプトに処理を追加する
戦闘の順番や敵の起こす行動の決定方法、味方キャラクターの選択肢の表示と選択時の処理を行うスクリプトは

で作成したBattleManagerスクリプトなので、ここに処理を追加していきます(一部変更している可能性があります)。
敵の行動パターンは戦闘用の敵のプレハブ等に別スクリプトとして取り付けて、そこに行動パターンを設定しておいてBattleManagerで敵固有の行動パターンを取得し行動を起こさせる方が敵によって行動パターンを変える事が出来ます。
しかしそれを行うと大変なのでBattleManagerスクリプト内で敵の行動をランダムに選んで実行するという方法にします。
BattleManagerスクリプトは長いので少しずつ記述していきます。
フィールド宣言の追加
まずはフィールドの宣言を追加します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 | using System.Collections; using System.Collections.Generic; using UnityEngine; using System.Linq; using UnityEngine.EventSystems; using UnityEngine.UI; public class BattleManager : MonoBehaviour { public enum CommandMode { SelectCommand, SelectDirectAttacker, SelectMagic, SelectMagicAttackTarget, SelectUseMagicOnAlliesTarget, SelectItem, SelectRecoveryItemTarget } // 現在戦闘に参加している全キャラクター private List<GameObject> allCharacterInBattleList = new List<GameObject>(); // 現在戦闘に参加している味方キャラクター private List<GameObject> allyCharacterInBattleList = new List<GameObject>(); // 現在戦闘に参加している敵キャラクター private List<GameObject> enemyCharacterInBattleList = new List<GameObject>(); // 現在の攻撃の順番 private int currentAttackOrder; // 現在攻撃をしようとしている人が選択中 private bool isChoosing; // 戦闘が開始しているかどうか private bool isStartBattle; // 戦闘シーンの最初の攻撃が始まるまでの待機時間 [SerializeField] private float firstWaitingTime = 3f; // 戦闘シーンのキャラ移行時の間の時間 [SerializeField] private float timeToNextCharacter = 1f; // 待ち時間 private float waitTime; // 戦闘シーンの最初の攻撃が始まるまでの経過時間 private float elapsedTime; // 戦闘が終了したかどうか private bool battleIsOver; // 現在のコマンド private CommandMode currentCommand; } |
CommandModeは味方キャラクターのターンの時に設定する現在何をしているか?を表す列挙型で敵のターンの時は使用しませんが、先に作っておきます。
battleDataは戦闘で使用するデータで以前に作成したAssets/RPG/Data/Status/Battle/BattleDataをインスペクタで設定します。
battleBasePositionは以前作成したキャラクターを配置する場所の元となるゲームオブジェクトのBattleBasePositionを設定します。
allCharacterInBattleListは現在戦闘に参加している味方と敵のキャラクターのリストです。
allyCharacterInBattleListは現在戦闘に参加している味方キャラクターのリストです。
enemyCharacterInBattleListは現在戦闘に参加している敵のキャラクターのリストです。
これらのリストは戦闘開始時にBattleDataからデータを取得してリストを作成します。
currentAttackOrderは現在ターンが回ってきているキャラクターの番号を入れます。
isChoosingは現在ターンが回ってきている人が行動を選択しているかどうか。
isStartBattleは戦闘が開始しているかどうか
firstWaitingTimeは戦闘が開始してから最初のターンのキャラクターが行動を起こすまでの間隔時間を設定します。
timeToNextCharacterは前のターンのキャラクターの行動が終わってから次のキャラクターが行動出来るまでの間隔時間です。
waitTimeは待ち時間を入れます(waitTimeに設定するのはfirstWaitingTimeかtimeToNextCharacterのどちらかです)。
elapsedTimeは間隔時間までの計測した時間を入れるフィールドです。
battleIsOverは戦闘が終了しているかどうかです。
currentCommandは現在のコマンドモードを入れます。
いっぱいフィールドがあって嫌になりますね・・・・・(^_^;)
Startメソッド
StartメソッドではBattleDataから取得した味方キャラクターと敵キャラクターのデータから戦闘用キャラクターのインスタンス化を行い、インスタンス化したキャラクターゲームオブジェクトをリストに追加しています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 | // Start is called before the first frame update void Start() { // キャラクターインスタンスの親 Transform charactersParent = new GameObject("Characters").transform; // キャラクターを配置するTransform Transform characterTransform; // 同じ名前の敵がいた場合の処理に使うリスト List<string> enemyNameList = new List<string>(); GameObject ins; CharacterBattleScript characterBattleScript; string characterName; // 味方パーティーのプレハブをインスタンス化 for (int i = 0; i < battleData.GetAllyPartyStatus().GetAllyGameObject().Count; i++) { characterTransform = battleBasePosition.Find("AllyPos" + i).transform; ins = Instantiate<GameObject>(battleData.GetAllyPartyStatus().GetAllyGameObject()[i], characterTransform.position, characterTransform.rotation, charactersParent); characterBattleScript = ins.GetComponent<CharacterBattleScript>(); ins.name = characterBattleScript.GetCharacterStatus().GetCharacterName(); if (characterBattleScript.GetCharacterStatus().GetHp() > 0) { allyCharacterInBattleList.Add(ins); allCharacterList.Add(ins); } } if (battleData.GetEnemyPartyStatus() == null) { Debug.LogError("敵パーティーデータが設定されていません。"); } // 敵パーティーのプレハブをインスタンス化 for (int i = 0; i < battleData.GetEnemyPartyStatus().GetEnemyGameObjectList().Count; i++) { characterTransform = battleBasePosition.Find("EnemyPos" + i).transform; ins = Instantiate<GameObject>(battleData.GetEnemyPartyStatus().GetEnemyGameObjectList()[i], characterTransform.position, characterTransform.rotation, charactersParent); // 既に同じ敵が存在したら文字を付加する characterName = ins.GetComponent<CharacterBattleScript>().GetCharacterStatus().GetCharacterName(); if (!enemyNameList.Contains(characterName)) { ins.name = characterName + 'A'; } else { ins.name = characterName + (char)('A' + enemyNameList.Count(enemyName => enemyName == characterName)); } enemyNameList.Add(characterName); enemyCharacterInBattleList.Add(ins); allCharacterList.Add(ins); } // キャラクターリストをキャラクターの素早さの高い順に並べ替え allCharacterList = allCharacterList.OrderByDescending(character => character.GetComponent<CharacterBattleScript>().GetCharacterStatus().GetAgility()).ToList<GameObject>(); // 現在の戦闘 allCharacterInBattleList = allCharacterList.ToList<GameObject>(); // 確認の為並べ替えたリストを表示 foreach (var character in allCharacterInBattleList) { Debug.Log(character.GetComponent<CharacterBattleScript>().GetCharacterStatus().GetCharacterName() + " : " + character.GetComponent<CharacterBattleScript>().GetCharacterStatus().GetAgility()); } // 戦闘前の待ち時間を設定 waitTime = firstWaitingTime; // ランダム値のシードの設定 Random.InitState((int)Time.time); } |
キャラクタープレハブをインスタンス化する機能自体は以前の記事で作成しているので特に問題はないと思いますが、インスタンス化したゲームオブジェクトの名前をキャラクター名を入れるようにしました。
その際に使用しているCharacterBattleScritpは戦闘用のキャラクター全てに取り付ける予定のスクリプトでそのキャラクターの実際の行動はCharacterBattleScriptで行います。
CharacterBattleScriptは後で作成します。
味方キャラクターの場合はallyCharacterInBattleListとallCharacterList、敵キャラクターの場合はenemyCharacterInBattleListとallCharacterListにインスタンス化したゲームオブジェクトを追加しています。
味方キャラクターの場合は死んでいる場合はリストに登録しないようにします。
敵の場合はキャラクター名が被ってしまうので、最初の敵はA、次の敵はBとなるようにします。
それを行っているのが、以下のような処理です。
1 2 3 4 5 6 7 8 9 10 | // 既に同じ敵が存在したら文字を付加する var characterName = ins.GetComponent<CharacterBattleScript>().GetCharacterStatus().GetCharacterName(); if (!enemyNameList.Contains(characterName)) { ins.name = characterName + 'A'; } else { ins.name = characterName + (char)('A' + enemyNameList.Count(enemyName => enemyName == characterName)); } enemyNameList.Add(characterName); |
敵のキャラクター名を取得し、そのキャラクター名の一部がenemyNameListに含まれていなければキャラクター名にAを付けて敵の名前にします。
例えば敵の名前がエルフであれば『エルフA』という名前にします。
敵のキャラクター名が既にenemyNameListに含まれていればenemyNameListにどれだけそのキャラクター名のデータがあるかをenemyNameList.Countで計測し、’A’という文字に足しています。
‘A’はchar型でこれにenemyNameList.Countでカウントした数を足すことで’A’が暗黙的にint型に変換されます。
‘A’はint型で65になります。
‘A’にカウントした数値を足して、(char)でキャストして文字型に戻しています。
enemyNameListに既にエルフが2体登録されていればenemyNameList.Count(enemyName => enemyName == characterName))では2が返されます。
1 2 3 | enemyNameList.Count(enemyName => enemyName == characterName) |
はラムダ式でenemyNameListに登録されている名前をひとつひとつenemyNameという仮の引数に入れ、その引数とcharacterName(インスタンス化する敵のゲームオブジェクトのキャラクター名)と同じ時にCountでカウントされます。
ラムダ式に関しては

を参照してみてください。
‘A’をint型に変換すると65で、それに2を足すと67になりこれをchar型にキャストをすると’C’が得られるので、そのキャラクターのインスタンス化したゲームオブジェクトの名前は『エルフC』となります。
味方キャラクターと敵キャラクタープレハブのインスタンス化とリストに登録が済んだら、戦闘の行動を起こす順番を作成する為にリストを並べ替えます。
1 2 3 | allCharacterList = allCharacterList.OrderByDescending(character => character.GetComponent<CharacterBattleScript>().GetCharacterStatus().GetAgility()).ToList<GameObject>(); |
allCharacterListのOrderByDescendingメソッドを使ってリストに登録されているゲームオブジェクトのキャラクターの素早さの数値の高い順にソートし、それをToListメソッドでその場でリスト化し再度allCharacterListに入れなおしています。
これで素早さの高い順にallCharacterListが並び替えられました。
現在の戦闘に参加している全キャラクターallCharacterInBattleListは最初はallCharacterListと同じなので、
1 2 3 | allCharacterInBattleList = allCharacterList.ToList<GameObject>(); |
上のようにallCharacterListからリストを作成しallCharacterInBattleListに入れておきます。
waitTimeには戦闘開始前なのでfirstWaitingTimeを入れておきます。
戦闘開始時にランダムのシードをゲーム開始からの時間をint型にキャストして設定します。
これは戦闘時に使用するランダム計算の種を毎回変更する為です。
Updateメソッド
Updateメソッドに次のターンのキャラクターの設定やキャラクター間の間隔時間の計算処理等を追加します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | // Update is called once per frame void Update() { // 戦闘が終了していたらこれ以降何もしない if (battleIsOver) { return; } // 戦闘開始 if (isStartBattle) { // 現在のキャラクターの攻撃が終わっている if (!isChoosing) { elapsedTime += Time.deltaTime; if (elapsedTime < waitTime) { return; } elapsedTime = 0f; isChoosing = true; // キャラクターの攻撃の選択に移る MakeAttackChoise(allCharacterInBattleList[currentAttackOrder]); // 次のキャラクターのターンにする currentAttackOrder++; // 全員攻撃が終わったら最初から if (currentAttackOrder >= allCharacterInBattleList.Count) { currentAttackOrder = 0; } } } else { Debug.Log("経過時間: " + elapsedTime); // 戦闘前の待機 elapsedTime += Time.deltaTime; if (elapsedTime >= waitTime) { // 2回目以降はキャラ間の時間を設定 waitTime = timeToNextCharacter; // 最初のキャラクターの待ち時間は0にする為にあらかじめ条件をクリアさせておく elapsedTime = timeToNextCharacter; isStartBattle = true; } } } |
Updateメソッドの最初でbattleIsOverがtrue(戦闘終了)だったらreturnでその後の処理をしないようにします。
isStartBattleがtrueの時は戦闘が開始していて、isChoosingがfalseだった時は戦闘中で行動選択中ではない時なので経過時間を計算し、待ち時間を過ぎていなければreturnでその後の処理をしません。
時間が経過していたらisChoosingをtrueにして行動選択中にします。
MakeAttackChoiseメソッドは行動選択処理をするメソッドで引数に今行動するキャラクターゲームオブジェクトを渡します。
currentAttackOrderをインクリメントして次のキャラクターの番にします。
currentAttackOrderがリストの最後になっていたら0を代入して最初のキャラクターに戻します。
isChoosingがtrueの時は味方キャラクターの行動中の処理を行いますが、今回の記事では敵の行動処理だけを作るので今のところ何も処理は記述しません。
戦闘開始していない時はelseにいきます。
戦闘開始前の時間計測がwaitTimeを越えたらwaitTimeにtimeToNextCharacterの時間を設定します。
elapsedTimeにもtimeToNextCharacterを設定し、戦闘開始した時の最初のキャラクターの待ち時間はないようにします。
isStartBattleにtrueを入れ戦闘を開始します。
MakeAttackChoiseメソッド
Updateメソッドで呼んでいるMakeAttackChoiseメソッドを作成します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | // キャラクターの攻撃の選択処理 public void MakeAttackChoise(GameObject character) { CharacterStatus characterStatus = character.GetComponent<CharacterBattleScript>().GetCharacterStatus(); // EnemyStatusにキャスト出来る場合は敵の攻撃処理 if (characterStatus as EnemyStatus != null) { Debug.Log(character.gameObject.name + "の攻撃"); EnemyAttack(character); } else { Debug.Log(characterStatus.GetCharacterName() + "の攻撃"); AllyAttack(character); } } |
引数で行動をするキャラクターのゲームオブジェクトを受け取ります。
受け取ったキャラクターのゲームオブジェクトからCharacterBattleScriptスクリプトを取得し、GetCharacterStatusメソッドでCharacterStatusデータを取得します。
CharacterStatusをas EnemyStatusでキャストしてnullでなければキャストが可能なので敵キャラクターの行動だと判断します。
ゲームオブジェクトの名前+攻撃という表示をコンソールに表示し、EnemyAttackメソッドにキャラクターのゲームオブジェクトを渡して呼び出します。
EnemyStatusにキャスト出来ないCharacterStatusは味方キャラクターに設定したAllyStatusなのでゲームオブジェクトの名前+攻撃という表示をしてAllyAttackメソッドにゲームオブジェクトを引数として渡して呼び出します。
敵キャラクターにはEnemyStatus、味方キャラクターにはAllyStatusスクリプトを取り付けていて、この二つは元々CharacterStatusクラスを継承して作成しているので、
下記のようにキャスト出来るかどうかで敵キャラクターなのか味方キャラクターなのかを判断し、該当するメソッドを呼び出すという処理にしています。
1 2 3 | if (characterStatus as EnemyStatus != null) { |
別のわかりやすい判定方法もあるかと思いますが、今回はこの方法にしました。
下のような感じでよかったのかも・・・・(^_^;)
1 2 3 4 5 | if(enemyCharacterInBattleList.Contains(character)) { } |
まぁよしとしましょう・・・・。
EnemyAttackメソッドはこの後作成しますが、AllyAttackメソッドは味方キャラクターの攻撃処理なので次回以降作成していきます。
AllyAttackメソッド
AllyAttackメソッドの中身は次回以降に作成していきますが、作成しておかないとエラーになるので中身以外は作成します。
1 2 3 4 5 | // 味方の攻撃 public void AllyAttack(GameObject character) { } |
EnemyAttackメソッド
敵の行動処理を行うEnemyAttackメソッドを作成します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 | // 敵の攻撃処理 public void EnemyAttack(GameObject character) { CharacterBattleScript characterBattleScript = character.GetComponent<CharacterBattleScript>(); CharacterStatus characterStatus = characterBattleScript.GetCharacterStatus(); if (characterStatus.GetSkillList().Count <= 0) { return; } // 敵がガード状態であればガードを解く if (character.GetComponent<Animator>().GetBool("Guard")) { character.GetComponent<CharacterBattleScript>().UnlockGuard(); } // 敵の行動アルゴリズム int randomValue = (int)(Random.value * characterStatus.GetSkillList().Count); var nowSkill = characterStatus.GetSkillList()[randomValue]; // テスト用(特定のスキルで確認) //nowSkill = characterStatus.GetSkillList()[0]; if (nowSkill.GetSkillType() == Skill.Type.DirectAttack) { var targetNum = (int)(Random.value * allyCharacterInBattleList.Count); // 攻撃相手のCharacterBattleScript characterBattleScript.ChooseAttackOptions(CharacterBattleScript.BattleState.DirectAttack, allyCharacterInBattleList[targetNum], nowSkill); Debug.Log(character.name + "は" + nowSkill.GetKanjiName() + "を行った"); } else if (nowSkill.GetSkillType() == Skill.Type.MagicAttack) { var targetNum = (int)(Random.value * allyCharacterInBattleList.Count); if (characterBattleScript.GetMp() >= ((Magic)nowSkill).GetAmountToUseMagicPoints()) { // 攻撃相手のCharacterBattleScript characterBattleScript.ChooseAttackOptions(CharacterBattleScript.BattleState.MagicAttack, allyCharacterInBattleList[targetNum], nowSkill); Debug.Log(character.name + "は" + nowSkill.GetKanjiName() + "を行った"); } else { Debug.Log("MPが足りない!"); // MPが足りない場合は直接攻撃を行う characterBattleScript.ChooseAttackOptions(CharacterBattleScript.BattleState.DirectAttack, allyCharacterInBattleList[targetNum], characterStatus.GetSkillList().Find(skill => skill.GetSkillType() == Skill.Type.DirectAttack)); Debug.Log(character.name + "は攻撃を行った"); } } else if (nowSkill.GetSkillType() == Skill.Type.RecoveryMagic) { if (characterBattleScript.GetMp() >= ((Magic)nowSkill).GetAmountToUseMagicPoints()) { var targetNum = (int)(Random.value * enemyCharacterInBattleList.Count); // 回復相手のCharacterBattleScript characterBattleScript.ChooseAttackOptions(CharacterBattleScript.BattleState.Healing, enemyCharacterInBattleList[targetNum], nowSkill); Debug.Log(character.name + "は" + nowSkill.GetKanjiName() + "を行った"); } else { Debug.Log("MPが足りない!"); var targetNum = (int)(Random.value * allyCharacterInBattleList.Count); // MPが足りない場合は直接攻撃を行う characterBattleScript.ChooseAttackOptions(CharacterBattleScript.BattleState.DirectAttack, allyCharacterInBattleList[targetNum], characterStatus.GetSkillList().Find(skill => skill.GetSkillType() == Skill.Type.DirectAttack)); Debug.Log(character.name + "は攻撃を行った"); } } else if (nowSkill.GetSkillType() == Skill.Type.Guard) { characterBattleScript.Guard(); // Guardアニメはboolなのでアニメーション遷移させたらすぐに次のキャラクターに移行させる ChangeNextChara(); Debug.Log(character.name + "は" + nowSkill.GetKanjiName() + "を行った"); } } |
最初に引数として受け取ったキャラクターのゲームオブジェクトからCharacterBattleScriptとそのCharacterBattleScriptのGetCharacterStatusメソッドを使ってCharacterStatusを取得します。
characterStatusのGetSkillListメソッドを使ってそのキャラクターが持っているスキルを取得し、スキルがひとつもなければreturnを使ってそれ以降の処理はさせないようにします。
次にキャラクターのAnimatorコンポーネントを取得し、アニメーションパラメータ―のGuardがtrueになっていたら、CharacterBattleScriptのUnlockGuardメソッドを呼んで防御状態を解除します。
これはこのキャラクターが前のターンで防御をしていた場合に次のターンが来たら防御を解除する為です。
次にRandam.valueで0~1の間のfloat値を取得しキャラクターが持っているスキルの数と掛けます。
これを(int)でキャストし整数値に変換しスキル数分のランダム値が得られるようにします。
その数値のスキルを取得しnowSkillに入れておきます。
nowSkill.GetSkillType()で選んだスキルのスキルタイプを取得し、そのスキルタイプがどのタイプであるかによって処理を分けます。
Skill.Type.DirectAttackだった時は直接攻撃なので、攻撃をする相手を味方キャラクター(allyCharacterInBattleList)の中からランダムに選びます。
攻撃相手を選んだら攻撃をするキャラクターのcharacterBattleScriptのChooseAttackOptionsメソッドに引数を指定して渡します。
第1引数はCharacterBattleScriptで定義したそのキャラクターが取った行動を示す列挙型を渡し、第2引数で攻撃相手、第3引数は使用したスキルです。
Skill.Type.MagicAttackだった時は魔法を使用するキャラクターのMPが使用する魔法のMP以上の時は魔法攻撃を行い、MPが足りない時は直接攻撃にします。
nowSkillはSkill型なので、(Magic)でキャストしてGetAmountToUseMagicPointsメソッドで使用するMPの量を取得する必要があります。
MPが足りない時に直接攻撃をさせますが、その時に注意が必要になります。
ChooseAttackOptionsメソッドの第3引数にnowSkillを渡してはいけないという事です。
nowSkillには元々MagicAttack系のスキルが入っているので、ここではDirectAttackのスキルを渡さなくてはいけません。
そこでそのキャラクターが持っているスキルの中からDirectAttackに該当するスキルを探してそれを渡しています(敵キャラクターはDirectAttackのスキルを必ず持っている必要があります)。
Skill.Type.RecoveryMagicの時も同様で、使用する相手をenemyCharacterInBattleListの中から探すことになります。
Skill.Type.Guardの時は防御をした時で、そのキャラクターのCharacterBattleScriptのGuardメソッドを呼びます。
防御の場合はアニメーションパラメータがbool型でそのキャラクターの次のターンになるまではずっと防御をしたままにするのでChangeNextCharaメソッドを呼んで直ぐに次のキャラクターにターンを回します。
ChangeNextCharaメソッド
次のキャラクターにターンを回すChangeNextCharaメソッドを作成します。
1 2 3 4 5 6 | // 次のキャラクターに移行 public void ChangeNextChara() { isChoosing = false; } |
ChangeNextCharaメソッドが呼ばれたらisChoosingをfalseにし次のキャラクターにターンが回るようにします。
キャラクターが死んだときの処理メソッド等
キャラクターが死んだときに該当するリストからそのキャラクターを削除する処理を記述します。
DeleteAllCharacterInBattleListメソッド
キャラクターが死んだときに戦闘に参加している全キャラクターからそのキャラクターを削除する必要があるのでDeleteAllCharacterInBattleListメソッドを作成します。
1 2 3 4 5 6 7 8 9 10 11 12 13 | public void DeleteAllCharacterInBattleList(GameObject deleteObj) { var deleteObjNum = allCharacterInBattleList.IndexOf(deleteObj); allCharacterInBattleList.Remove(deleteObj); if(deleteObjNum < currentAttackOrder) { currentAttackOrder--; } // 全員攻撃が終わったら最初から if (currentAttackOrder >= allCharacterInBattleList.Count) { currentAttackOrder = 0; } } |
引数で受け取ったリストから削除するゲームオブジェクトがリスト内で何番目かをIndexOfで求めてdeleteObjNumに入れます。
その後リストからそのゲームオブジェクトを削除し、deleteObjNumがcurrentAttackOrderより小さい時はcurrentAttackOrderをデクリメントして1減らします。
これはcurrentAttackOrderの番号はそのゲームオブジェクトを削除する前のリストの何番目かを表しているので、次のターンのキャラクターの前のリストのゲームオブジェクトを削除した場合はcurrentAttackOrderを1減らす必要がある為です。
currentAttackOrderが戦闘に参加しているキャラクターリストの数以上になっていたら最初のキャラクターにターンを回します。
例えば以下のように4人のキャラクターが戦闘に参加しているとして、現在currentAttackOrderが2だとします。
自身の前の順番のキャラクターを倒すと、リストからそのキャラクターゲームオブジェクトが削除されるのでcurrentAttackOrderの2は自身の前のキャラクターを指してしまいますのでcurrentAttackOrderを1減らしてまた自身を指すように調整します。
自身の後のキャラクターを倒した場合で、倒したキャラクターがリストの最後である場合にUpdateメソッドで次のターンに行くとこの時点ではcurrentAttackOrderが3になっているので範囲外のリストを参照してエラーになってしまいます。
なので、リストを削除した段階でcurrentAttackOrderがリストの数を超えていたら0にして最初のキャラクターのターンにしています。
もっと簡単な処理で出来そうですが・・・・(´Д`)
DeleteAllyCharacterInBattleListメソッド
味方キャラクターが死んだ時はallyCharacterInBattleListから削除します。
1 2 3 4 5 6 7 8 9 | public void DeleteAllyCharacterInBattleList(GameObject deleteObj) { allyCharacterInBattleList.Remove(deleteObj); if (allyCharacterInBattleList.Count == 0) { Debug.Log("味方が全滅"); battleIsOver = true; } } |
allyCharacterInBattleListに登録されている数が0になったら味方キャラクターがいなくなったので全滅しbattleIsOverをtrueにして戦闘処理を終えます。
DeleteEnemyCharacterInBattleListメソッド
敵キャラクターが死んだときはenemyCharacterInBattleListから削除します。
1 2 3 4 5 6 7 8 9 | public void DeleteEnemyCharacterInBattleList(GameObject deleteObj) { enemyCharacterInBattleList.Remove(deleteObj); if (enemyCharacterInBattleList.Count == 0) { Debug.Log("敵が全滅"); battleIsOver = true; } } |
やっていることは味方キャラクターの時と同じです。
味方キャラクターが全滅、または敵キャラクターが全滅した時は戦闘を終了させますが、その処理はまた今度作成したいと思います。
BattleManagerゲームオブジェクトのインスペクタは以下のように設定します。
firstWaitingTimeは3にし戦闘シーンに入ってから3秒間経ってから最初のキャラクターの行動を始めます。
timeToNextCharacerは1にし前のキャラクターの行動が終わってから1秒経ったら次のキャラクターが行動するようにします。
キャラクター個々の戦闘処理スクリプトCharacterBattleScriptの作成
BattleManagerスクリプトでターンが回ってきたキャラクターの選択とその行動の決定をしました。
実際の行動の処理は戦闘用のキャラクターのプレハブにCharacterBattleScriptスクリプトを取り付けそこで行います。
スクリプトはAssets/RPG/Scripts/Battleフォルダに入れます。
スクリプトが長いので分割して説明します。
クラス定義とフィールド宣言
まずはクラスの定義とフィールドを作成していきます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 | using System.Collections; using System.Collections.Generic; using UnityEngine; public class CharacterBattleScript : MonoBehaviour { // 戦闘中のキャラクターの状態 public enum BattleState { Idle, DirectAttack, MagicAttack, Healing, UseHPRecoveryItem, UseMPRecoveryItem, UseNumbnessRecoveryItem, UsePoisonRecoveryItem, IncreaseAttackPowerMagic, IncreaseDefencePowerMagic, NumbnessRecoveryMagic, PoisonnouRecoveryMagic, Damage, Guard, Dead, } private BattleManager battleManager; private BattleStatusScript battleStatusScript; [SerializeField] private CharacterStatus characterStatus = null; private Animator animator; private BattleState battleState; // 元のステータスからコピー // HP private int hp = 0; // MP private int mp = 0; // 補助の素早さ private int auxiliaryAgility = 0; // 補助の力 private int auxiliaryPower = 0; // 補助の打たれ強さ private int auxiliaryStrikingStrength = 0; // 痺れ状態か private bool isNumbness; // 毒状態か private bool isPoison; // 今選択したスキル private Skill currentSkill; // 今のターゲット private GameObject currentTarget; // 今使用したアイテム private Item currentItem; // ターゲットのCharacterBattleScript private CharacterBattleScript targetCharacterBattleScript; // ターゲットのCharacterStatus private CharacterStatus targetCharacterStatus; // 攻撃選択後のアニメーションが終了したかどうか private bool isDoneAnimation; // キャラクターが死んでいるかどうか private bool isDead; // 攻撃力アップしているかどうか private bool isIncreasePower; // 攻撃力アップしているポイント private int increasePowerPoint; // 攻撃力アップしているターン private int numOfTurnsIncreasePower = 3; // 攻撃力アップしてからのターン private int numOfTurnsSinceIncreasePower = 0; // 防御力アップしているかどうか private bool isIncreaseStrikingStrength; // 防御力アップしているポイント private int increaseStrikingStrengthPoint; // 防御力アップしているターン private int numOfTurnsIncreaseStrikingStrength = 3; // 防御力アップしてからのターン private int numOfTurnsSinceIncreaseStrikingStrength = 0; } |
BattleStateはSkillのスキルタイプと同じような表記をしていますが、これはこのキャラクターが現在どんな行動をしているかを表す列挙型として使います。
自分のターンでない時はBattleState.Idleにします。
battleManagerはBattleManagerスクリプト、battleStatusScriptは戦闘シーンで味方キャラクターのステータスを表示しているスクリプトを入れます。
characterStatusにはこのスクリプト(CharacterBattleScript)を取り付けたキャラクターのCharacterStatusデータを設定します。
animatorはこのキャラクターのAnimatorコンポーネント、battleStateはこのキャラクターの状態を表す列挙型のBattleStateを保持します。
キャラクターのステータスをコピーして使用する必要がある?
さて、フィールド宣言を見て少し不思議に思った方もいるかもしれません。
それは以下のようなフィールドを用意しているからです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | // 元のステータスからコピー // HP private int hp = 0; // MP private int mp = 0; // 補助の素早さ private int auxiliaryAgility = 0; // 補助の力 private int auxiliaryPower = 0; // 補助の打たれ強さ private int auxiliaryStrikingStrength = 0; // 痺れ状態か private bool isNumbness; // 毒状態か private bool isPoison; |
キャラクターにはCharacterStatusというキャラクターのステータスを設定していてそこでHPやMP、痺れ状態か、毒状態か等の情報の保持と取得が出来るはずです。
なぜCharacterBattleScriptで改めて同じフィールドを宣言しているかというと、CharacterStatusのデータが一つだと同じキャラクターが何体か出現した時に困るからです。
例えば『エルフ』が敵として2体出現し『エルフA』『エルフB』としそれぞれEnemyStatusスクリプト(CharacterStatusを継承して作った)から作成したElfデータを使うとします。
Elfデータでは最大HPや現在のHP等のデータを持っていますが、『エルフA』に攻撃を与えてElfデータを書き換えると『エルフB』のCharacterStatusのGetHpメソッドを呼び出した時にElfデータからデータを取得する為既にHPが減った状態になってしまいます。
『エルフA』はElfA、『エルフB』はElfBというデータを予め作っておいて個別に設定すればいいですが、膨大な量のデータを作成する必要が出てくるので、今回はElfデータは初期データとして使い、そこから一旦CharacterBattleScriptにHPやMPのデータをコピーし、戦闘中はCharacterBattleScriptのHPやMPを使うようにします。
こうすることで『エルフA』と『エルフB』がデータを共有する必要がなくなります。
味方キャラクターの場合は同一キャラクターはいないのでそのまま個々のデータを書き換えて使用してもいいんですが、敵キャラクターと合わせてCharacterBattleScriptにデータをコピーして使う事にします。
キャラクターの攻撃力等は同じなのでCharacterStatusからデータを取得します。
戦闘終了時にCharacterBattleScriptのデータをUnityChanStatusデータやYujiStatusデータに上書きをするようにします(この処理は次の記事以降作成します)。
auxiliaryAgility、auxiliaryPower、auxiliaryStrikingStrengthは戦闘中に上がった攻撃力や防御力等を入れます。
currentSkillは選択したスキル
currentTargetは攻撃等の相手のゲームオブジェクト
currentItemは使用したアイテム
targetCharacterBattleScriptは攻撃等の相手のCharacterBattleScript
targetCharacterStatusは攻撃などの相手のCharacterStatus
を保持します。
なぜこれらのフィールドを用意したかというと、攻撃をしたキャラクターが攻撃や魔法のアニメーションを終了した後に攻撃対象のダメージアニメーションやダメージエフェクトの表示、ダメージを減らす処理等を行いたいからです。
なので攻撃者のアニメーションが終了するまで攻撃相手の情報を保持し、アニメーションが終わったらその情報を使ってダメージ処理を行います。
isDoneAnimationはこのキャラクターのターンの行動アニメーションが終了したかどうかを表すフラグです。
isDeadはこのキャラクターが死んでいるかどうかを表すフラグです。
Startメソッド
Startメソッドに処理を記述していきます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | private void Start() { animator = GetComponent<Animator>(); // 元データから設定 hp = characterStatus.GetHp(); mp = characterStatus.GetMp(); isNumbness = characterStatus.IsNumbnessState(); isPoison = characterStatus.IsPoisonState(); // 状態の設定 battleState = BattleState.Idle; // コンポーネントの取得 battleManager = GameObject.Find("BattleManager").GetComponent<BattleManager>(); battleStatusScript = GameObject.Find("BattleUI/StatusPanel").GetComponent<BattleStatusScript>(); // 既に死んでいる場合は倒れている状態にする if (characterStatus.GetHp() <= 0) { animator.CrossFade("Dead", 0f, 0, 1f); isDead = true; } } |
インスペクタで設定したそのキャラクターのCharacterStatusデータからHP、MP、痺れ、毒の状態を取得しフィールドに保持します。
battleManagerはBattleManagerゲームオブジェクトを探し、そこからGetComponentでBattleManagerスクリプトを取得します。
battleStatusScriptも同じように取得します。
戦闘開始時に既にそのキャラクターのHPが0以下であれば死んでいるのでanimator.CrossFadeを使ってアニメーションでDead状態で始まるようにしています。
第1引数にAnimatorControllerの状態を指定し、第2引数で遷移の期間、第3引数でクロスフェードが発生するレイヤーを指定し、第4引数で状態の時間を指定します。
CrossFadeを使うとAnimatorControllerで状態を繋げていなくても遷移させることが出来ます。
また単純に
1 2 3 | animator.SetBool("Dead", true); |
とすると、戦闘開始時に死ぬアニメーションが再生されてしまいます。
CrossFadeメソッドを使用すると第4引数でその状態の正規化された1fの位置、つまり1回のアニメーションの再生の最後の部分の死んで倒れた状態で戦闘が始まります。
Updateメソッド
Updateメソッドに処理を追加していきます。
Updateメソッドで行う事は自身のターンになったらその行動のアニメーションが終了するのを待って終了したら攻撃等の対象にエフェクトを表示したり対象のHPを減らしたりといった処理をします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 | void Update() { // 既に死んでいたら何もしない if (isDead) { return; } // 自分のターンでなければ何もしない if (battleState == BattleState.Idle) { return; } // アニメーションが終わっていなければ何もしない if (!isDoneAnimation) { return; } // 選択したアニメーションによって処理を分ける if (battleState == BattleState.DirectAttack) { ShowEffectOnTheTarget(); DirectAttack(); // 自分のターンが来たので上がったパラメータのチェック CheckIncreaseAttackPower(); CheckIncreaseStrikingStrength(); } else if (battleState == BattleState.MagicAttack) { ShowEffectOnTheTarget(); MagicAttack(); // 自分のターンが来たので上がったパラメータのチェック CheckIncreaseAttackPower(); CheckIncreaseStrikingStrength(); } else if (battleState == BattleState.Healing || battleState == BattleState.NumbnessRecoveryMagic || battleState == BattleState.PoisonnouRecoveryMagic ) { ShowEffectOnTheTarget(); UseMagic(); // 自分のターンが来たので上がったパラメータのチェック CheckIncreaseAttackPower(); CheckIncreaseStrikingStrength(); } else if (battleState == BattleState.IncreaseAttackPowerMagic) { ShowEffectOnTheTarget(); UseMagic(); // 自身の攻撃力をアップした場合はターン数をカウントしない if(currentTarget == this.gameObject) { CheckIncreaseStrikingStrength(); } else { CheckIncreaseAttackPower(); CheckIncreaseStrikingStrength(); } } else if(battleState == BattleState.IncreaseDefencePowerMagic) { ShowEffectOnTheTarget(); UseMagic(); // 自身の防御力をアップした場合はターン数をカウントしない if (currentTarget == this.gameObject) { CheckIncreaseAttackPower(); } else { CheckIncreaseAttackPower(); CheckIncreaseStrikingStrength(); } } else if (battleState == BattleState.UseHPRecoveryItem || battleState == BattleState.UseMPRecoveryItem || battleState == BattleState.UseNumbnessRecoveryItem || battleState == BattleState.UsePoisonRecoveryItem ) { UseItem(); // 自分のターンが来たので上がったパラメータのチェック CheckIncreaseAttackPower(); CheckIncreaseStrikingStrength(); } // ターゲットのリセット currentTarget = null; currentSkill = null; currentItem = null; targetCharacterBattleScript = null; targetCharacterStatus = null; battleState = BattleState.Idle; // 自身の選択が終了したら次のキャラクターにする battleManager.ChangeNextChara(); isDoneAnimation = false; } |
isDeadがtrueだったら既にこのキャラクターは死んでいるのでreturnでその後の処理はしません。
battleStateがBattleState.Idleであればそのキャラクターのターンではないのでreturnでその後の処理はしません。
isDoneAnimationがfalseであればアニメーションが終了していないのでreturnでその後の処理はしません。
行動がBattleState.DirectAttackであればShowEffectOnTheTargetメソッドを呼んで攻撃対象にエフェクトを表示します。
DirectAttackメソッドを呼んで攻撃処理をします。
CheckIncreaseAttackPowerメソッドで攻撃力アップ魔法のチェック、CheckIncreaseStrikingStrengthで防御力アップ魔法のチェックを行います。
これは魔法で攻撃力や防御力が上がっている時に指定したターン経過したら効果を消すためのチェックをするメソッドです。
行動がBattleState.MagicAttackの時はMagicAttackメソッドに変えているだけでDirectAttackと同じです。
行動が回復魔法、状態回復の時はUseMagicメソッドに変更しているだけです。
行動がBattleState.IncreaseAttackPowerMagicの時は攻撃力を上げる対象のゲームオブジェクトと自身が同じだったらCheckIncreaseAttackPowerメソッドは呼び出さないようにします。
これは自分自身に攻撃力アップ魔法をかけた時にそのターン内で1回ターンを過ごしたと計測しない為です。
行動がBattleState.IncreaseDefencePowerMagicの時もやっている事は同じでCheckIncreaseStrikingStrengthを呼び出さないようにしています。
行動がアイテムを使用した場合はShowEffectOnTheTargetを呼び出していませんが、ItemデータでSkillReceivingSideEffectにエフェクトを設定した場合は呼び出すとエフェクトを表示出来ます。
アイテム個々でエフェクトを変えたい場合はItemというスキル一つではなく新たにSkillスクリプトを継承してMagicのように個々のアイテムスクリプトを作りエフェクトを設定出来るようにするといいかもしれません。
行動が終わったら攻撃等の対象情報を初期化しそのキャラクターの状態をBattleState.Idleに戻し、BattleManagerスクリプトのChangeNextCharaメソッドを呼んで次のキャラクターにターンを回します。
そしてisDoneAnimationをfalseにします。
フィールドのセッター、ゲッターメソッド
CharacterBattleScriptに用意したHPやMP等のデータを設定したり取得するメソッドを作成します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 | public CharacterStatus GetCharacterStatus() { return characterStatus; } public void SetHp(int hp) { this.hp = Mathf.Max(0, Mathf.Min(characterStatus.GetMaxHp(), hp)); if (this.hp <= 0) { Dead(); } } public int GetHp() { return hp; } public void SetMp(int mp) { this.mp = Mathf.Max(0, Mathf.Min(characterStatus.GetMaxMp(), mp)); } public int GetMp() { return mp; } public bool IsDoneAnimation() { return isDoneAnimation; } public int GetAuxiliaryAgility() { return auxiliaryAgility; } public int GetAuxiliaryPower() { return auxiliaryPower; } public int GetAuxiliaryStrikingStrength() { return auxiliaryStrikingStrength; } // 補正の素早さを設定 public void SetAuxiliaryAgility(int value) { auxiliaryAgility = value; } // 補正の力を設定 public void SetAuxiliaryPower(int value) { auxiliaryPower = value; } // 補正の打たれ強さを設定 public void SetAuxiliaryStrikingStrength(int value) { auxiliaryStrikingStrength = value; } public bool IsNumbness() { return isNumbness; } public bool IsPoison() { return isPoison; } public void SetNumbness(bool isNumbness) { this.isNumbness = isNumbness; } public void SetPoison(bool isPoison) { this.isPoison = isPoison; } public bool IsIncreasePower() { return isIncreasePower; } public void SetIsIncreasePower(bool isIncreasePower) { this.isIncreasePower = isIncreasePower; } public bool IsIncreaseStrikingStrength() { return isIncreaseStrikingStrength; } public void SetIsIncreaseStrikingStrength(bool isIncreaseStrikingStrength) { this.isIncreaseStrikingStrength = isIncreaseStrikingStrength; } public void SetBattleState(BattleState state) { this.battleState = state; } public void SetIsDoneAnimation() { isDoneAnimation = true; } |
SetHpやSetMpメソッドではそのキャラクターのHPやMPの最大値と受け取った値をMathf.Minメソッドで最小値の方を取得し、それをMathf.Maxメソッドで0と比較して大きい方を返します。
最初のMathf.Minでは値を回復した時に最大HPやMPを超えないようにし、次のMathf.Maxではダメージを受けて減らした時に0より下の数値にならないようにする為です。
HPが0以下になったらDeadメソッドを呼び出して死んだ後の処理をします。
選んだ選択肢の処理メソッドChooseAttackOptionsの作成
BattleManagerメソッドで敵の行動を選んだらそのキャラクターのCharacterBattleScriptのChooseAttackOptionsメソッドを呼び出していました。
ChooseAttackOptionsメソッドは選んだ行動を処理する為のメソッドです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | // 選択肢から選んだモードを実行 public void ChooseAttackOptions(BattleState selectOption, GameObject target, Skill skill = null, Item item = null) { // スキルやターゲットの情報をセット currentTarget = target; currentSkill = skill; targetCharacterBattleScript = target.GetComponent<CharacterBattleScript>(); targetCharacterStatus = targetCharacterBattleScript.GetCharacterStatus(); // 選択したキャラクターの状態を設定 battleState = selectOption; if (selectOption == BattleState.DirectAttack) { animator.SetTrigger("DirectAttack"); } else if (selectOption == BattleState.MagicAttack || selectOption == BattleState.Healing || selectOption == BattleState.IncreaseAttackPowerMagic || selectOption == BattleState.IncreaseDefencePowerMagic || selectOption == BattleState.NumbnessRecoveryMagic || selectOption == BattleState.PoisonnouRecoveryMagic ) { animator.SetTrigger("MagicAttack"); // 魔法使用者のMPを減らす SetMp(GetMp() - ((Magic)skill).GetAmountToUseMagicPoints()); // 使用者が味方キャラクターであればStatusPanelの更新 if (GetCharacterStatus() as AllyStatus != null) { battleStatusScript.UpdateStatus(GetCharacterStatus(), BattleStatusScript.Status.MP, GetMp()); } Instantiate(((Magic)skill).GetSkillUserEffect(), transform.position, ((Magic)skill).GetSkillUserEffect().transform.rotation); } else if (selectOption == BattleState.UseHPRecoveryItem || selectOption == BattleState.UseMPRecoveryItem || selectOption == BattleState.UseNumbnessRecoveryItem || selectOption == BattleState.UsePoisonRecoveryItem ) { currentItem = item; animator.SetTrigger("UseItem"); } } |
ChooseAttackOptionsメソッドの引数でSkill型とItem型を取れるようにしていますが、Item型の引数は必要ない事もあるので引数が渡ってこなかった場合はnullで初期化するようにしています。
メソッドの最初で引数で受け取った攻撃等の対象のゲームオブジェクトから情報を取得しフィールドに保持しておきます。
行動がBattleState.DirectAttackの場合はアニメーションパラメータのDirectAttackをトリガーして直接攻撃のアニメーションを再生させます。
行動が魔法系の時はアニメーションパラメータのMagicAttackをトリガーして魔法攻撃のアニメーションを再生させます。
SetMpメソッドを呼んで魔法使用者のMPを減らします。
魔法を使用したのが味方キャラクターだった場合は魔法使用者のBattleStatusScriptのUpdateStatusメソッドを呼んでステータス表示を更新します。
その後に引数で受け取ったskillの攻撃者側のエフェクトをインスタンス化し表示します。
インスタンス化する場所は魔法使用者の位置で、エフェクトの角度はエフェクト自身の元の角度を使います。
行動がアイテム系だった時はcurrentItemにitemを入れアニメーションパラメータのUseItemをトリガーします。
今回はアイテム系は味方キャラクターのみ使う事が出来るようになっています。
敵キャラクターもアイテムを使えるようにする場合はそれ用に追加の処理を記述していく必要があります。
ChooseAttackOptionsメソッドは引数として受け取った情報をフィールドに保持し、このキャラクターの状態を変更し、受け取った行動のエフェクトの表示等をしています。
対象のゲームオブジェクトにエフェクトを表示するShowEffectOnTheTargetメソッド
攻撃等をした時に攻撃対象のゲームオブジェクトの近くにエフェクトを表示するShowEffectOnTheTargetメソッドを作成します。
1 2 3 4 5 6 | // ターゲットエフェクトの表示 public void ShowEffectOnTheTarget() { Instantiate<GameObject>(currentSkill.GetSkillReceivingSideEffect(), currentTarget.transform.position, currentSkill.GetSkillReceivingSideEffect().transform.rotation); } |
currentSkillが使用したスキルなのでそこから相手に表示するエフェクトを取得しインスタンス化します。
直接攻撃を受けた対象のダメージ処理をするDirectAttackメソッド
直接攻撃を受けた対象のダメージ処理をするDirectAttackメソッドを作成します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | public void DirectAttack() { var targetAnimator = currentTarget.GetComponent<Animator>(); targetAnimator.SetTrigger("Damage"); var damage = 0; // 攻撃相手のStatus if (targetCharacterStatus as AllyStatus != null) { var castedTargetStatus = (AllyStatus)targetCharacterBattleScript.GetCharacterStatus(); // 攻撃相手の通常の防御力+相手のキャラの補助値 var targetDefencePower = castedTargetStatus.GetStrikingStrength() + (castedTargetStatus.GetEquipArmor()?.GetAmount() ?? 0) + targetCharacterBattleScript.GetAuxiliaryStrikingStrength(); damage = Mathf.Max(0, (characterStatus.GetPower() + auxiliaryPower) - targetDefencePower); // 相手のステータスのHPをセット targetCharacterBattleScript.SetHp(targetCharacterBattleScript.GetHp() - damage); // ステータスUIを更新 battleStatusScript.UpdateStatus(targetCharacterStatus, BattleStatusScript.Status.HP, targetCharacterBattleScript.GetHp()); } else if (targetCharacterStatus as EnemyStatus != null) { var castedTargetStatus = (EnemyStatus)targetCharacterBattleScript.GetCharacterStatus(); // 攻撃相手の通常の防御力+相手のキャラの補助値 var targetDefencePower = castedTargetStatus.GetStrikingStrength() + targetCharacterBattleScript.GetAuxiliaryStrikingStrength(); damage = Mathf.Max(0, (characterStatus.GetPower() + (((AllyStatus)characterStatus).GetEquipWeapon()?.GetAmount() ?? 0) + auxiliaryPower) - targetDefencePower); // 敵のステータスのHPをセット targetCharacterBattleScript.SetHp(targetCharacterBattleScript.GetHp() - damage); } else { Debug.LogError("直接攻撃でターゲットが設定されていない"); } Debug.Log(gameObject.name + "は" + currentTarget.name + "に" + currentSkill.GetKanjiName() + "をして" + damage + "を与えた。"); } |
攻撃を受けたゲームオブジェクトはcurrentTargetなのでそこからAnimatorコンポーネントを取得し、アニメーションパラメータのDamageをトリガーして攻撃を受けたキャラクターのダメージアニメーションを再生させます。
攻撃を受けたキャラクターが味方キャラクターであればそのキャラクターのCharacterStatusをAllyStatusにキャストしてcastedTargetStatusに入れます。
castedTargetStatusからキャラクターの防御力や装備している防具、魔法で補正されている防御力から全体の防御力を計算しtargetDefencePowerに入れます。
1 2 3 | castedTargetStatus.GetEquipArmor()?.GetAmount() ?? 0 |
上の処理は分かり辛いですが、castedTargetStatus.GetEquipArmorメソッドで装備している防具を取得し、それがnullだったら防具を装備していないので??の後の0を返し、
nullでなければGetAmountメソッドの値を取得し返します。
ダメージの計算ではMathf.Maxメソッドを使って0と計算したダメージを比較し大きい方をdamageに入れます。
1 2 3 | damage = Mathf.Max(0, (characterStatus.GetPower() + auxiliaryPower) - targetDefencePower); |
これはtargetDefencePowerの値の方が大きかった時にdamageにマイナスの値が入ってしまう事を避ける為です。
ダメージの計算が終了したら攻撃対象のCharacterBattleScriptのSetHpメソッドを呼んでHPを減らします。
また味方キャラクターが攻撃を受けた場合はBattleStatusScriptのUpdateメソッドを呼び出してステータス表示を更新します。
攻撃を受けたのが敵キャラクターの場合は防具を装備していないので防御力のみでダメージを計算します。
やっていることは味方キャラクターの時と同じです。
魔法攻撃を受けた対象のダメージ処理をするMagicAttack
魔法攻撃を受けた対象のダメージ処理をするMagicAttackメソッドを作成します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | public void MagicAttack() { var targetAnimator = currentTarget.GetComponent<Animator>(); targetAnimator.SetTrigger("Damage"); var damage = 0; // 攻撃相手のStatus if (targetCharacterStatus as AllyStatus != null) { var castedTargetStatus = (AllyStatus)targetCharacterBattleScript.GetCharacterStatus(); var targetDefencePower = castedTargetStatus.GetStrikingStrength() + (castedTargetStatus.GetEquipArmor()?.GetAmount() ?? 0); damage = Mathf.Max(0, ((Magic)currentSkill).GetMagicPower() - targetDefencePower); //// 相手のステータスのHPをセット targetCharacterBattleScript.SetHp(targetCharacterBattleScript.GetHp() - damage); // ステータスUIを更新 battleStatusScript.UpdateStatus(targetCharacterStatus, BattleStatusScript.Status.HP, targetCharacterBattleScript.GetHp()); } else if (targetCharacterStatus as EnemyStatus != null) { var castedTargetStatus = (EnemyStatus)targetCharacterBattleScript.GetCharacterStatus(); var targetDefencePower = castedTargetStatus.GetStrikingStrength(); damage = Mathf.Max(0, ((Magic)currentSkill).GetMagicPower() - targetDefencePower); // 相手のステータスのHPをセット targetCharacterBattleScript.SetHp(targetCharacterBattleScript.GetHp() - damage); } else { Debug.LogError("魔法攻撃でターゲットが設定されていない"); } Debug.Log(gameObject.name + "は" + currentTarget.name + "に" + currentSkill.GetKanjiName() + "をして" + damage + "を与えた。"); } |
やっている事はDirectAttackメソッドとほとんど同じなので説明は省きます。
魔法攻撃以外の魔法を受けた時の処理をするUseMagicメソッド
魔法攻撃以外の魔法を受けた時の処理をするUseMagicメソッドを作成します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | public void UseMagic() { // アニメーション状態を作ってなかったのでDamageにする currentTarget.GetComponent<Animator>().SetTrigger("Damage"); var magicType = ((Magic)currentSkill).GetSkillType(); if (magicType == Skill.Type.RecoveryMagic) { var recoveryPoint = ((Magic)currentSkill).GetMagicPower() + characterStatus.GetMagicPower(); if (targetCharacterStatus as AllyStatus != null) { targetCharacterBattleScript.SetHp(targetCharacterBattleScript.GetHp() + recoveryPoint); battleStatusScript.UpdateStatus(targetCharacterStatus, BattleStatusScript.Status.HP, targetCharacterBattleScript.GetHp()); } else { targetCharacterBattleScript.SetHp(GetHp() + recoveryPoint); } Debug.Log(gameObject.name + "は" + ((Magic)currentSkill).GetKanjiName() + "を使って" + currentTarget.name + "を" + recoveryPoint + "回復した。"); } else if (magicType == Skill.Type.IncreaseAttackPowerMagic) { increasePowerPoint = ((Magic)currentSkill).GetMagicPower() + characterStatus.GetMagicPower(); targetCharacterBattleScript.SetAuxiliaryPower(targetCharacterBattleScript.GetAuxiliaryPower() + increasePowerPoint); targetCharacterBattleScript.SetIsIncreasePower(true); Debug.Log(gameObject.name + "は" + ((Magic)currentSkill).GetKanjiName() + "を使って" + currentTarget.name + "の力を" + increasePowerPoint + "増やした。"); } else if (magicType == Skill.Type.IncreaseDefencePowerMagic) { increaseStrikingStrengthPoint = ((Magic)currentSkill).GetMagicPower() + characterStatus.GetMagicPower(); targetCharacterBattleScript.SetAuxiliaryStrikingStrength(targetCharacterBattleScript.GetAuxiliaryStrikingStrength() + increaseStrikingStrengthPoint); targetCharacterBattleScript.SetIsIncreaseStrikingStrength(true); Debug.Log(gameObject.name + "は" + ((Magic)currentSkill).GetKanjiName() + "を使って" + currentTarget.name + "の打たれ強さを" + increaseStrikingStrengthPoint + "増やした。"); } else if (magicType == Skill.Type.NumbnessRecoveryMagic) { targetCharacterStatus.SetNumbness(false); Debug.Log(gameObject.name + "は" + ((Magic)currentSkill).GetKanjiName() + "を使って" + currentTarget.name + "の痺れを消した"); } else if (magicType == Skill.Type.PoisonnouRecoveryMagic) { targetCharacterStatus.SetPoisonState(false); Debug.Log(gameObject.name + "は" + ((Magic)currentSkill).GetKanjiName() + "を使って" + currentTarget.name + "の毒を消した"); } } |
魔法のタイプがUseMagicの時は対象のCharacterBattleScriptのSetHpを呼び出してHPを回復させます。
IncreaseAttackPowerMagicの時は攻撃力の補正値を上げます。
IncreaseDefencePowerMagicの時は防御力の補正値を上げます。
NumbnessRecoveryMagicの時は痺れをなくします。
PoisonnouRecoveryMagicの時は毒をなくします。
アイテムを受けた時の処理を行うUseItemメソッド
アイテムを使用された時の処理を行うUseItemメソッドを作成します。
今回はアイテムを使用するのは味方キャラクターだけを想定しています。
この記事では敵キャラクターの処理だけを作っていっていますが、UseItemメソッドで行っていることもUseMagicメソッド等とほとんど変わらないのでここで作ってしまいます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | public void UseItem() { currentTarget.GetComponent<Animator>().SetTrigger("Damage"); // キャラクターのアイテム数を減らす ((AllyStatus)characterStatus).SetItemNum(currentItem, ((AllyStatus)characterStatus).GetItemNum(currentItem) - 1); if (currentItem.GetItemType() == Item.Type.HPRecovery) { // 回復力 var recoveryPoint = currentItem.GetAmount(); targetCharacterBattleScript.SetHp(targetCharacterBattleScript.GetHp() + recoveryPoint); battleStatusScript.UpdateStatus(targetCharacterStatus, BattleStatusScript.Status.HP, targetCharacterBattleScript.GetHp()); Debug.Log(gameObject.name + "は" + currentItem.GetKanjiName() + "を使って" + currentTarget.name + "のHPを" + recoveryPoint + "回復した。"); } else if (currentItem.GetItemType() == Item.Type.MPRecovery) { // 回復力 var recoveryPoint = currentItem.GetAmount(); targetCharacterBattleScript.SetMp(targetCharacterBattleScript.GetMp() + recoveryPoint); battleStatusScript.UpdateStatus(targetCharacterStatus, BattleStatusScript.Status.MP, targetCharacterBattleScript.GetMp()); Debug.Log(gameObject.name + "は" + currentItem.GetKanjiName() + "を使って" + currentTarget.name + "のMPを" + recoveryPoint + "回復した。"); } else if(currentItem.GetItemType() == Item.Type.NumbnessRecovery) { targetCharacterStatus.SetNumbness(false); Debug.Log(gameObject.name + "は" + currentItem.GetKanjiName() + "を使って" + currentTarget.name + "の痺れを消した。"); } else if(currentItem.GetItemType() == Item.Type.PoisonRecovery) { targetCharacterStatus.SetPoisonState(false); Debug.Log(gameObject.name + "は" + currentItem.GetKanjiName() + "を使って" + currentTarget.name + "の毒を消した。"); } // アイテム数が0になったらItemDictionaryからそのアイテムを削除 if (((AllyStatus)characterStatus).GetItemNum(currentItem) == 0) { ((AllyStatus)characterStatus).GetItemDictionary().Remove(currentItem); } } |
アイテムを使用されたキャラクターのアニメーション状態を作っていなかったのでアイテムを受けたキャラクターはダメージを受けた時と同じアニメーションを再生する事にします。
アイテムを使用するのは味方キャラクターだけだと想定しているので、CharacterStatusをAllyStatusにキャストしてSetItemNumメソッドを使って使用したアイテムの数を一つ減らします。
その後は使用したアイテムの種類を判別し、対応する処理をしているだけです。
行っていることはHPやMPを回復したり、状態を回復しているだけなので問題はないと思います。
アイテムを減らした後にそのアイテムが0になったらAllyStatusのItemDictionaryからそのアイテム自体を削除します。
防御した時のGuardメソッドと解除した時のUnlockGuardメソッド
キャラクターが防御した時のGuardメソッドと防御を解除した時のUnlockGuardメソッドを作成します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | // 防御 public void Guard() { // 自分のターンが来たので上がったパラメータのチェック CheckIncreaseAttackPower(); CheckIncreaseStrikingStrength(); animator.SetBool("Guard", true); SetAuxiliaryStrikingStrength(GetAuxiliaryStrikingStrength() + 10); } // 防御を解除 public void UnlockGuard() { animator.SetBool("Guard", false); SetAuxiliaryStrikingStrength(GetAuxiliaryStrikingStrength() - 10); } |
BattleManagerでターンが来たキャラクターの行動を選択しChooseAttackOptionsメソッドを呼び出してそのキャラクターの状態を変更していますが、防御をする時だけはChooseAttackOptionsメソッドを呼び出さず直接Guardメソッドを呼び出しています。
なのでUpdateメソッド内でbattleStateを比較しても防御の場合はBattleState.Idleの状態なのでそれ以降の処理はしていません。
そこでGuardメソッドでCheckIncreaseAttackPowerメソッドとCheckIncreaseStrikingStrengthメソッドを呼んで補正値のチェックをしておきます。
アニメーションパラメータのGuardをtrueにして防御アニメーションを再生させます。
UnlockGuardメソッドは防御をしたキャラクターのターンが終了し、他のキャラクターのターンになってから再び防御をしているキャラクターに戻った時にBattleManagerスクリプトで呼び出しています。
ここではアニメーションパラメータのGuardをfalseにし、補正値を元に戻しています。
キャラクターが死んだときの処理をするDeadメソッド
キャラクターが死んだときの処理をするDeadメソッドを作成します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | // 死んだときに実行する処理 public void Dead() { animator.SetTrigger("Dead"); battleManager.DeleteAllCharacterInBattleList(this.gameObject); if (GetCharacterStatus() as AllyStatus != null) { battleStatusScript.UpdateStatus(GetCharacterStatus(), BattleStatusScript.Status.HP, GetHp()); battleManager.DeleteAllyCharacterInBattleList(this.gameObject); } else if (GetCharacterStatus() as EnemyStatus != null) { battleManager.DeleteEnemyCharacterInBattleList(this.gameObject); } isDead = true; } |
AnimatorControllerのアニメーションパラメータのDeadをトリガーし死んだときのアニメーションを再生します。
死んだのでBattleManagerのDeleteAllCharacterInBattleListから自身のゲームオブジェクトを削除します。
その後、このキャラクターが味方キャラクターか敵キャラクターかを判別し、該当するリストから自身のゲームオブジェクトを削除します。
isDeadをtrueにし、UpdateメソッドのisDeadで判定している以降の処理を実行しないようにします。
攻撃補正チェックをするCheckIncreaseAttackPowerメソッドと防御補正チェックをするCheckIncreaseStrikingStrengthメソッド
攻撃補正チェックをするCheckIncreaseAttackPowerメソッドと防御補正チェックをするCheckIncreaseStrikingStrengthメソッドを作成します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | public void CheckIncreaseAttackPower() { // 自分のターンが来た時に何らかの効果魔法を使ってたらターン数を増やす if (IsIncreasePower()) { numOfTurnsSinceIncreasePower++; if (numOfTurnsSinceIncreasePower >= numOfTurnsIncreasePower) { numOfTurnsSinceIncreasePower = 0; SetAuxiliaryPower(GetAuxiliaryPower() - increasePowerPoint); SetIsIncreasePower(false); Debug.Log(gameObject.name + "の攻撃力アップの効果が消えた"); } } } public void CheckIncreaseStrikingStrength() { if (IsIncreaseStrikingStrength()) { numOfTurnsSinceIncreaseStrikingStrength++; if (numOfTurnsSinceIncreaseStrikingStrength >= numOfTurnsIncreaseStrikingStrength) { numOfTurnsSinceIncreaseStrikingStrength = 0; SetAuxiliaryStrikingStrength(GetAuxiliaryStrikingStrength() - increaseStrikingStrengthPoint); SetIsIncreaseStrikingStrength(false); Debug.Log(gameObject.name + "の防御力アップの効果が消えた"); } } } |
これらのメソッドは補正魔法が使われてから一定のターンが経過したら補正を元に戻す為の処理をしています。
これで敵の行動処理を記述したCharacterBattleScriptが出来たので、
Assets/RPG/Prefabs/Characters/Enemyフォルダの敵のプレハブであるElfPrefab、GoblinPrefab、GoblinWarrirPrefab、
Assets/RPG/Prefabs/Characters/Allyフォルダの味方の戦闘シーン用のプレハブであるBattleUnityChan、BattleYuji、
にCharacterBattleScriptを設定し、インスペクタのcharacterStatusにそれぞれのキャラクターのステータスデータを設定します。
例えばElfPrefabであれば、以下のようにcharacterStatusにElfデータのアセットファイルを設定します。
アニメーションが終了したことを知らせるビヘイビアの作成
ここまでで敵の行動選択と実際の行動処理の作成がほとんど作成出来ました。
しかし実際に敵に行動をさせてみると敵がアニメーションをした後の処理が実行されません。
それは敵のアニメーションが終了したかどうかを示しているisDoneAnimationがずっとfalseのままだからです。
アニメーションが終了したらそのキャラクターのCharacterBattleScriptのisDoneAnimationをtrueにし、Updateメソッドの処理を実行するようにしなければいけません。
そこで戦闘で使用しているキャラクターのAnimatorControllerにビヘイビアを取り付けてアニメーションが終了したらそのキャラクターのCharacterBattleScriptのisDoneAnimationをtrueにします。
そこでAnimatorControllerの状態にEndStateというビヘイビアスクリプトを取り付けてアニメーションが終了したらそのキャラクターのCharacterBattleScriptのisDoneAnimationをtrueにすることにします。
Assets/RPG/Animators/AllyフォルダのBattleUnityChanアニメーターコントローラーを選択し、AnimatorタブのDirectAttack状態を選択し、インスペクタのAdd Behaviourボタンを押してNew Scriptを選択し、名前をEndStateと入力します。
AssetsフォルダにEndStateビヘイビアスクリプトが出来ると思うので、Assets/RPG/Scripts/Battleフォルダへ移動させておきます。
EndStateビヘイビアスクリプトには既にメソッドが定義されているのでOnStateEnterメソッドとOnStateExitメソッドのコメントを外しておきます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 | using System.Collections; using System.Collections.Generic; using UnityEngine; public class EndState : StateMachineBehaviour { private BattleManager battleManager; private CharacterBattleScript characterBattleScript; // OnStateEnter is called when a transition starts and the state machine starts to evaluate this state override public void OnStateEnter(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) { // BattleManagerを取得していなければ探す if (battleManager == null) { battleManager = GameObject.Find("BattleManager").GetComponent<BattleManager>(); } if (characterBattleScript == null) { characterBattleScript = animator.GetComponent<CharacterBattleScript>(); } } // OnStateUpdate is called on each Update frame between OnStateEnter and OnStateExit callbacks //override public void OnStateUpdate(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) { //} // OnStateExit is called when a transition ends and the state machine finishes evaluating this state override public void OnStateExit(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) { // スキルを使った側の場合はアニメーションの終了フラグのオンと次のキャラクターへのターン if (stateInfo.IsName("DirectAttack") || stateInfo.IsName("MagicAttack") || stateInfo.IsName("UseItem") ) { characterBattleScript.SetIsDoneAnimation(); } } // OnStateMove is called right after Animator.OnAnimatorMove() //override public void OnStateMove(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) //{ // // Implement code that processes and affects root motion //} // OnStateIK is called right after Animator.OnAnimatorIK() //override public void OnStateIK(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) //{ // // Implement code that sets up animation IK (inverse kinematics) //} } |
OnStateEnterメソッドはAnimatorControllerのEndStateビヘイビアスクリプトが設定された状態に入った時に呼ばれます。
そこでbattleManagerとCharacterBattleScriptが設定されていなければ取得します。
OnStateExitメソッドはEndStateが設定された状態から出る時に呼ばれるので、ここでそのキャラクターのCharacterBattleScriptのSetIsDoneAnimationメソッドを呼び出してisDoneAnimationをtrueにします。
SetIsDoneAnimationメソッドを呼び出すのはアニメーションの状態がDirectAttack、MagicAttack、UseItemの時に行います。
これで行動を起こしたキャラクターのアニメーションが終わってその状態から抜ける時にisDoneAnimationがtrueになり、次のキャラクターへとターンを回します。
現時点ではDirectAttack状態のみEndStateを取り付けていますが、他の状態にも取り付けてください。
これで機能が出来ました。
終わりに
敵キャラの行動処理が出来上がりましたが非常にスクリプトが長いし分かり辛いですね。
ただキャラクターの行動処理をさせるCharacterBattleScriptは味方のキャラクターでも同じなので味方キャラクターで新たに追加する必要はありません。
味方キャラクターの行動を選択する処理等はBattleManagerスクリプトに追加していくという感じになります。
行動処理の後にDebug.Logを使って情報をコンソールに表示していますが、場合によっては情報の表示順が前後してしまいます。
この情報はコンソールで確認する為に表示しているだけなので、次回以降でUIに情報を表示する機能を作っていく予定なのでそれまではあまり気にせずに置いておいてください。
次回は味方キャラクターの行動を選択する機能を作成していきます。

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