
ここまでのまほうは、ダメージを与えるか、HPを回復するかの2つでした。今回は、相手を「どく」にしたり、石にして動けなくしたり、味方の守りをかためたりする状態を変えるまほうを作ります。状態は何ターンか続いて、アイコンでひと目で分かるようにします。RPGらしい駆け引きが、ぐっと増えますよ!
- シリーズの目次:ターン制RPGの作り方
- 前回:#14 全体のまほうと、覚えるまほう
今回作るもの
| 状態 | アイコン | 効果 | 続くターン |
|---|---|---|---|
| どく | 緑のしずく | ターンのおわりに、最大HPの4分の1のダメージ。体が緑っぽい色になる | 5 |
| せきか | 灰色の岩 | 動けない。体が灰色になる | 3 |
| ふうじ | おふだ | 動けない | 2 |
| ぼうぎょアップ | 青い盾 | 受けるダメージが0.7倍 | 3 |
| まほう | 使う人 | MP | 相手 | 効果 | 覚える Lv |
|---|---|---|---|---|---|
| まもりの風 | フェアリー | 5 | 味方全員 | ぼうぎょアップ | 3 |
| へびのどく | メドゥーサ | 4 | 敵1体 | どく | 3 |
| おふだ | キョンシー | 4 | 敵1体 | ふうじ | 5 |
| せきかのまなざし | メドゥーサ | 7 | 敵全体 | せきか(40%の確率) | 5 |
- 状態を、#2 キャラクターと敵のデータと同じScriptableObjectのデータにする(
StatusEffectData) - 状態をかけるまほう(
StatusSpellCommand) - 状態のアイコンを、敵の頭の上と、ステータスのウインドに出す(
EffectIcons) - ターンのおわりに、どくのダメージを受けて、状態の残りターンを減らす
- 生き返らせる「めざめの歌」は、次の#16で「めざましどけい」と一緒に作ります。ボスに せきか・ふうじ が効かないようにするのは#23です
| スクリプト | 役割 | 付ける場所 |
|---|---|---|
| StatusEffectData | 状態1つ分のデータ(名前・アイコン・続くターン・効果) | (付けない) |
| ActiveEffect | かかっている状態1つと、残りのターン | (付けない) |
| BattleUnit・DamageCalculator | 状態を持つ、動けるか、受けるダメージの倍率 | - |
| EffectIcons | 状態のアイコンを並べる | 敵のプレハブ・StatusRowのプレハブ |
| UnitView・StatusRow | アイコンと体の色を出す | - |
| StatusSpellCommand | 状態をかけるまほう | (付けない) |
| BattleManager | 動けない人を飛ばす、ターンのおわり | - |
状態のしくみ

