
前回で、まほうが使えるようになりました。でも、まだ1人にしか使えません。今回は、敵全体・味方全員に効くまほうを作ります。全体のまほうは、選んでいるときに対象の全員を光らせて、1人用のまほうと見まちがえないようにします。さらに、レベルが上がると新しいまほうを覚えて、本を読むポーズでお知らせするところまで作ってみたいと思います!
- シリーズの目次:ターン制RPGの作り方
- 前回:#13 まほうとMP
今回作るもの
- 攻撃のまほう・回復のまほうに「全体」を選べるようにする(いなずま・みんなでヒール)
- 全体のまほうを選ぶと、対象の全員が光って、Enterで決める
- 全員に同時に当たる・回復する
- まほうごとに「覚えるレベル」を決めて、レベルが上がったら新しいまほうを覚える(本を読むポーズ)
| まほう | 使う人 | MP | 相手 | 効果 | 覚える Lv |
|---|---|---|---|---|---|
| ぷるんアタック | スライムちゃん | 4 | 敵1体 | こうげきの1.5倍 | 4 |
| みんなでヒール | フェアリー | 8 | 味方全員 | 回復 威力14 | 6 |
| いなずま | メドゥーサ | 9 | 敵全体 | まほう 威力22 | 7 |
- まもりの風・へびのどく など、状態を変えるまほうは、次の#15で足します
| スクリプト | 役割 | 付ける場所 |
|---|---|---|
| TargetSelector | 全員を光らせて、決めるのを待つ | - |
| SkillCommand・AttackSpellCommand・HealSpellCommand | 全体を選ぶ・全員に効かせる | - |
| BattleEffects | 何人かに同時に当たる演出 | - |
| SkillLearning・CharacterData | 覚えるまほうと、覚えるレベル | (付けない) |
| BattleUnit・BattleLineup | 覚えたまほう、最初の経験値(確認用) | - |
| UnitView・BattleManager | 本を読むポーズで、覚えたことを知らせる | - |
全体をねらおう

