
前回、地下を3つの階に分けて、奥へ進めるようにしました。でも、地下2・3階の新しい敵は、みんな「たたく」しかしてきませんでした・・・。今回は、羽ペン・ぶあつい辞書・とびだす地球儀・ランプのおばけ・まほうのページに、それぞれの行動を付けたいと思います!
- シリーズの目次:ターン制RPGの作り方
- 前回:#21 地下へ進もう
今回作るもの

- 敵の行動に「選ばれやすさ」を付ける(たたく 2:せきかのことば 1 なら、たたく をよく使う)
- 「順番に使う」敵も作る(ぶあつい辞書は、ためる → のしかかり → ためる …)
- 行動の多くは、仲間のまほう・わざのクラスを、そのまま敵にも使う
| 敵 | 階 | 行動(選ばれやすさ) | 使うクラス |
|---|---|---|---|
| 羽ペン | 地下2 | たたく(1)/れんぞくひっかき(1):0.6倍×2回 | MultiHitSkillCommand(れんげき と同じ) |
| ぶあつい辞書 | 地下2 | 順番に:ためる → のしかかり(1.5倍) | ChargeCommand(新しく作る)・MultiHitSkillCommand |
| とびだす地球儀 | 地下2 | たたく(2)/ころがりアタック(1):味方全員に 0.7倍 | AllAttackCommand(新しく作る) |
| ランプのおばけ | 地下3 | たたく(1)/ほのお(2):まほう 威力 16 | AttackSpellCommand(まほうの矢 と同じ) |
| まほうのページ | 地下3 | たたく(2)/せきかのことば(1):1人を 30% で せきか | StatusSpellCommand(へびのどく と同じ) |
| スクリプト | 変えること |
|---|---|
| EnemyMove(新しく作る) | 敵の行動1つ分:コマンドと、選ばれやすさ |
| EnemyData | コマンドの配列を、EnemyMove の配列と「順番に使う」に |
| BattleUnit・EnemyBrain | 選ばれやすさ・順番で行動を選ぶ |
| AllAttackCommand(新しく作る) | 相手の全員に当たる攻撃 |
| ChargeCommand(新しく作る) | ためる(何もしないで、次の大技の予告) |
| BattleContext | 相手の、立っている全員 |
| UnitView・IdleBounce | 敵がまほうを使うときは ふくらむ、回る、石になったら止まる |
| BattleManager | 石になった仲間は、コマンドを選ばない |
敵の行動に、選ばれやすさを付けよう

