
前回で、攻撃したときの手応えを作りました。でも、敵のターンはまだ静かで、スライムちゃんがダメージを受けてもゲージが変わるだけです・・・。今回は、敵が踏み込んで攻撃してくる動きと、スライムちゃんが受けたとき・防いだとき・回復したときの演出を作ってみたいと思います!
- シリーズの目次:デッキ構築カードゲームの作り方
- 前回:#10 攻撃の手応えを作ろう
今回作るもの
- 敵が前へ踏み込んで攻撃して、元の位置へ戻る
- スライムちゃんがダメージを受けたとき:痛がる絵になって白く光り、のけぞる。画面のふちが赤く光る
- 防御を積んだとき:盾がポンと大きくなる
- 防御で防いだとき:盾がはねて、防いだ量を青い数字と青い火花で出す。全部防いだら揺れない
- 回復したとき:緑の数字と、上へ上がるキラキラ
- HPのゲージ:減った分を白く残して、あとから縮める
| スクリプト | 役割 | 付ける場所 |
|---|---|---|
| DamageVignette | 画面のふちを赤く光らせる | DamageVignette(新しく作る) |
| EnemyView(足す) | 前へ踏み込む・戻る | - |
| BattleManager(足す) | 敵が攻撃するときに踏み込ませる | - |
| HitReaction(足す) | 痛がる絵・画面のふち・盾・回復 | Slime にも付ける |
| Fighter(直す) | 防いだとき・防御を積んだとき・回復したときに HitReaction を呼ぶ | - |
| EffectSpawner・DamagePopup(足す) | 青い数字・緑の数字と、火花・キラキラ | - |
| HpGauge(足す) | 減った分を白く残す | - |
#10で作った HitReaction と EffectSpawner に足していくので、新しいスクリプトは1つだけです。
前回の続きから
前回の最後の状態から始めます。痛がるスライムちゃんと、画面のふちの絵は、素材のzipを入れ直すと入ります。
- DeckBuildingAssets.zip
DeckBuilding/Sprites/Slime/slime_damage.png(痛がるスライムちゃん。右向き、200×200)DeckBuilding/Sprites/Effects/fx_vignette.png(画面のふち。真ん中が透明で、ふちほど白い)- 自分で入れた場合は、Sprite Mode:Singleにして Apply
敵が踏み込んで攻撃する
今は、敵のターンになると、敵はその場から動かずにダメージだけが入ります。攻撃するときに前(左)へ踏み込んで、ぶつかったらダメージ、そして元の位置へ戻るようにします。
EnemyViewに足します。まず一番上にusing System.Collections;を足して、項目を足します。
using System.Collections;
using TMPro;
[SerializeField] private Sprite chargeIcon;
[Header("★#11 攻撃の動き")]
[SerializeField] private Vector2 lungeOffset = new Vector2(-180f, 0f); // 踏み込む向きと距離
[SerializeField] private float stepInSeconds = 0.12f; // 踏み込む時間
[SerializeField] private float stepOutSeconds = 0.25f; // 戻る時間
private int actionIndex; // 次に使う行動の番号
private Vector2 homePosition; // ★#11 並んだときの位置
Setupの最後で、並んだ位置を覚えておきます。
actionIndex = 0;
ShowIntent();
homePosition = ((RectTransform)transform).anchoredPosition; // ★#11 EnemyParty が置いた位置を覚えておく
}
踏み込む・戻るコルーチンを、ShowIntentの上に足します。
// ★#11 前へ踏み込む(だんだん速く)
public IEnumerator StepIn()
{
yield return Slide(homePosition, homePosition + lungeOffset, stepInSeconds);
}
// ★#11 元の位置へ戻る
public IEnumerator StepOut()
{
yield return Slide(homePosition + lungeOffset, homePosition, stepOutSeconds);
}
// ★#11 from から to へ、seconds 秒で動かす
private IEnumerator Slide(Vector2 from, Vector2 to, float seconds)
{
RectTransform rectTransform = (RectTransform)transform;
float time = 0f;
while (time < seconds)
{
time += Time.deltaTime;
float t = Mathf.Clamp01(time / seconds);
rectTransform.anchoredPosition = Vector2.Lerp(from, to, t * t);
yield return null;
}
rectTransform.anchoredPosition = to;
}
homePosition:EnemyPartyが敵を並べたときの位置。SetupはEnemyPartyが位置を決めたあとに呼ばれるので、ここで覚えます- 踏み込むのは0.12秒、戻るのは0.25秒
- 「素早く出て、ゆっくり戻る」と、ぶつかった勢いが出ます
t * t:#10のCardFlyと同じ「だんだん速く」の動き
BattleManagerの敵のターンで、攻撃の前後に踏み込ませます。
if (action.attack > 0)
{
yield return StartCoroutine(enemy.StepIn()); // ★#11 前へ踏み込む
player.TakeDamage(action.attack);
yield return StartCoroutine(enemy.StepOut()); // ★#11 元の位置へ戻る
}
yield return StartCoroutine(…)で、踏み込み終わるのを待ってからダメージを入れる- ぶつかった瞬間にダメージが入るので、次の節で作るスライムちゃんの反応とぴったり重なります
#9でBattleManagerの敵のターンをコルーチンにしておいたので、「踏み込む → ダメージ → 戻る」を上から順に書くだけで、動きの終わりを待てます。