この節では、敵全体・味方全員に効くまほうを作ります。
全員を光らせて、決めるのを待つ
1人を選ぶときは、#6 たたかう相手を選ぼうのTargetSelectorで、矢印キーで1人ずつ光らせました。全体のときは選ぶ必要がないので、対象の全員を光らせて、Enterで決める(Escでやめる)だけです。
全員が光っていると、ひと目で「これは全体に効くんだな」と分かります。1人だけ光っていると、全体のまほうなのに1人にしか効かないように見えてしまいます。
TargetSelectorに、変数を足します(isWaitingの下)。
private bool isAll; // 全員を選んでいるときは true(矢印キーで動かさない)
SelectTargetAsyncの下に、メソッドを足します。
// candidates を全員光らせて、決めるのを待つ(決めたら true、キャンセルなら false)
// others は、選んでいる間だけ暗くしておく人たち
public async Awaitable<bool> SelectAllAsync(UnitView[] candidates, UnitView[] others)
{
// コマンドを決めた Enter が、まだこのフレームに残っていることがあるので、1フレーム待ってから始める
await Awaitable.NextFrameAsync(destroyCancellationToken);
foreach (UnitView view in others)
{
view.SetHighlight(false);
view.SetDimmed(true);
}
foreach (UnitView view in candidates)
{
view.SetDimmed(false);
view.SetHighlight(true); // 全員を光らせる
}
targets = candidates;
index = 0;
isAll = true;
targetSource.Reset();
isWaiting = true;
EventSystem.current.SetSelectedGameObject(null);
UnitView chosen = await targetSource.Awaitable;
foreach (UnitView view in candidates)
{
view.SetHighlight(false);
}
foreach (UnitView view in others)
{
view.SetDimmed(false);
}
isWaiting = false;
isAll = false;
return chosen != null;
}
- 決めたかどうかは、今までと同じ
targetSourceで待ちます。Enterならtargets[index](だれか1人)、Escならnullが入るので、nullでなければ「決めた」です - 全員を光らせるのは、#5 だれの番か、ひと目で分かるようにしようで作った
SetHighlightを、全員に呼ぶだけです
全員を選んでいる間は、矢印キーやマウスで1人に動かないようにします。Updateの、矢印キーを読むところのifに条件を足します。
// 右か下で次の相手、左か上で前の相手(全員を選んでいるときは動かさない)
if (!isAll && inputActions.UI.Navigate.WasPressedThisFrame())
PointAtの最初のifにも足します。
if (!isWaiting || isAll)
- クリックは、だれをクリックしても「決めた」になります(
Chooseはそのまま)
全体を選ぶまほう
SkillCommandに、全体かどうかの欄を足します(effectPrefabの下)。
[SerializeField] private bool targetsAll; // チェックすると、敵全体・味方全員に効く
public bool TargetsAll => targetsAll;
SelectAllyAsyncの下に、選ぶメソッドを2つ足します。
// 敵の全員を光らせて、決めるのを待つ(相手は実行するときに決めるので、Target は null)
protected async Awaitable<BattleAction> SelectAllEnemiesAsync(BattleContext context)
{
context.MessageText.text = "てき ぜんたいに つかう?";
bool isDecided = await context.TargetSelector.SelectAllAsync(context.Enemies, context.Party);
if (!isDecided)
{
return null;
}
return new BattleAction(this, null);
}
// 味方の全員を光らせて、決めるのを待つ
protected async Awaitable<BattleAction> SelectAllAlliesAsync(BattleContext context)
{
context.MessageText.text = "みかた ぜんいんに つかう?";
bool isDecided = await context.TargetSelector.SelectAllAsync(context.LivingParty(), context.Enemies);
if (!isDecided)
{
return null;
}
return new BattleAction(this, null);
}
- 全体のまほうは、相手を1人に決めません。実行するときに「その時点で立っている全員」にするので、BattleActionのTargetは
nullです(選んだあとに倒れた敵には当たらないように)
何人かに同時に当たる演出
#8 攻撃の手応えを作ろうのBattleEffectsのPlayHitAsyncは、1人に当たる演出でした。全体のまほうで、これを1人ずつ同時に呼ぶと、ヒットストップと画面の揺れが何回も重なってしまいます。特に揺れは、揺れている途中の位置を「元の位置」として覚えてしまい、画面がずれたまま止まることがあります。
そこで、何人に当たっても、ヒットストップと揺れは1回だけにします。PlayHitAsyncを、次の2つのメソッドに書き換えます。
// 1人に当たった瞬間の演出
public async Awaitable PlayHitAsync(UnitView target, int damage)
{
await PlayHitAllAsync(new UnitView[] { target }, new int[] { damage });
}
// 何人かに同時に当たった瞬間の演出。ヒットストップと揺れは1回だけ(いちばん大きいダメージで揺らす)
public async Awaitable PlayHitAllAsync(UnitView[] targets, int[] damages)
{
List<Awaitable> hits = new List<Awaitable>();
int maxDamage = 0;
for (int i = 0; i < targets.Length; i++)
{
UnitView target = targets[i];
int damage = damages[i];
BattleUnit unit = target.Unit;
if (unit.Row != null)
{
unit.Row.SetHp(unit.Hp, unit.Stats.maxHp); // ステータスのゲージを減らす(仲間だけ)
}
if (!unit.IsEnemy)
{
damageVignette.Play(); // 仲間が受けたら、画面のふちを赤く
}
DamagePopup popup = Instantiate(damagePopupPrefab, popupLayer);
popup.transform.position = target.transform.position;
popup.Setup(damage);
Instantiate(sparksPrefab, target.transform.position, Quaternion.identity);
hits.Add(target.HitAsync()); // 光る・のけぞるは、全員いっしょに始める
maxDamage = Mathf.Max(maxDamage, damage);
}
Awaitable shake = ShakeAsync(maxDamage);
await HitStopAsync();
foreach (Awaitable hit in hits)
{
await hit;
}
await shake;
}
PlayHitAsyncは、1人だけの配列を作ってPlayHitAllAsyncを呼ぶようにしました。たたかう などは、今までどおりPlayHitAsyncを呼べばOKですnew UnitView[] { target }は、「target が1つだけ入った配列」を作る書き方です- 1行目に
using System.Collections.Generic;を足す(Listを使うため)
倒れるのは、全員いっしょに
全体のまほうで何体も倒したときに、「1体消えてメッセージ、次の1体が消えてメッセージ…」と1体ずつ進むと、機械的な処理に見えて、まとめて倒した爽快感がありません。そこで、当たる・倒れる演出までは全員いっしょに起こして、そのあとでメッセージを1人ずつ出すようにします。
#13でBattleCommandに作ったShowFallAsyncは、「消える演出」と「メッセージ」を続けて行っていました。これを分けて、演出だけ・メッセージだけを使えるようにします。ShowFallAsyncを、次の3つのメソッドに書き換えます。
// HPが0になった相手の演出:敵は消えて、仲間は戦闘不能の絵に(メッセージは出さない)
protected async Awaitable PlayFallAsync(BattleUnit target)
{
if (target.IsEnemy)
{
await target.View.DisappearAsync();
}
else
{
target.View.ShowDown();
}
}
// 倒れたときのメッセージ
protected string FallMessage(BattleUnit target)
{
return target.IsEnemy ? $"{target.Name}を たおした!" : $"{target.Name}は たおれてしまった!";
}
// HPが0になった相手の演出をして、メッセージを出す(1人を相手にするコマンド用)
protected async Awaitable ShowFallAsync(BattleUnit target, BattleContext context)
{
await PlayFallAsync(target);
await context.Message.ShowAsync(FallMessage(target));
}
- たたかう など1人を相手にするコマンドは、今までどおり
ShowFallAsyncを呼べばOKです - 全体のまほうでは、
PlayFallAsyncを全員分いっしょに始めて、全部終わってからFallMessageでメッセージを出します
攻撃のまほう・回復のまほうを、全体にも
#13のAttackSpellCommandのSelectAsyncとExecuteAsyncを、次のように書き換えます。
public override async Awaitable<BattleAction> SelectAsync(BattleUnit user, BattleContext context)
{
if (TargetsAll)
{
return await SelectAllEnemiesAsync(context);
}
return await SelectEnemyAsync(context);
}
public override async Awaitable ExecuteAsync(BattleUnit user, BattleAction action, BattleContext context)
{
// 全体なら、まだ立っている敵の全員。1人なら、ねらった相手(倒れていたら別の敵)
List<BattleUnit> targets = new List<BattleUnit>();
if (TargetsAll)
{
foreach (UnitView view in context.Enemies)
{
if (!view.Unit.IsDead)
{
targets.Add(view.Unit);
}
}
}
else
{
targets.Add(FindTarget(user, action, context));
}
SpendMp(user);
await context.Message.ShowAsync($"{user.Name}は {CommandName}を となえた!");
await user.View.PlayOrbAsync(); // 杖をふる
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.Magic(power, user, target);
target.TakeDamage(damages[i]);
PlayEffect(views[i]);
}
await context.Effects.PlayHitAllAsync(views, damages);
user.View.ResumeIdle();
// 倒れた相手は、全員いっしょに消える(1体ずつ順番に消すと、機械的に見えるので)
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;
}
// 演出が終わってから、1人ずつメッセージを出す
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));
}
}
}
- 1人のときも、相手が1人だけ入ったリストにすれば、同じ流れで書けます
- 倒した敵は、全員いっしょにしぼんで消えてから、「〇〇に 50の ダメージ!」「〇〇を たおした!」のメッセージが1人ずつ出ます
- 1行目に
using System.Collections.Generic;を足す
HealSpellCommandも、同じように書き換えます。
public override async Awaitable<BattleAction> SelectAsync(BattleUnit user, BattleContext context)
{
if (TargetsAll)
{
return await SelectAllAlliesAsync(context);
}
return await SelectAllyAsync(context);
}
public override async Awaitable ExecuteAsync(BattleUnit user, BattleAction action, BattleContext context)
{
// 全体なら、まだ立っている仲間の全員。1人なら、選んだ人(倒れていたら自分)
List<BattleUnit> targets = new List<BattleUnit>();
if (TargetsAll)
{
foreach (UnitView view in context.Party)
{
if (!view.Unit.IsDead)
{
targets.Add(view.Unit);
}
}
}
else if (action.Target.Unit.IsDead)
{
targets.Add(user); // 選んだ人が先に倒れていたら、自分を回復する
}
else
{
targets.Add(action.Target.Unit);
}
SpendMp(user);
await context.Message.ShowAsync($"{user.Name}は {CommandName}を となえた!");
await user.View.PlayCastAsync(); // 力をためる
// 全員いっしょに回復の演出を始めて、全部終わるまで待つ
List<Awaitable> heals = new List<Awaitable>();
int[] amounts = new int[targets.Count];
for (int i = 0; i < targets.Count; i++)
{
BattleUnit target = targets[i];
PlayEffect(target.View);
amounts[i] = target.Heal(DamageCalculator.Heal(power, user));
heals.Add(context.Effects.PlayHealAsync(target.View, amounts[i]));
}
foreach (Awaitable heal in heals)
{
await heal;
}
user.View.ResumeIdle();
for (int i = 0; i < targets.Count; i++)
{
await context.Message.ShowAsync($"{targets[i].Name}の HPが {amounts[i]} かいふくした!");
}
}
- 回復は、#8と同じ「始めておいて、あとで待つ」で、全員の緑の数字をいっしょに出しています
- 1行目に
using System.Collections.Generic;を足す