この節では、状態をデータにして、BattleUnitが持てるようにします。
状態のデータ
どく・せきか・ふうじ・ぼうぎょアップは、効き方がちがうだけで、「何ターンか続いて、アイコンが付く」ところは同じです。そこで、状態の1つ1つを、効き方の数字を入れたデータにします。
- 「StatusEffectData」スクリプトを作る
using UnityEngine;
// 状態1つ分のデータ(どく・せきか・ふうじ・ぼうぎょアップ など)
[CreateAssetMenu(fileName = "NewStatusEffect", menuName = "TurnBasedRpg/Status Effect")]
public class StatusEffectData : ScriptableObject
{
[SerializeField] private string effectName; // 画面に出す名前
[SerializeField] private Sprite icon; // アイコン
[SerializeField] private Color color = Color.white; // 名前を飛び出させるときの色
[SerializeField] private Color tint = Color.white; // かかっている間の体の色(白なら変えない)
[SerializeField] private int turns = 3; // 続くターン(かけたターンも1と数える)
[SerializeField] private bool canAct = true; // チェックを外すと、動けなくなる(せきか・ふうじ)
[SerializeField] private float damageRate = 1f; // 受けるダメージの倍率(ぼうぎょアップは 0.7)
[SerializeField] private float poisonRate = 0f; // ターンのおわりに、最大HPのこの割合のダメージ(どくは 0.25)
public string EffectName => effectName;
public Sprite Icon => icon;
public Color Color => color;
public Color Tint => tint;
public int Turns => turns;
public bool CanAct => canAct;
public float DamageRate => damageRate;
public float PoisonRate => poisonRate;
}
- 状態ごとにクラスを作る方法もありますが、今回の4つは「数字がちがうだけ」なので、1つのデータの形にまとめました。データを作るだけで、新しい状態を増やせます
かかっている状態
データは「どくとは何か」を書いたもので、全員で同じものを使います。「この敵は、どくにかかっていて、あと2ターン」という、1人ずつちがう情報は別に持ちます。
- 「ActiveEffect」スクリプトを作る
// かかっている状態1つ:どの状態か(データ)と、残りのターン
public class ActiveEffect
{
public StatusEffectData Data { get; }
public int TurnsLeft { get; set; }
public ActiveEffect(StatusEffectData data)
{
Data = data;
TurnsLeft = data.Turns;
}
}
- #7 たたかう:ダメージを計算してメッセージを出そうで、データ(CharacterData)は読むだけにして、今のHPはBattleUnitに持たせたのと同じ考え方です
BattleUnitに状態を持たせる
BattleUnitに、状態のリストを足します(Skillsの下)。
public List<ActiveEffect> Effects { get; } = new List<ActiveEffect>(); // かかっている状態
LearnSkillsの上に、状態を扱うプロパティとメソッドを足します。
// 動けるか(せきか・ふうじ など、動けない状態が1つでもあれば false)
public bool CanAct
{
get
{
foreach (ActiveEffect effect in Effects)
{
if (!effect.Data.CanAct)
{
return false;
}
}
return true;
}
}
// 受けるダメージの倍率(ぼうぎょアップ などを、全部かけ合わせたもの)
public float DamageRate
{
get
{
float rate = 1f;
foreach (ActiveEffect effect in Effects)
{
rate *= effect.Data.DamageRate;
}
return rate;
}
}
// 状態をかける(もうかかっていたら、残りのターンを最初に戻す)
public void AddEffect(StatusEffectData data)
{
foreach (ActiveEffect effect in Effects)
{
if (effect.Data == data)
{
effect.TurnsLeft = data.Turns;
return;
}
}
Effects.Add(new ActiveEffect(data));
ShowEffects();
}
// ターンのおわりに、残りのターンを1つ減らす。0になって消えた状態を返す
public List<ActiveEffect> CountDownEffects()
{
List<ActiveEffect> expired = new List<ActiveEffect>();
foreach (ActiveEffect effect in Effects)
{
effect.TurnsLeft--;
if (effect.TurnsLeft <= 0)
{
expired.Add(effect);
}
}
foreach (ActiveEffect effect in expired)
{
Effects.Remove(effect);
}
ShowEffects();
return expired;
}
// かかっている状態を、見た目(アイコン・体の色)に出す
public void ShowEffects()
{
View.ShowEffects(Effects);
if (Row != null)
{
Row.ShowEffects(Effects);
}
}
- CanAct・DamageRate:
getの中に書いた計算が、プロパティを読むたびに動きます。unit.CanActと書くだけで、今かかっている状態から「動けるか」が分かります - CountDownEffects:リストを
foreachで回している途中にRemoveすると、エラーになります。そこで、消すものをいったん別のリスト(expired)に集めてから、あとで消しています
HPが0になったら、状態は消しておきます。TakeDamageを、次のように書き換えます。
// ダメージを受ける(0より下にはしない)。倒れたら、かかっている状態は消える
public void TakeDamage(int damage)
{
Hp = Mathf.Max(0, Hp - damage);
if (IsDead && Effects.Count > 0)
{
Effects.Clear();
ShowEffects();
}
}
DamageCalculatorのPhysicalとMagicで、returnの前に1行ずつ足して、受けるダメージの倍率をかけます。
damage *= defender.DamageRate; // ぼうぎょアップ などの倍率
return Mathf.Max(1, Mathf.RoundToInt(damage));
状態のアイコンを出そう