#10では、敵のコマンドの配列から、ランダムに1つ選んでいました。どのコマンドも同じくらい選ばれるので、「ときどき使う大技」が作れません。
そこで、行動ごとに「選ばれやすさ(重み)」を持たせます。
EnemyMove と EnemyData
- 「EnemyMove」スクリプトを作る
using UnityEngine;
// 敵の行動1つ分:使うコマンドと、選ばれやすさ
[System.Serializable]
public class EnemyMove
{
public BattleCommand command;
[Min(1)] public int weight = 1; // 選ばれやすさ(ほかの行動の2倍にすると、2倍よく使う)
}
[System.Serializable]で、Inspectorの配列の1つ1つに「コマンド」と「選ばれやすさ」を並べて入れられるようにします(#21のEnemyGroupと同じです)[Min(1)]を付けると、Inspectorで0以下を入れられなくなります。重みが0の行動は選ばれないので、入れ忘れを防ぎます
EnemyDataのcommandsの行を、次の2行に書き換えます。
[SerializeField] private EnemyMove[] moves; // 使う行動(この中から選ぶ)
[SerializeField] private bool usesMovesInOrder; // チェックすると、上から順番に使う(選ばれやすさは見ない)
Commandsのプロパティも、次の2行に書き換えます。
public EnemyMove[] Moves => moves;
public bool UsesMovesInOrder => usesMovesInOrder;
欄の名前と型が変わるので、今まで入れていた敵のコマンドは消えます。地下1階の3体も、下の表のとおりにMovesを入れ直してください。
敵ごとの行動を入れる

この回で作る行動のアセット(DoubleScratch など)は、あとの節で作ります。先に作ってから入れても、Attackだけ入れておいて、あとで足してもかまいません。
| ファイル名 | Moves(Command・Weight) | Uses Moves In Order |
|---|---|---|
| SpilledInk | Attack・1 | - |
| PaperPlane | Attack・1 | - |
| Bookworm | Bite・1 | - |
| QuillPen | Attack・1 / DoubleScratch・1 | - |
| ThickDictionary | Charge・1 / BodyPress・1 | チェック |
| PopupGlobe | Attack・2 / RollAttack・1 | - |
| LampGhost | Attack・1 / Fire・2 | - |
| MagicPage | Attack・2 / StoneWord・1 | - |
- ランプのおばけは、まほうを使うので、Statsの Magic を 4 にする(ほかの敵は0のまま)
選ばれやすさで選ぶ(EnemyBrain)
BattleUnitのCommandsのコメントを直して、Memberの下に2つ足します。
public BattleCommand[] Commands { get; } // 選べるコマンド(仲間だけ。敵は EnemyData の行動から選ぶ)
public EnemyData EnemyData { get; } // 敵のデータ(敵だけ。仲間は null)
public int MoveCount { get; set; } // 行動を決めた回数(順番に使う敵が、次の行動を調べるのに使う)
敵のデータから作るコンストラクタの、Commands = data.Commands;の行を次のように書き換えます。
EnemyData = data;
EnemyBrainを、次のように書き換えます。
using System.Collections.Generic;
// 敵が、使う行動と相手を選ぶ
public static class EnemyBrain
{
public static BattleAction Decide(BattleUnit enemy, BattleUnit[] party)
{
BattleCommand command = ChooseCommand(enemy);
// 相手は、まだ倒れていない仲間から選ぶ
List<BattleUnit> living = new List<BattleUnit>();
foreach (BattleUnit member in party)
{
if (!member.IsDead)
{
living.Add(member);
}
}
BattleUnit target = living[GameRandom.Index(living.Count)];
BattleAction action = new BattleAction(command, target.View);
action.User = enemy;
return action;
}
// 使う行動を選ぶ。順番に使う敵は上から順に、ほかの敵は選ばれやすさで
private static BattleCommand ChooseCommand(BattleUnit enemy)
{
EnemyMove[] moves = enemy.EnemyData.Moves;
if (enemy.EnemyData.UsesMovesInOrder)
{
// 最後まで使ったら、最初に戻る(2つなら、ためる → のしかかり → ためる …)
EnemyMove next = moves[enemy.MoveCount % moves.Length];
enemy.MoveCount++;
return next.command;
}
// 選ばれやすさを全部足した数の中から1つ引いて、どの行動の分に入ったかを調べる
int total = 0;
foreach (EnemyMove move in moves)
{
total += move.weight;
}
int roll = GameRandom.Index(total);
foreach (EnemyMove move in moves)
{
if (roll < move.weight)
{
return move.command;
}
roll -= move.weight;
}
return moves[0].command; // 選ばれやすさがどれも1以上なら、ここには来ない
}
}
- 選ばれやすさ:まほうのページ(たたく 2・せきかのことば 1)なら、合計は3。0〜2の数を1つ引いて、0と1なら たたく、2なら せきかのことばです。引いた数から重みを引きながら、どの行動の分に入ったかを順に調べています
- 順番に使う:
MoveCountを行動の数で割った余り(%)を番号にします。2つなら 0・1・0・1 … とくり返します - 最後の
returnは、ふつうは通りません。C#では、どの道を通っても値を返すように書く必要があるので、置いています
乱数は、今までと同じGameRandomから引いています。シードを決めれば、どの敵がどの行動をするかも、毎回同じになります(#9)。
れんぞくひっかき・のしかかり:れんげきのクラスを、そのまま使おう

羽ペンの「れんぞくひっかき」は、こうげき の0.6倍で2回。これは、#13で作ったキョンシーの「れんげき」(MultiHitSkillCommand)と同じ形です。新しいスクリプトは要りません。
- TurnBasedRpg/Dataに「EnemySkills」フォルダを作る(敵だけが使う行動を、仲間のまほうと分けておく)
- Create>TurnBasedRpg>Skills>Multi Hit で2つ作る
| ファイル名 | Command Name | Mp Cost | Damage Rate | Hit Count |
|---|---|---|---|---|
| DoubleScratch | れんぞくひっかき | 0 | 0.6 | 2 |
| BodyPress | のしかかり | 0 | 1.5 | 1 |
- 敵はMPを持っていない(0)ので、Mp Cost は0にします。0なら
CanUseはいつも true で、SpendMpで減るMPもありません - メッセージは「羽ペンの れんぞくひっかき!」のように出るので、Command Nameは「〜する」ではなく「〜き」「〜り」の形にしています
- ポーズの絵が無い敵は、
PlayAttackPoseAsyncが何もしないので、前に出て2回当たって、戻ります
スライムキョンシーちゃんのわざを、敵も使ってくるんですね・・・
ころがりアタック:全員に当たる攻撃


とびだす地球儀の「ころがりアタック」は、仲間3人に同時に当たる攻撃です。全員に当たる攻撃は、まだ「まほう」しかありません(#14のいなずま)。たたかう と同じ「こうげき」で計算する全体攻撃を、新しいクラスで作ります。
相手の、立っている全員
BattleContextのLivingPartyの上に、次のメソッドを足します。
// user の相手(仲間なら敵、敵なら仲間)のうち、まだ倒れていない全員
public List<BattleUnit> LivingOpponents(BattleUnit user)
{
UnitView[] opponents = user.IsEnemy ? Party : Enemies;
List<BattleUnit> living = new List<BattleUnit>();
foreach (UnitView view in opponents)
{
if (!view.Unit.IsDead)
{
living.Add(view.Unit);
}
}
return living;
}
FindLivingOpponent(1人を返す)の、全員版です。敵が使えば仲間の全員、仲間が使えば敵の全員になります
AllAttackCommand


- Scripts/Commandsに「AllAttackCommand」スクリプトを作る
using System.Collections.Generic;
using UnityEngine;
// 相手の全員に当たる攻撃(とびだす地球儀の ころがりアタック):くるっと回りながら前に出て、立っている全員にダメージ
[CreateAssetMenu(fileName = "NewAllAttack", menuName = "TurnBasedRpg/Commands/All Attack")]
public class AllAttackCommand : BattleCommand
{
[SerializeField] private float damageRate = 0.7f; // 1人あたりのダメージの倍率
public override async Awaitable ExecuteAsync(BattleUnit user, BattleAction action, BattleContext context)
{
List<BattleUnit> targets = context.LivingOpponents(user);
await context.Message.ShowAsync($"{user.Name}の {CommandName}!");
Awaitable spin = user.View.SpinAsync(0.4f); // 回るのと、前に出るのを、いっしょに始める
await user.View.StepAsync(true);
await spin;
// 全員いっしょに当たる
UnitView[] views = new UnitView[targets.Count];
int[] damages = new int[targets.Count];
for (int i = 0; i < targets.Count; i++)
{
BattleUnit target = targets[i];
views[i] = target.View;
damages[i] = DamageCalculator.Physical(user, target, damageRate);
target.TakeDamage(damages[i]);
}
await context.Effects.PlayHitAllAsync(views, damages);
await user.View.StepAsync(false);
// 倒れた相手は全員いっしょに、そのあとで1人ずつメッセージ(#14 の全体のまほうと同じ)
List<Awaitable> falls = new List<Awaitable>();
foreach (BattleUnit target in targets)
{
if (target.IsDead)
{
falls.Add(PlayFallAsync(target));
}
}
foreach (Awaitable fall in falls)
{
await fall;
}
for (int i = 0; i < targets.Count; i++)
{
await context.Message.ShowAsync($"{targets[i].Name}に {damages[i]}の ダメージ!");
}
foreach (BattleUnit target in targets)
{
if (target.IsDead)
{
await context.Message.ShowAsync(FallMessage(target));
}
}
}
}
- 相手は、選ぶときではなく実行するときに決めます。先に動いた仲間が倒れていたら、その人には当たりません
- ダメージは、たたかう と同じ
DamageCalculator.Physicalに、倍率(0.7)をかけたものです。ぼうぎょ中の人には半分になります - 当たったあとの演出とメッセージは、#14の全体のまほう(AttackSpellCommand)と同じ順番です
SelectAsyncは書いていません。敵しか使わないので、相手を選ぶ画面は要らないからです
くるっと回る


UnitViewのResumeIdleの上に、次のメソッドを足します。
// くるっと1回転する(ころがりアタック)。前の向きへ転がるように回す
public async Awaitable SpinAsync(float seconds)
{
float time = 0f;
while (time < seconds)
{
time += Time.deltaTime;
float angle = -360f * forward * Mathf.Clamp01(time / seconds);
transform.localRotation = Quaternion.Euler(0f, 0f, angle);
await Awaitable.NextFrameAsync(destroyCancellationToken);
}
transform.localRotation = Quaternion.identity;
}
- Z軸の回転は、プラスが左回りです。敵は左を向いている(
forwardが -1)ので、角度はプラスになり、左へ転がるように回ります - 最後に
Quaternion.identity(回っていない向き)に戻して、少しずれた角度で止まらないようにしています
| ファイル名 | 作るメニュー | Command Name | Damage Rate |
|---|---|---|---|
| RollAttack | Create>TurnBasedRpg>Commands>All Attack | ころがりアタック | 0.7 |
ほのお・せきかのことば:まほうのクラスを敵にも


ランプのおばけの「ほのお」と、まほうのページの「せきかのことば」は、まほうです。#13のAttackSpellCommandと、#15のStatusSpellCommandを、そのまま使います。


- ほのお用に、オレンジの火の粉を作る
- Prefabsの MagicBurst を選んで、Ctrl+D で複製し、名前を FireBurst にする
- FireBurstを開き、Particle Systemの Start Color を、オレンジ(FF7319)と黄色(FFD94D)の「Random Between Two Colors」にする
- EnemySkillsに、2つ作る
| ファイル名 | 作るメニュー | Command Name | Mp Cost | そのほか |
|---|---|---|---|---|
| Fire | Create>TurnBasedRpg>Skills>Attack Spell | ほのお | 0 | Effect Prefab:FireBurst、Power:16 |
| StoneWord | Create>TurnBasedRpg>Skills>Status Spell | せきかのことば | 0 | Effect Prefab:MagicBurst、Effect:Stone、Chance:30 |
- どちらも、相手は1人です(Targets All と For Allies は外したまま)。相手は
EnemyBrainが選んだ仲間になります
ポーズの絵が無い敵は、ふくらむ


まほうのクラスは、使う人のPlayOrbAsync(杖をふる)を呼びます。でも、敵には杖をふるポーズの絵がありません。このまま敵に使わせると、エラーで止まります(待機の弾みidleBounceも、ポーズの絵も、敵は空のため)。
そこで、ポーズの絵が無いときは、かわりに少しふくらんで戻るようにします。UnitViewのPlayFramesAsyncの最初に、次のifを足します。
// 待機の弾みを止めて、ポーズの絵を順に出す(最後の絵のまま止まる)
// ポーズの絵が無い敵は、かわりに ふくらんで、もとに戻る
private async Awaitable PlayFramesAsync(Sprite[] frames)
{
if (frames == null)
{
await SwellAsync();
return;
}
idleBounce.enabled = false;
SpinAsyncの上に、次のメソッドを足します。
// ふくらんで、もとの大きさに戻る(敵が まほうを使う・力をためるとき)
private async Awaitable SwellAsync()
{
const float seconds = 0.4f;
float time = 0f;
while (time < seconds)
{
time += Time.deltaTime;
// Sin で 0 → 1 → 0 と変えて、いちばん大きいときに 1.15 倍
float wave = Mathf.Sin(Mathf.Clamp01(time / seconds) * Mathf.PI);
transform.localScale = Vector3.one * (1f + 0.15f * wave);
await Awaitable.NextFrameAsync(destroyCancellationToken);
}
transform.localScale = Vector3.one;
}
Mathf.Sinに 0〜π(半周)を入れると、0 → 1 → 0 と変わります。これで「ふくらんで、戻る」を1つの式で書けますPlayOrbAsync(杖をふる)・PlayCastAsync(力をためる)など、ポーズのメソッドは全部PlayFramesAsyncを通るので、ここを1か所直すだけで、敵がどのまほうを使ってもふくらみます
石になった仲間


今まで、せきか は仲間から敵にかけるだけでした。敵が仲間を石にするようになったので、仲間の側も2つ直します。
- 石になっている間は、待機の弾みを止める(灰色のまま弾んでいると、石に見えない)
- 石になっている仲間は、コマンドを選ばない(選んでも「うごけない!」になるだけなので)
IdleBounceのtimerの上に、プロパティを足します。
public bool IsPaused { get; set; } // true の間は、コマを進めない(せきか などで動けないとき)
Updateの最初に、次のifを足します。
if (IsPaused)
{
// 止まっている間も、痛がる絵などのあとは、今の待機のコマに戻しておく
image.sprite = frames[frameOrder[orderIndex]];
return;
}
enabledを false にして止めると、受けたときのHitAsyncなどがenabled = trueに戻してしまいます。止める理由を別に持つことで、ほかの演出とぶつからないようにしています
UnitViewのShowEffectsを、次のように書き換えます。
public void ShowEffects(List<ActiveEffect> effects)
{
if (effectIcons != null)
{
effectIcons.Show(effects);
}
tint = Color.white;
bool canAct = true;
foreach (ActiveEffect effect in effects)
{
if (effect.Data.Tint != Color.white)
{
tint = effect.Data.Tint;
}
if (!effect.Data.CanAct)
{
canAct = false;
}
}
SetDimmed(isDimmed);
// せきか などで動けない間は、待機の弾みを止める(仲間だけ)
if (idleBounce != null)
{
idleBounce.IsPaused = !canAct;
}
}
- 状態がかかったとき・とけたときに、毎回呼ばれるメソッドです。せきか がとけると
IsPausedが false に戻り、また弾み始めます
BattleManagerのSelectActionsAsyncで、倒れている人を飛ばしているifを、次のように書き換えます。
BattleUnit unit = party[index];
if (!CanChoose(unit))
{
// 倒れている人・せきか などで動けない人は、コマンドを選ばない
selected[index] = null;
コマンドでキャンセルしたときの行も、次のように書き換えます。
// コマンドでキャンセルしたら、前の人に戻る(選べない人は飛ばす)
index = PreviousChoosableIndex(index);
PreviousLivingIndexを、次の2つのメソッドに書き換えます。
// コマンドを選べるか(倒れている人と、せきか などで動けない人は選ばない)
private bool CanChoose(BattleUnit unit)
{
return !unit.IsDead && unit.CanAct;
}
// index より前で、コマンドを選べる一番近い人の番号(いなければ index のまま)
private int PreviousChoosableIndex(int index)
{
for (int i = index - 1; i >= 0; i--)
{
if (CanChoose(lineup.PartyUnits[i]))
{
return i;
}
}
return index;
}
- 「選べるか」を
CanChooseの1か所にまとめて、進むときと戻るときで同じ決まりを使うようにしています



石になっている間は、ほかの2人でがんばってね・・・!
ためてから のしかかる:順番に使う敵


ぶあつい辞書の「のしかかり」は、1.5倍の大技です。いきなり使われると、よけようがありません。そこで、1つ前のターンに「ためる」で予告します。予告を見たら、ぼうぎょ(受けるダメージ半分)で備えられます。
ChargeCommand


- Scripts/Commandsに「ChargeCommand」スクリプトを作る
using UnityEngine;
// ためる:その番は攻撃しないで、力をためる(次の大きな攻撃の予告)
[CreateAssetMenu(fileName = "NewCharge", menuName = "TurnBasedRpg/Commands/Charge")]
public class ChargeCommand : BattleCommand
{
[SerializeField] private string message = "ちからを ためている!"; // 「〇〇は 」のあとに出す
public override async Awaitable ExecuteAsync(BattleUnit user, BattleAction action, BattleContext context)
{
Awaitable cast = user.View.PlayCastAsync(); // 力をためる(ポーズの絵が無い敵は、ふくらむ)
await context.Message.ShowAsync($"{user.Name}は {message}");
await cast;
user.View.ResumeIdle();
}
}
- 何もダメージを与えない、予告だけのコマンドです。ふくらむのとメッセージを、いっしょに始めています
- メッセージを欄にしておくと、ほかの敵が「ページを めくっている…」のような、別の予告を使うこともできます
| ファイル名 | 作るメニュー | Command Name | Message |
|---|---|---|---|
| Charge | Create>TurnBasedRpg>Commands>Charge | ためる | ちからを ためている! |
順番に使う


- ThickDictionaryのMovesを、上から Charge・BodyPress にして、Uses Moves In Order にチェックを入れる
- 1ターン目に ためる、2ターン目に のしかかり、3ターン目にまた ためる … になります
辞書の すばやさ は4で、とても遅いです。ぼうぎょ は すばやさ に関係なくターンの最初に動く(#9)ので、「ためている」を見た次のターンに ぼうぎょ を選べば、のしかかり に間に合います。



「ためている」が見えたら、ぼうぎょ ですね!
動かして確認!


- Lobbyシーンを開いて再生し、「地下へ」→ ちか2かい・ちか3かい(まだ行けないときは、ちか1かい からクリアしていく)
- 羽ペンは、ときどき「れんぞくひっかき」で2回当ててくる
- ぶあつい辞書は、「ちからを ためている!」のあと、次のターンに「のしかかり」
- とびだす地球儀は、ときどき回りながら転がってきて、3人に同時に当てる
- ランプのおばけは、よく「ほのお」を使う(ふくらんでから、オレンジの火の粉)
- まほうのページの「せきかのことば」で石になった仲間は、灰色で止まり、コマンドを選ばない。3ターンで元に戻る
- 地下1階の敵は、今までどおり(本の虫の かみつく も、どく になる)
GIFは、確かめやすいように、BattleLineupの敵を入れ替えてBattleシーンから撮っています。Battleシーンから再生するときは、BattleLineupの Enemies に、確かめたい敵を入れてください。
#22の動作確認
- 敵の行動が、選ばれやすさに合わせて選ばれる(たたく 2:せきかのことば 1 なら、たたく が多い)
- ぶあつい辞書は、ためる と のしかかり を交互に使う
- ころがりアタックは、立っている仲間の全員に同時に当たる
- 敵がまほうを使うと、エラーにならずに、ふくらんでから当たる
- 石になった仲間は、弾みが止まり、コマンドを選ぶ順番から飛ばされる
あとがき
今回は、地下2・3階の敵に、それぞれの行動を付けました。行動に「選ばれやすさ」と「順番」を持たせたので、敵の性格をInspectorだけで変えられます。仲間のまほう・わざのクラスがそのまま敵にも使えたのは、#4でコマンドをクラスにしておいたおかげです。次回は、いよいよ地下3階の奥にいるボス「禁じられた本」を作りたいと思います!



辞書の のしかかり、ぼうぎょ しないと本当に痛いです・・・



まほうのページは、石にしてくるから先に倒すのがおすすめよ!
コメント
コメント一覧 (1件)
[…] 前回:#22 地下2・3階の敵 […]