レベルでまほうを覚えよう

この節では、レベルが上がったら、新しいまほうを覚えるようにします。
覚えるまほうと、覚えるレベル
#13では、CharacterDataのSkillsに、使えるまほうをそのまま並べていました。これを「まほう」と「覚えるレベル」の組にします。
- 「SkillLearning」スクリプトを作る
// 覚えるまほう1つ分:何レベルで、どのまほうを覚えるか
[System.Serializable]
public class SkillLearning
{
public int level; // 覚えるレベル
public SkillCommand skill;
}
- #2 キャラクターと敵のデータのStatsと同じ、
[System.Serializable]の小さなクラスです。Inspectorで「Level」と「Skill」の組を並べられます
CharacterDataのskillsとSkillsを、次のように書き換えます。
[SerializeField] private SkillLearning[] skills; // 覚えるまほうと、覚えるレベル
public SkillLearning[] Skills => skills;
本を読むポーズの絵も足します(変数はcastFramesの下、プロパティはCastFramesの下)。
[SerializeField] private Sprite[] bookFrames; // 本を読む(book_01・02・03)
public Sprite[] BookFrames => bookFrames;
BattleUnitで、覚えたまほうを持つ
BattleUnitのSkillsの行を、次のように書き換えます。
public List<SkillCommand> Skills { get; } = new List<SkillCommand>(); // 覚えたまほう・わざ(仲間だけ)
- 1行目に
using System.Collections.Generic;を足す
TakeDamageの上に、まほうを覚えるメソッドを足します。
// 今のレベルで覚えられるまほうのうち、まだ覚えていないものを覚える。新しく覚えたものを返す
public List<SkillCommand> LearnSkills()
{
List<SkillCommand> learned = new List<SkillCommand>();
foreach (SkillLearning learning in characterData.Skills)
{
if (learning.level <= Level && !Skills.Contains(learning.skill))
{
Skills.Add(learning.skill);
learned.Add(learning.skill);
}
}
return learned;
}
- Skills.Contains:リストにもう入っているかを調べる。同じまほうを2回覚えないように
- データの並び順のまま覚えるので、まほうの一覧も、覚える順(レベルの低い順)に並びます
仲間のコンストラクタを、次のように書き換えます。
// 仲間のデータから作る。totalExp は、最初から持っている経験値の合計(今は確認用。#17 でロビーから持ち越す)
public BattleUnit(CharacterData data, UnitView view, int totalExp)
{
Name = data.CharacterName;
characterData = data;
TotalExp = totalExp;
Level = LevelTable.LevelFor(TotalExp);
Stats = Stats.AtLevel(data.BaseStats, data.Growth, Level);
Hp = Stats.maxHp;
Mp = Stats.maxMp;
LearnSkills();
View = view;
view.Unit = this;
Commands = data.Commands;
}
- レベルを経験値から決めて、そのレベルまでに覚えるまほうを、最初に全部覚えます
Levelの行の= 1は、そのままでOKです(敵は1のまま)
確かめるために、最初の経験値を決める
みんなでヒール(Lv6)や いなずま(Lv7)を確かめるには、そこまでレベルを上げる必要があります。何十回も戦うのは大変なので、BattleLineupに確認用の欄を作って、最初から経験値を持たせます。
BattleLineupに、欄を足します(statusWindowの下)。
[SerializeField] private int startExp = 0; // 確認用:仲間が最初から持っている経験値の合計(0ならLv1)
Setupの、仲間を作るところを次のように書き換えます。
BattleUnit unit = new BattleUnit(character, view, startExp);
unit.Row = row;
StatusRows[i] = row;
PartyUnits[i] = unit;
// レベルと能力を、ステータスの行に出す
row.SetLevel(unit.Level);
row.SetHp(unit.Hp, unit.Stats.maxHp);
row.SetMp(unit.Mp, unit.Stats.maxMp);
| Start Exp | レベル | 覚えているまほう(Lv1のほかに) |
|---|---|---|
| 0 | Lv1 | - |
| 45 | Lv4 | スライムちゃん:ぷるんアタック |
| 100 | Lv6 | + フェアリー:みんなでヒール |
| 140 | Lv7 | + メドゥーサ:いなずま |
本を読むポーズで知らせる
UnitViewに、変数を足して(castFramesの下)、
private Sprite[] bookFrames; // 本を読む(仲間だけ)
SetupCharacterの最後に1行足します。
bookFrames = data.BookFrames;
PlayCastAsyncの下に、メソッドを足します。
// まほうを覚えた:正面を向いて、本を読む
public async Awaitable PlayBookAsync()
{
await PlayFramesAsync(bookFrames);
}
#12 経験値とレベルアップのBattleManagerのShowLevelUpAsyncの最後に、覚えたまほうを知らせるところを足します。
// 新しいレベルで覚えたまほうを、1つずつ知らせる
foreach (SkillCommand skill in unit.LearnSkills())
{
Awaitable book = unit.View.PlayBookAsync();
await messageWindow.ShowAsync($"{unit.Name}は {skill.CommandName}を おぼえた!");
await book;
}
最後に、MagicCommandの一覧を開くところで、リストを配列にして渡します(CommandMenuは配列を受け取るので)。
BattleCommand skill = await context.SkillMenu.WaitForCommandAsync(user.Skills.ToArray(), user);
ListのToArray()は、中身を同じ順に入れた配列を作ります
データを作る
- Data/Skillsに、まほうを3つ作る
| ファイル | 作り方 | Command Name | Mp Cost | Effect Prefab | Targets All | ほか |
|---|---|---|---|---|---|---|
| PurunAttack | Multi Hit | ぷるんアタック | 4 | なし | - | Damage Rate 1.5、Hit Count 1 |
| AllHeal | Heal Spell | みんなでヒール | 8 | HealSparkles | チェック | Power 14 |
| Thunder | Attack Spell | いなずま | 9 | MagicBurst | チェック | Power 22 |
ぷるんアタックは、#13のれんげき(MultiHitSkillCommand)を、1.5倍で1回にしたものです。1回だけのときに「あわせて」と出ないように、MultiHitSkillCommandのメッセージの行を書き換えます。
// 2回以上なら「あわせて」を付ける
string together = hitCount > 1 ? "あわせて " : "";
await context.Message.ShowAsync($"{target.Name}に {together}{total}の ダメージ!");
- 4人のデータのSkillsを、Level と Skill の組に入れ直す
| キャラ | Skills |
|---|---|
| スライムちゃん | Lv1 SmallHeal、Lv4 PurunAttack |
| フェアリー | Lv1 Heal、Lv6 AllHeal |
| キョンシー | Lv1 DoubleStrike |
| メドゥーサ | Lv1 MagicArrow、Lv7 Thunder |
- 4人のデータのBook Framesに、
<名前>_book_01・02・03をドラッグ