この節では、かかっている状態を、アイコンで見せます。
- 素材のTurnBasedRpgAssets.zipに、アイコン4つ(
Sprites/Icons/icon_poison・icon_stone・icon_seal・icon_guard)を足しました。ダウンロードし直して、プロジェクトに入れる
EffectIcons
- 「EffectIcons」スクリプトを作る
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
// かかっている状態のアイコンを並べる(同じ GameObject の Horizontal Layout Group が並べてくれる)
public class EffectIcons : MonoBehaviour
{
[SerializeField] private float iconSize = 44f;
// 前のアイコンを消して、今の状態のアイコンを作り直す
public void Show(List<ActiveEffect> effects)
{
foreach (Transform child in transform)
{
Destroy(child.gameObject);
}
foreach (ActiveEffect effect in effects)
{
GameObject icon = new GameObject(effect.Data.EffectName, typeof(RectTransform), typeof(Image));
icon.transform.SetParent(transform, false);
((RectTransform)icon.transform).sizeDelta = new Vector2(iconSize, iconSize);
Image image = icon.GetComponent<Image>();
image.sprite = effect.Data.Icon;
image.raycastTarget = false;
}
}
}
- アイコンは数が変わるので、プレハブを置いておくのではなく、
new GameObjectでその場で作っています。typeof(Image)を渡すと、Imageの付いたGameObjectが作られます
アイコンを置く場所を作ります。
- Prefabs/Enemyを開いて、子に空のGameObject「EffectIcons」
- アンカー:top-center、Pos Y:20(頭の少し上)、Width:300、Height:64
- Add Component>Horizontal Layout Group:Spacing 6、Child Alignment:Middle Center、Control Child Size・Child Force Expandのチェックは全部外す
- Add Component>EffectIcons、Icon Sizeを64にする(敵は離れて見るので、大きめに)
- Prefabs/StatusRowを開いて、NameTextの子に、同じように「EffectIcons」
- アンカー:middle-right、Pivot X:1、Pos X:0、Width:100、Height:44(名前の右はしに重ねる)
- Horizontal Layout GroupのChild AlignmentはMiddle Right

UnitViewとStatusRowで出す
UnitViewに、欄と変数を足します(欄はdimColorの下、変数はisDownの下)。
[SerializeField] private EffectIcons effectIcons; // 状態のアイコン(敵のプレハブだけ。仲間はステータスの行に出す)
private bool isDimmed; // 暗くしているか
private Color tint = Color.white; // 状態による体の色(せきかは灰色)
SetDimmedを書き換えて、ShowEffectsを足します。
// 暗くする(false で、状態の色に戻す。戦闘不能の人は暗いまま)
public void SetDimmed(bool dimmed)
{
isDimmed = dimmed;
image.color = (isDimmed || isDown) ? dimColor : tint;
}
// かかっている状態を見た目に出す:アイコンと、体の色(白以外の色の状態があれば、その色)
public void ShowEffects(List<ActiveEffect> effects)
{
if (effectIcons != null)
{
effectIcons.Show(effects);
}
tint = Color.white;
foreach (ActiveEffect effect in effects)
{
if (effect.Data.Tint != Color.white)
{
tint = effect.Data.Tint;
}
}
SetDimmed(isDimmed);
}
- せきかの間は体が灰色に、どくの間は具合が悪そうな緑っぽい色になります。暗くするときと、状態の色に戻すときを、
SetDimmedの1か所で決めています - 1行目に
using System.Collections.Generic;を足す - Prefabs/EnemyのUnit ViewのEffect Iconsに、EffectIconsをドラッグ
StatusRowにも、欄とメソッドを足します。
[SerializeField] private EffectIcons effectIcons;
public void ShowEffects(List<ActiveEffect> effects)
{
effectIcons.Show(effects);
}
- 欄は
mpGaugeの下、メソッドはSetHighlightの下に足す - 1行目に
using System.Collections.Generic;を足す - Prefabs/StatusRowのStatus RowのEffect Iconsに、NameTextの子のEffectIconsをドラッグ
状態をかけるまほう