ダメージを受けたとき
次は、スライムちゃんがダメージを受けたときの反応です。#10で敵に付けた HitReaction を、スライムちゃんにも付けます。敵とちがうのは3つです。
- 痛がる絵に切りかえる(待機の弾む動きは止める)
- のけぞる向きは左(敵は右から攻撃してくるので)
- 画面のふちを赤く光らせる(自分が受けたダメージだと、すぐ分かるように)
画面のふち(DamageVignette)
- Canvasの子にDamageVignette(UI (Canvas)>Image)
- PopupLayerのすぐ上(PopupLayerより1つ前)に置く。数字は赤いふちより手前に出ます
- アンカー:stretch-stretch、Left・Top・Right・Bottom:0
- Source Image:fx_vignette、Color:#E8433A
- Raycast Targetのチェックを外す(カードの操作をじゃましないように)
- DeckBuilding/Scriptsに「DamageVignette」スクリプトを作って、DamageVignetteに付ける
using UnityEngine;
using UnityEngine.UI;
// 画面のふちを赤く光らせて、だんだん消す(スライムちゃんがダメージを受けたとき)
[RequireComponent(typeof(Image))]
public class DamageVignette : MonoBehaviour
{
[SerializeField] private float fadeSeconds = 0.5f; // 消えるまでの時間
[SerializeField] private float maxAlpha = 0.6f; // 一番濃いときの濃さ
private Image image;
private float timeLeft;
private void Awake()
{
image = GetComponent<Image>();
SetAlpha(0f);
}
public void Play()
{
timeLeft = fadeSeconds;
}
private void Update()
{
if (timeLeft <= 0f)
{
return;
}
// ヒットストップ中も消えていくように、unscaledDeltaTime で減らす
timeLeft = Mathf.Max(0f, timeLeft - Time.unscaledDeltaTime);
SetAlpha(maxAlpha * timeLeft / fadeSeconds);
}
private void SetAlpha(float alpha)
{
Color color = image.color;
color.a = alpha;
image.color = color;
}
}
Awakeで透明にしておく- エディタでは赤いふちが見えたままでOK。再生すると消えます
Playで残り時間を0.5秒にして、Updateで濃さを0.6から0に下げていくSetAlpha:Colorは構造体なので、image.color.a = …とは書けません。いったん変数に取り出して、aを変えてから戻します