SkillsをSkillLearningの組に変えると、前に入れていたまほう(#13のSmallHealなど)は外れます。4人とも、Level 1のまほうから入れ直しましょう。
動かして確認!

全体のまほう
- BattleのBattle LineupのStart Expを140、Partyの3人目をメドゥーサにする
- 1ターン目:3人とも「ぼうぎょ」(敵の攻撃でHPを減らしておく)
- 2ターン目:スライムちゃんは ぼうぎょ、フェアリーは まほう > みんなでヒール、メドゥーサは まほう > いなずま
覚えるまほう

- Start Expを95(Lv5、あと5でLv6)、Partyの3人目をキョンシーに戻す
- 3人とも「たたかう」で敵を全員倒す(11ポイントもらって106になり、Lv6に上がる)
#14の動作確認
- 全体のまほうを選ぶと、対象の全員が光る。Enterで決まり、Escで一覧に戻る
- いなずま は、立っている敵の全員に同時に当たる。揺れは1回だけ。倒した敵は全員いっしょに消えて、そのあとメッセージが出る
- みんなでヒール は、立っている仲間の全員に緑の数字が出る(HPが満タンの人にも「+0」が出るので、効いたことが分かる)
- レベルが上がって覚えるまほうがあると、本を読むポーズで「〇〇を おぼえた!」
- 確かめたら、Start Expを0、Partyの3人目をキョンシーに戻す
あとがき
今回は、全体のまほうと、レベルで覚えるまほうを作りました。全員を光らせるだけで、全体に効くことがひと目で分かりますね。次回は、どく・石化などの「状態」を変えるまほうを作りたいと思います!
コメント
コメント一覧 (1件)
[…] 前回:#14 全体のまほうと、覚えるまほう […]