この節では、状態をかけるまほうを作ります。
- Scripts/Commandsに「StatusSpellCommand」スクリプトを作る
using System.Collections.Generic;
using UnityEngine;
// 状態をかけるまほう(どく・せきか・ふうじ・ぼうぎょアップ など)
[CreateAssetMenu(fileName = "NewStatusSpell", menuName = "TurnBasedRpg/Skills/Status Spell")]
public class StatusSpellCommand : SkillCommand
{
[SerializeField] private StatusEffectData effect; // かける状態
[SerializeField] private bool forAllies; // チェックすると、味方にかける(まもりの風 など)
[SerializeField, Range(0, 100)] private int chance = 100; // かかる確率(%)
public override async Awaitable<BattleAction> SelectAsync(BattleUnit user, BattleContext context)
{
if (forAllies)
{
return TargetsAll ? await SelectAllAlliesAsync(context) : await SelectAllyAsync(context);
}
return TargetsAll ? await SelectAllEnemiesAsync(context) : await SelectEnemyAsync(context);
}
public override async Awaitable ExecuteAsync(BattleUnit user, BattleAction action, BattleContext context)
{
// かける相手:全体なら立っている全員、1人ならねらった相手(倒れていたら、敵は別の敵・味方は自分)
List<BattleUnit> targets = new List<BattleUnit>();
if (TargetsAll)
{
UnitView[] side = forAllies ? context.Party : context.Enemies;
foreach (UnitView view in side)
{
if (!view.Unit.IsDead)
{
targets.Add(view.Unit);
}
}
}
else if (forAllies && action.Target.Unit.IsDead)
{
targets.Add(user);
}
else
{
targets.Add(FindTarget(user, action, context));
}
SpendMp(user);
await context.Message.ShowAsync($"{user.Name}は {CommandName}を となえた!");
if (forAllies)
{
await user.View.PlayCastAsync(); // 味方には、力をためる
}
else
{
await user.View.PlayOrbAsync(); // 敵には、杖をふる
}
// 全員いっしょに、かかったかを決めて、名前を飛び出させる
bool[] isApplied = new bool[targets.Count];
for (int i = 0; i < targets.Count; i++)
{
BattleUnit target = targets[i];
isApplied[i] = GameRandom.Range(0f, 100f) < chance;
if (isApplied[i])
{
target.AddEffect(effect);
PlayEffect(target.View);
context.Effects.ShowText(target.View, effect.EffectName, effect.Color);
}
else
{
context.Effects.ShowText(target.View, "ミス", Color.gray);
}
}
await Awaitable.WaitForSecondsAsync(0.6f);
user.View.ResumeIdle();
// そのあとで、1人ずつメッセージ
for (int i = 0; i < targets.Count; i++)
{
if (isApplied[i])
{
await context.Message.ShowAsync($"{targets[i].Name}は {effect.EffectName}に なった!");
}
else
{
await context.Message.ShowAsync($"{targets[i].Name}には きかなかった!");
}
}
}
}
- かかる確率:
GameRandom.Range(0f, 100f) < chanceで、chanceが40なら40%の割合でtrueになります。結果に関わるので、#7のGameRandomを使います - [SerializeField, Range(0, 100)]:
Rangeを付けると、Inspectorで0〜100のスライダーになります - 演出は#14 全体のまほうと、覚えるまほうと同じく、全員いっしょに出してから、メッセージを1人ずつにしています
- かからなかったときも、「ミス」の文字を出します。何も起きないと、まほうが効いたのか分からなくて不安になるからです
- 待つのは0.6秒だけなので、ScriptableObjectのまほうでは
destroyCancellationTokenを渡さずに待っています(ScriptableObjectにはdestroyCancellationTokenが無いため)
ターンのはじめとおわり