HitReactionに足す
HitReactionに、スライムちゃん用の項目を足します。
[SerializeField] private float hitStopSeconds = 0.06f; // ★#10 時間を止める長さ
[Header("★#11 スライムちゃん用")]
[SerializeField] private Sprite hurtSprite; // 痛がる絵(空なら絵を変えない)
[SerializeField] private float hurtSeconds = 0.5f; // 痛がる絵を出しておく時間
[SerializeField] private bool showVignette; // 画面のふちを赤くする(スライムちゃんだけチェック)
private RectTransform body;
private Image bodyImage; // ★#11
private IdleBounce idleBounce; // ★#11 痛がっている間は止める(付いていなければ null)
private Vector2 startPosition;
private ScreenShake screenShake; // ★#10
private DamageVignette damageVignette; // ★#11
Startは、flashImage.spriteの行を消して、2行を足します。
private void Start()
{
body = (RectTransform)transform;
startPosition = body.anchoredPosition;
bodyImage = GetComponent<Image>(); // ★#11
idleBounce = GetComponent<IdleBounce>(); // ★#11
flashImage.color = Color.clear;
effectSpawner = FindFirstObjectByType<EffectSpawner>(); // ★#10
hitStop = FindFirstObjectByType<HitStop>(); // ★#10
screenShake = FindFirstObjectByType<ScreenShake>(); // ★#10
damageVignette = FindFirstObjectByType<DamageVignette>(); // ★#11
}
flashImage.sprite = GetComponent<Image>().sprite;の行を消す- スライムちゃんは弾む動きや痛がる絵で絵がどんどん変わるので、Startで1回だけ入れた形では合いません。光る直前に入れるように、
FlashAndKnockbackへ移します
- スライムちゃんは弾む動きや痛がる絵で絵がどんどん変わるので、Startで1回だけ入れた形では合いません。光る直前に入れるように、
Playの最後に、画面のふちを足します。
screenShake.Play(damage); // ★#10 揺らす
if (showVignette)
{
damageVignette.Play(); // ★#11 画面のふちを赤く
}
}
FlashAndKnockbackの始めと終わりに足します。
private IEnumerator FlashAndKnockback()
{
// ★#11 痛がる絵に変える
if (hurtSprite != null)
{
if (idleBounce != null)
{
idleBounce.enabled = false;
}
bodyImage.sprite = hurtSprite;
}
flashImage.sprite = bodyImage.sprite; // ★#11 今の絵の形で光らせる(Start から移した)
// 白く光って、後ろへ押される
flashImage.color = Color.white;
body.anchoredPosition = startPosition;
// ★#11 少し痛がってから、待機の動きに戻す
if (hurtSprite != null)
{
yield return new WaitForSeconds(hurtSeconds);
if (idleBounce != null)
{
idleBounce.enabled = true;
}
}
}
- 痛がる絵にする間は、IdleBounceを
enabled = falseで止める- 止めないと、0.15秒後に待機の絵に上書きされてしまいます
enabled = trueに戻すと、IdleBounceが次のコマから待機の絵に戻してくれます- 敵は
hurtSpriteが空なので、今までと同じ動きのままです
スライムちゃんに付ける
- Stage>PlayerArea>Slimeの子にFlash(UI (Canvas)>Image)
- Slimeの子の一番上、アンカー:stretch-stretch、Left・Top・Right・Bottom:0
- Source Image:walk_61(仮。Preserve Aspectの項目を出すため)、Material:WhiteFlash
- Color:白、Alpha:0、Raycast Targetのチェックを外す、Preserve Aspectにチェック
- Slimeに Add Component>HitReaction
- Flash Image:Flash
- Knockback:X -40、Y 0(左へのけぞる)
- Hurt Sprite:slime_damage
- Show Vignette:チェックを入れる

最後に、Fighterで HitReaction を探すところを1か所直します。スライムちゃんの Fighter は PlayerArea に、HitReaction は子の Slime に付いているので、GetComponentでは見つかりません。
hitReaction = GetComponentInChildren<HitReaction>(); // ★#10(★#11 スライムちゃんは子の Slime に付くので InChildren に)
GetComponentInChildrenは、自分と子の中から探します。敵は自分に付いているので、敵もこれで見つかります

防御を積んだとき
下じきガードを使ったら、HPのゲージの横の盾が「ポン」と大きくなるようにします。盾は#8で作ったBlockBadgeです。
HitReactionに、盾の項目を足します。
[SerializeField] private bool showVignette; // 画面のふちを赤くする(スライムちゃんだけチェック)
[Header("★#11 防御の盾")]
[SerializeField] private RectTransform shieldIcon; // HpGauge の盾(BlockBadge)
Playの下に足します。
// ★#11 防御が増えたとき:盾がポンと大きくなる
public void PlayBlockGained()
{
PopShield();
}
// ★#11 盾を大きくする(Update で元の大きさに戻る)
private void PopShield()
{
shieldIcon.localScale = Vector3.one * 1.6f;
}
private void Update()
{
shieldIcon.localScale = Vector3.Lerp(shieldIcon.localScale, Vector3.one, Mathf.Min(1f, 12f * Time.deltaTime));
}
- #8の予告の「ポン」と同じ形です。1.6倍にして、Updateで1へ近づけます
FighterのAddBlockから呼びます。
public void AddBlock(int amount)
{
block += amount;
Debug.Log($"{fighterName}の防御 +{amount} 防御 {block}");
if (hitReaction != null)
{
hitReaction.PlayBlockGained(); // ★#11
}
}
盾をつなぎます。敵も防御をするので、敵のプレハブにもつなぎます。
- SlimeのHitReaction:Shield Icon にPlayerArea>BlockBadge
- Prefabs/EnemyのHitReaction:Shield Icon にBlockBadge
Shield Iconが空のままだと、Updateで毎フレームエラー(NullReferenceException)になります。スライムちゃんと敵のプレハブの両方に入れましょう。

防御で防いだとき
敵の攻撃を防御で受けとめたときは、盾がはねて、防いだ量を青い数字で出し、青い火花で弾きます。
青い火花のプレハブ
#10のHitSparksを複製して、色と数を変えます。各項目の意味は12-4 パーティクルで演出しよう:Particle Systemの基本で紹介しています。
- ProjectウインドでPrefabs/HitSparksを選んで Ctrl+D、名前をBlockSparks
- BlockSparksを開いて、次のように変える
- Start Lifetime:0.2〜0.35、Start Speed:5〜9、Start Size:0.3〜0.5
- Start Color:#9FD3FF・#FFFFFF
- Gravity Modifier:0.5
- EmissionのBurstsのCount:12
数字の色を変える
DamagePopupに、文字と色を変えるメソッドを足します(Updateの上)。
// ★#11 文字と色を変える(防いだ数字・回復の数字)
public void SetLabel(string label, Color color)
{
text.text = label;
text.color = color;
}
EffectSpawnerに足します。
[SerializeField] private ParticleSystem sparksPrefab;
[Header("★#11 防御と回復")]
[SerializeField] private ParticleSystem blockSparksPrefab;
[SerializeField] private Color blockColor = new Color(0.29f, 0.56f, 0.85f); // 防いだ数字の色(青)
// ★#11 防いだ量を青い数字で出して、青い火花で弾く
public void ShowBlocked(int blocked, Vector3 position)
{
ShowLabel(blocked.ToString(), blocked, blockColor, position);
Instantiate(blockSparksPrefab, position, Quaternion.identity);
}
// ★#11 文字と色を指定して、数字を出す
private void ShowLabel(string label, int amount, Color color, Vector3 position)
{
DamagePopup popup = Instantiate(damagePopupPrefab, popupLayer);
popup.transform.position = position;
popup.Setup(amount);
popup.SetLabel(label, color);
}
PlaySparksの下に足すShowLabel:Setupで大きさと動きを決めてから、SetLabelで文字と色だけ上書きします。数字の動きは#10と同じです
- EffectsのEffect Spawnerで、Block Sparks PrefabにPrefabs/BlockSparksを入れる
HitReactionとFighterから呼ぶ
HitReactionのPlayBlockGainedの上に足します。
// ★#11 防御で受けとめたとき:盾がはねて、青い数字と青い火花
public void PlayBlocked(int blocked)
{
PopShield();
effectSpawner.ShowBlocked(blocked, shieldIcon.position);
}
FighterのTakeDamageは、#10で足したifを次のように書きかえます。
// ★#10 HP が減ったら、反応を出す(★#11 防いだときの反応も)
if (hitReaction != null)
{
if (blocked > 0)
{
hitReaction.PlayBlocked(blocked);
}
if (damage > 0)
{
hitReaction.Play(damage);
}
}
- 防いだ量(
blocked)があれば、盾の反応 - HPが減った量(
damage)があれば、#10のダメージの反応 - 6の攻撃を防御5で受けると、両方が出ます(青い「5」と赤い「1」)
- 防御10で全部防ぐと、
damageが0なので全部防いだら揺れない。「完全に守りきった」ことが、揺れないことで伝わります
演出は「足す」だけでなく「出さない」ことでも伝わります。全部防いだときに揺れや赤いふちを出さないと、少しでも受けたときとの差がはっきりします。