この節では、動けない人を飛ばして、ターンのおわりに、どくのダメージと残りターンの処理をします。
動けない人を飛ばす
#9 バトルの流れを作ろうのBattleManagerのExecuteActionsAsyncに、動けない人を飛ばすifを足します。action.User.IsDeadのifの下です。
if (!action.User.CanAct)
{
// せきか・ふうじ などで動けない
await messageWindow.ShowAsync($"{action.User.Name}は うごけない!");
continue;
}
ExecuteActionsAsyncの最後(foreachの閉じかっこの下)に、ターンのおわりを呼ぶ行を足します。
// 勝ち負けが決まっていなければ、ターンのおわり
if (!context.IsEscaped && LivingEnemyViews().Length > 0 && !IsPartyDown())
{
await EndTurnAsync();
}
ターンのおわり
BattleEffectsに、どくのダメージの演出を足します(PlayHealAsyncの下)。
// どく などで、何人かが同時にダメージを受ける:色の付いた数字を出して、のけぞる(ヒットストップと揺れは無し)
public async Awaitable PlayStatusDamageAsync(UnitView[] targets, int[] damages, Color[] colors)
{
List<Awaitable> hits = new List<Awaitable>();
for (int i = 0; i < targets.Length; i++)
{
UnitView target = targets[i];
BattleUnit unit = target.Unit;
if (unit.Row != null)
{
unit.Row.SetHp(unit.Hp, unit.Stats.maxHp);
}
ShowText(target, damages[i].ToString(), colors[i]);
hits.Add(target.HitAsync());
}
foreach (Awaitable hit in hits)
{
await hit;
}
}
- たたかれたときより控えめにするため、ヒットストップと画面の揺れは入れません。数字の色(黄色がかった緑)で、どくのダメージだと分かります。回復の数字は明るい緑なので、少し色を変えて見分けられるようにしています
BattleManagerに、ターンのおわりのメソッドを足します(ExecuteActionsAsyncの下)。
// ターンのおわり:どくのダメージを受けて、状態の残りターンを1つ減らす
private async Awaitable EndTurnAsync()
{
List<BattleUnit> living = new List<BattleUnit>();
foreach (BattleUnit unit in lineup.PartyUnits)
{
if (!unit.IsDead)
{
living.Add(unit);
}
}
foreach (BattleUnit unit in lineup.EnemyUnits)
{
if (!unit.IsDead)
{
living.Add(unit);
}
}
// どく:全員いっしょにダメージを受けてから、1人ずつメッセージ
List<BattleUnit> poisoned = new List<BattleUnit>();
List<int> damages = new List<int>();
List<Color> colors = new List<Color>();
List<string> names = new List<string>();
foreach (BattleUnit unit in living)
{
foreach (ActiveEffect effect in unit.Effects)
{
if (effect.Data.PoisonRate > 0f)
{
poisoned.Add(unit);
damages.Add(Mathf.Max(1, Mathf.RoundToInt(unit.Stats.maxHp * effect.Data.PoisonRate)));
colors.Add(effect.Data.Color);
names.Add(effect.Data.EffectName);
break;
}
}
}
if (poisoned.Count > 0)
{
UnitView[] views = new UnitView[poisoned.Count];
for (int i = 0; i < poisoned.Count; i++)
{
BattleUnit unit = poisoned[i];
unit.TakeDamage(damages[i]);
views[i] = unit.View;
}
await battleEffects.PlayStatusDamageAsync(views, damages.ToArray(), colors.ToArray());
await FallTogetherAsync(poisoned);
for (int i = 0; i < poisoned.Count; i++)
{
await messageWindow.ShowAsync($"{poisoned[i].Name}は {names[i]}で {damages[i]}の ダメージ!");
}
foreach (BattleUnit unit in poisoned)
{
if (unit.IsDead)
{
await messageWindow.ShowAsync(unit.IsEnemy ? $"{unit.Name}を たおした!" : $"{unit.Name}は たおれてしまった!");
}
}
}
// 残りのターンを減らして、消えた状態を知らせる
foreach (BattleUnit unit in living)
{
if (unit.IsDead)
{
continue;
}
foreach (ActiveEffect effect in unit.CountDownEffects())
{
await messageWindow.ShowAsync($"{unit.Name}の {effect.Data.EffectName}が とけた!");
}
}
}
// 倒れた人を、全員いっしょに消す(敵はしぼんで消え、仲間は戦闘不能の絵に)
private async Awaitable FallTogetherAsync(List<BattleUnit> units)
{
List<Awaitable> falls = new List<Awaitable>();
foreach (BattleUnit unit in units)
{
if (!unit.IsDead)
{
continue;
}
if (unit.IsEnemy)
{
falls.Add(unit.View.DisappearAsync());
}
else
{
unit.View.ShowDown();
}
}
foreach (Awaitable fall in falls)
{
await fall;
}
}
- ダメージ:最大HPの4分の1(Poison Rate 0.25)。4ターンほうっておくと倒れるぐらいの、強めの設定です。HPの多い敵ほど、どくのダメージも大きくなります
- どくで何体も倒れたときも、#14と同じく、全員いっしょに消えてからメッセージを出します
- 続くターンの数え方:状態をかけたターンのおわりにも1つ減るので、Turns 3の せきか は「かけたターン+次の2ターン」動けません
データを作ろう
- TurnBasedRpg/Dataに「Effects」フォルダを作り、Create>TurnBasedRpg>Status Effect で4つ作る
| ファイル | Effect Name | Icon | Color | Tint | Turns | Can Act | Damage Rate | Poison Rate |
|---|---|---|---|---|---|---|---|---|
| Poison | どく | icon_poison | #9CC23A | #A8D08D | 5 | ✓ | 1 | 0.25 |
| Stone | せきか | icon_stone | #B0B0B0 | #8C8C8C | 3 | - | 1 | 0 |
| Seal | ふうじ | icon_seal | #FF9A3D | 白 | 2 | - | 1 | 0 |
| GuardUp | ぼうぎょアップ | icon_guard | #6FB7FF | 白 | 3 | ✓ | 0.7 | 0 |
- Data/Skillsに、Create>TurnBasedRpg>Skills>Status Spell で4つ作る
| ファイル | Command Name | Mp Cost | Effect Prefab | Targets All | Effect | For Allies | Chance |
|---|---|---|---|---|---|---|---|
| WindGuard | まもりの風 | 5 | HealSparkles | ✓ | GuardUp | ✓ | 100 |
| SnakePoison | へびのどく | 4 | MagicBurst | - | Poison | - | 100 |
| Talisman | おふだ | 4 | MagicBurst | - | Seal | - | 100 |
| StoneGaze | せきかのまなざし | 7 | MagicBurst | ✓ | Stone | - | 40 |
- 覚えるまほう(Skills)に足す:フェアリー Lv3 WindGuard、メドゥーサ Lv3 SnakePoison・Lv5 StoneGaze、キョンシー Lv5 Talisman
- Skillsは覚えるレベルの順に並べる(まほうの一覧も、この順に並びます)

動かして確認!

- BattleのBattle LineupのStart Expを70(Lv5)、Partyの3人目をメドゥーサにする
- 1ターン目:スライムちゃんは ぼうぎょ、フェアリーは まもりの風、メドゥーサは せきかのまなざし
- 2ターン目:スライムちゃん・フェアリーは ぼうぎょ、メドゥーサは へびのどく(せきかになっていない敵に)
#15の動作確認
- まもりの風で、ステータスの名前の右に盾のアイコンが付き、受けるダメージが減る
- せきかのまなざしで、かかった敵は灰色になって岩のアイコンが付き、「うごけない!」で行動しない。かからなかった敵には「ミス」
- へびのどくで、ターンのおわりに緑の数字でダメージを受ける
- 続くターンが終わると「〇〇の せきかが とけた!」で、アイコンが消えて元の色に戻る
- 確かめたら、Start Expを0、Partyの3人目をキョンシーに戻す
あとがき
今回は、状態をデータにして、どく・せきか・ふうじ・ぼうぎょアップを作りました。データの数字を変えるだけで、新しい状態も増やせますね。次回は、バトル中に使う「どうぐ」を作りたいと思います!
コメント
コメント一覧 (1件)
[…] 前回:#15 状態異常と補助のまほう […]