回復のキラキラ
ばんそうこうを使ったら、緑の「+5」と、上へ上がるキラキラを出します。
キラキラのプレハブ
- Prefabs/HitSparksを Ctrl+D で複製して、名前をHealSparkles
- Start Lifetime:0.6〜1、Start Speed:1〜2.5、Start Size:0.25〜0.5
- Start Color:#7CFC9A・#FFFFFF
- Gravity Modifier:-0.5(マイナスにすると、上へ上がっていきます)
- Duration:1、EmissionのBurstsのCount:14、ShapeのRadius:0.8

EffectSpawner・HitReaction・Fighterに足す
EffectSpawnerの項目と、メソッドを足します。
[SerializeField] private Color blockColor = new Color(0.29f, 0.56f, 0.85f); // 防いだ数字の色(青)
[SerializeField] private ParticleSystem healSparklesPrefab;
[SerializeField] private Color healColor = new Color(0.18f, 0.68f, 0.36f); // 回復の数字の色(緑)
// ★#11 回復した量を緑の数字で出して、キラキラを上げる
public void ShowHeal(int amount, Vector3 position)
{
ShowLabel("+" + amount, amount, healColor, position);
Instantiate(healSparklesPrefab, position, Quaternion.identity);
}
ShowBlockedの下に足す- EffectsのEffect Spawnerで、Heal Sparkles PrefabにPrefabs/HealSparklesを入れる
HitReactionのPlayBlockGainedの下に足します。
// ★#11 回復したとき:緑の数字とキラキラ
public void PlayHeal(int amount)
{
effectSpawner.ShowHeal(amount, transform.position);
}
FighterのHealから呼びます。
public void Heal(int amount)
{
hp = Mathf.Min(maxHp, hp + amount);
Debug.Log($"{fighterName}のHPが{amount}回復 HP {hp}/{maxHp}");
if (hitReaction != null)
{
hitReaction.PlayHeal(amount); // ★#11
}
}

遅れて縮むHPゲージ
最後はHPのゲージです。大きなダメージを受けたとき、ゲージがパッと短くなるだけだと「どれぐらい減ったか」が見えにくいので、減った分をいったん白く残して、あとから縮めるようにします。格闘ゲームやRPGでよく見る形です。
ゲージの後ろに白いゲージを置く
- PlayerArea>HpBarの子にHpLag(UI (Canvas)>Image)
- HpBarの子の一番上(HpFillより先=後ろに描かれる)
- アンカーと位置・大きさは、HpFillと同じ(stretch-stretch、Left・Top・Right・Bottom:3)
- Source Image:ui_white、Color:白
- Image Type:Filled、Fill Method:Horizontal、Fill Origin:Left
- Raycast Targetのチェックを外す
- Prefabs/EnemyのHpBarにも、同じHpLagを作る

HpGaugeに足す
[SerializeField] private TextMeshProUGUI blockText;
[Header("★#11 減った分を白く残す")]
[SerializeField] private Image hpLag; // HpFill の後ろの白いゲージ
[SerializeField] private float lagDelay = 0.4f; // 減ってから縮み始めるまでの時間
[SerializeField] private float lagSpeed = 1.2f; // 縮む速さ(1秒でゲージ何本分か)
private Fighter fighter;
private float lagFill = 1f; // ★#11 白いゲージの長さ(0〜1)
private float previousFill = 1f; // ★#11 前のフレームの HP の割合
private float lagTimer; // ★#11 縮み始めるまでの残り時間
Updateのゲージのところを直して、UpdateLagを呼びます。
// HP のゲージと数字
float fill = (float)fighter.Hp / fighter.MaxHp;
hpFill.fillAmount = fill;
UpdateLag(fill); // ★#11
hpText.text = $"{fighter.Hp}/{fighter.MaxHp}";
一番下に足します。
// ★#11 白いゲージを、少し待ってから HP に追いつかせる
private void UpdateLag(float fill)
{
if (fill < previousFill)
{
lagTimer = lagDelay; // 減った瞬間から数え直す
}
previousFill = fill;
if (fill > lagFill)
{
lagFill = fill; // 回復したときは、すぐにそろえる
}
else if (lagTimer > 0f)
{
lagTimer -= Time.deltaTime;
}
else
{
lagFill = Mathf.MoveTowards(lagFill, fill, lagSpeed * Time.deltaTime);
}
hpLag.fillAmount = lagFill;
}
- HPが減った瞬間に、0.4秒のタイマーを始める
- 続けて減ったときは、そこから数え直すので、白い部分は最後のダメージから0.4秒待つ
- 待ち終わったら、
Mathf.MoveTowardsでfillまで一定の速さで縮めるMoveTowards(今, 目標, 1回に進める量)は、目標を追いこさずに近づく計算です
- 回復したときは、白いゲージもすぐにそろえる(白が赤より短いと、白が見えなくなるだけなので)
- PlayerAreaとPrefabs/EnemyのHp Gaugeで、Hp LagにHpLagを入れる

スライム赤と白の2本を重ねるだけなんですね。思ったより簡単でした



そうそう!白い部分が見えている間に「16も減った!」って分かるから、次は防御しようって思えるよね
動かして確認!
再生して、次の順番で遊んでみましょう。確かめやすいように、Starting Deckを「下じきガード3枚・ばんそうこう2枚」にすると、毎ターン同じ5枚になります(確かめたら戻しましょう)。
- 1ターン目:下じきガード2枚(防御10)→ ターン終了。ブリキロボの6を全部防ぐ
- 2ターン目:下じきガード1枚(防御5)→ ターン終了。ブリキロボの6のうち1と、びっくり箱の16を受ける
- 3ターン目:ばんそうこう2枚で回復


#11の動作確認
- 敵が攻撃するとき、左へ素早く踏み込んで、ゆっくり元の位置へ戻る
- スライムちゃんがダメージを受けると、痛がる絵になって白く光り、左へのけぞる。少しすると待機の動きに戻る
- ダメージを受けると、画面のふちが赤く光って消える
- 防御を積むと、盾がポンと大きくなる
- 防御で防ぐと、盾のところから青い数字と青い火花が出る。全部防いだときは、スライムちゃんは光らず、画面も揺れない
- 回復すると、緑の「+5」とキラキラが上へ上がる
- HPが減ると、減った分が白く残って、少しあとから縮む。敵のゲージも同じ
あとがき
今回は、敵のターンにも手応えを付けて、受けたとき・防いだとき・回復したときが、見ただけで分かるようにしました。特に「全部防いだら揺れない」は、何も起きないことで守りきった気持ちよさが出るので、ぜひ試してみてください。次回は、敵を全部倒したとき・スライムちゃんのHPが0になったときの勝ち負けと、勝ったときにもらえる報酬のカード選びを作りたいと思います!
コメント
コメント一覧 (1件)
[…] 前回:#11 守る・受ける・回復するときの演出 […]