
前回で攻撃に手応えが出ました。でもこのゲーム、まだ一度も負けません。敵に囲まれても平気なので、逃げる理由も、強化を選ぶ意味もありませんでした。今回はヴァンサバ風サバイバーゲームのシリーズの9回目として、スライムちゃんにHPを持たせて、やられる側の演出とゲームオーバーを作ってみたいと思います!
- シリーズの目次:ヴァンサバ風ゲームの作り方
- 前回:#8 手応えのある攻撃の演出を作ろう
今回作るもの
- スライムちゃんにHPを持たせる(100から減る)
- 敵に触れられるとHPが減る。続けて減りすぎないように無敵時間を入れる
- ダメージを受けたら点滅して、画面が赤く光る
- 画面の上にHPゲージを出す
- HPが0になったらゲームオーバー。もう一度遊べるようにする
作る・直すスクリプトは4つです。
| スクリプト | 役割 | 付ける場所 |
|---|---|---|
| PlayerHealth(新しく作る) | HP・無敵時間・点滅・やられた合図 | Player |
| GameOverView(新しく作る) | HPが0になったら止めて、パネルを出す | Canvas |
| StatusView(直す) | HPゲージと、被弾の赤い光 | Canvas |
| Enemy(直す) | 触れたときに与えるダメージを持つ | 敵のプレハブ |
前回の続きから
- Unityのバージョンは6.3 LTS
- 記事作成時点では6000.3.22f1で確認しています
- #8の状態:攻撃が当たると数字が出て、敵がのけぞって倒れる
当たり判定のイベントは6-10 当たり判定のイベントとタグで紹介しています。
敵に「触れたときのダメージ」を持たせよう
まずEnemyに1つ足します。
[SerializeField] private int touchDamage = 10; // 触れたときに与えるダメージ
// プレイヤー側から読む
public int TouchDamage => touchDamage;
- 敵のほうに持たせる理由
- #10で3種類の敵を出します。硬い敵は痛い、弱い敵は軽いというように、敵ごとに変えたくなるからです
- プレイヤー側に「触れたら10」と書いてしまうと、敵を増やすたびにプレイヤーを直すことになります
HPを持たせよう(PlayerHealth)
SurvivorsLike/Scriptsに「PlayerHealth」を作る
using System;
using UnityEngine;
// スライムちゃんのHP。敵に触れられると減り、0になったら終わり
public class PlayerHealth : MonoBehaviour
{
[SerializeField] private int maxHp = 100;
[SerializeField] private float invincibleSeconds = 0.5f; // 続けて減らない時間
[SerializeField] private float blinkInterval = 0.08f; // 点滅の速さ
[SerializeField] private SpriteRenderer spriteRenderer;
public int Hp { get; private set; }
public int MaxHp => maxHp;
// ダメージを受けたこと・やられたことを、ほかのスクリプトに知らせる
public event Action Damaged;
public event Action Died;
private float invincibleTimer;
private void Awake()
{
Hp = maxHp;
}
private void Update()
{
if (invincibleTimer <= 0f)
{
return;
}
invincibleTimer -= Time.deltaTime;
// 無敵の間だけ、一定の間隔で見えたり消えたりさせる
bool visible = invincibleTimer <= 0f || (int)(invincibleTimer / blinkInterval) % 2 == 0;
spriteRenderer.enabled = visible;
}
private void OnTriggerStay2D(Collider2D other)
{
if (Hp <= 0 || invincibleTimer > 0f)
{
return;
}
if (!other.TryGetComponent(out Enemy enemy))
{
return;
}
Hp = Mathf.Max(0, Hp - enemy.TouchDamage);
invincibleTimer = invincibleSeconds;
Damaged?.Invoke();
if (Hp <= 0)
{
spriteRenderer.enabled = true;
Died?.Invoke();
}
}
}
OnTriggerStay2Dを使う- 敵は触れたまま止まるので、「入った瞬間」の
OnTriggerEnter2Dだと最初の1回しかダメージが入りません - 触れている間ずっと呼ばれる
OnTriggerStay2Dにして、無敵時間で回数を抑えます
- 敵は触れたまま止まるので、「入った瞬間」の
- 無敵時間(0.5秒)
- これが無いと、敵1体に触れているだけで1秒に何十回もダメージが入り、一瞬で終わります
- 「何秒に1回まで」を決めるのは、#5のホイッパーでやったことと同じ考え方です
- 点滅のしかた
invincibleTimer / blinkIntervalの整数部分が偶数か奇数かで、見える・見えないを切り替えています- コルーチンでも書けますが、この式1つで済むので今回はこちらにしました
eventで知らせる- HPを減らすことと、赤く光らせること・ゲームオーバーの画面を出すことは別の仕事です
PlayerHealthは「減った」「やられた」とだけ言い、あとは受け取った側にまかせます
Playerに付ける
- HierarchyでPlayerを選び、Add Component>PlayerHealth
- Sprite Rendererの欄に、Player自身のSprite Rendererを入れる

減らないときは、当たり判定が届いているかを見る
敵は#3で決めたKeep Distance(0.5)まで近づくと止まります。この距離は、プレイヤーのCollider(半径0.2・少し下にずらしてある)と敵のCollider(半径0.35)がちょうど触れるか触れないかのギリギリです。手元では、囲まれると数秒でHPが0になるところまで確認出来ました。
もしまったく減らないときは、数字をいじる前にSceneビューで2つの緑の円が重なっているかを見てください。円が重なっていなければ、どんな数字を入れても当たりません。重なっていなければKeep Distanceを0.35ぐらいに小さくします。
HPゲージと、赤い光を出そう
画面に置く
- Canvasの
ExpBarを選んでCtrl+Dで複製し、名前を「HpBar」にする- Pos Yを-75にする(経験値バーのすぐ下)
- 中の
ExpBarFillの名前を「HpBarFill」に、Colorを赤にする
- Canvasの上で右クリック>UI>Image、名前を「DamageFlash」にする
- Anchor Presetsでstretch・stretchをAltキーを押しながら選び、画面いっぱいに広げる
- Source Image:ui_white、Color:赤、アルファ(A)は0
- Raycast Target のチェックを外す
- HierarchyでLevelTextの下・UpgradePanelの上に置く(強化のカードより奥)
DamageFlashは、ふだんは透明でも画面全体をおおっています。Raycast Targetにチェックが入ったままだと、クリックをDamageFlashが受け止めてしまい、強化のカードが押せなくなります。「カードが反応しない」ときは、まずここを見てください。

StatusViewに足す
[SerializeField] private PlayerHealth playerHealth;
[SerializeField] private Image hpBarFill;
[SerializeField] private Image damageFlash;
[SerializeField] private float flashAlpha = 0.35f; // 光る強さ
[SerializeField] private float flashSeconds = 0.3f; // 消えるまでの秒数
OnEnableとOnDisableに1行ずつ足します。
playerHealth.Damaged += ShowFlash; // OnEnable
playerHealth.Damaged -= ShowFlash; // OnDisable
Updateの先頭に1行と、光らせる処理を足します。
hpBarFill.fillAmount = (float)playerHealth.Hp / playerHealth.MaxHp;
private void ShowFlash()
{
StopCoroutine(nameof(FlashRoutine));
StartCoroutine(nameof(FlashRoutine));
}
private IEnumerator FlashRoutine()
{
float timer = flashSeconds;
while (timer > 0f)
{
timer -= Time.unscaledDeltaTime;
float alpha = flashAlpha * (timer / flashSeconds);
damageFlash.color = new Color(1f, 0f, 0f, alpha);
yield return null;
}
damageFlash.color = new Color(1f, 0f, 0f, 0f);
}
Time.unscaledDeltaTimeで消すTime.deltaTimeにすると、やられてtimeScaleが0になった瞬間に画面が赤いまま固まります- #8のレベルアップの演出と同じで、止まっている間も消えてほしいUIの演出はこちらを使います
StopCoroutine(nameof(...))にした理由- #8で
StopAllCoroutines()を使いましたが、StatusViewにはレベルアップのポップも動いています - 全部止めると、被弾のたびにレベルアップの演出が消えてしまいます。名前を指定して、赤い光だけを止め直します
StopCoroutineに名前を渡して止められるのは、名前で始めたコルーチンだけです。名前で止めるときは、始めるときもStartCoroutine(nameof(...))と名前で書きます
- #8で
スライムStopCoroutineに名前を渡しているのに止まらない・・・というときは、始めるほうがStartCoroutine(FlashRoutine())のままになっていないか見てください
同じ理由で、ShowLabelの中も次のように直してください。
StopCoroutine(nameof(PopLabel));
StartCoroutine(nameof(PopLabel));
- HierarchyでCanvasを選び、増えた欄にPlayer・HpBarFill・DamageFlashを入れる
ゲームオーバーを作ろう
パネルを置く
- CanvasのUpgradePanelを選んでCtrl+Dで複製し、名前を「GameOverPanel」にする
- 中の
Cardsは削除する - Colorのアルファを200ぐらいに上げる(もう操作できないので、しっかり暗くする)
- 中の
GameOverPanelの上で右クリック>UI>Text – TextMeshPro、名前を「GameOverText」にする- Text:GAME OVER、Font Size:110、Color:赤、Alignment:中央・中央、Pos Y:120
GameOverPanelの上で右クリック>UI>Button – TextMeshPro、名前を「RetryButton」にする- Pos Y:-120、Width:420、Height:110
- 中のTextを「もう一度」、Font Size:40にする
GameOverPanelのチェックを外して、最初は非表示にする- Hierarchyで一番下(LevelUpLabelより下)に置く
スクリプト
SurvivorsLike/Scriptsに「GameOverView」を作る
using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEngine.UI;
// HPが0になったらゲームを止めて、パネルを出す
public class GameOverView : MonoBehaviour
{
[SerializeField] private PlayerHealth playerHealth;
[SerializeField] private GameObject panel;
[SerializeField] private Button retryButton;
private void OnEnable()
{
playerHealth.Died += Show;
retryButton.onClick.AddListener(Retry);
}
private void OnDisable()
{
playerHealth.Died -= Show;
retryButton.onClick.RemoveListener(Retry);
}
private void Show()
{
panel.SetActive(true);
Time.timeScale = 0f;
}
private void Retry()
{
// 止めたまま読み込み直すと、次のゲームも止まったままになる
Time.timeScale = 1f;
SceneManager.LoadScene(SceneManager.GetActiveScene().name);
}
}
Time.timeScale = 1fを戻すのを忘れない- timeScaleはシーンを読み込み直しても元に戻りません。0のまま次のゲームが始まって、「何も動かない」と悩むのは、よくあるつまずきです
- 同じシーンを読み込み直す
- 敵も金平糖も強化も、全部まとめて最初の状態に戻ります。作り込んだリセット処理を書かずに済む、いちばん手軽なやり直し方です
- タイトル画面からのちゃんとした流れは、#11で作ります
- HierarchyでCanvasを選び、Add Component>GameOverView
- Player Health:Player、Panel:GameOverPanel、Retry Button:RetryButton





うう、囲まれるとあっという間にやられてしまいます・・・



それでいいんだよ!逃げる理由ができて、はじめて移動が楽しくなるんだから
動かして確認!


#9の動作確認
- 敵に触れられるとHPゲージが減る(1回10)
- 減った瞬間に画面が赤く光り、スライムちゃんが点滅する
- 触れっぱなしでも、0.5秒に1回までしか減らない
- 点滅が終わると、スライムちゃんはちゃんと見えたままになる
- HPが0になるとゲームが止まって「GAME OVER」が出る
- 「もう一度」を押すと最初から遊べる(止まったままにならない)
- レベルアップの強化を選んでいる間は、ダメージも入らない(
timeScaleが0なので)
バランスを考えよう
| 変えたいこと | 変える数字 |
|---|---|
| やられにくくする | PlayerHealthのMax Hp、またはInvincible Secondsを長くする |
| 敵1体ずつを痛くする | EnemyのプレハブのTouch Damage |
| 囲まれたときだけ痛くする | 敵のKeep Distanceを小さくして、たくさん触れられるようにする |
無敵時間は0.5秒がちょうど良かったです。0.2秒だと囲まれた瞬間に溶けてしまい、1秒にすると突っ込んでも平気になってしまいました。「囲まれたら3〜4秒でやられる」ぐらいを目安に決めています。
画面を揺らしたい人へ
被弾のときに画面をガクッと揺らすと、さらに痛そうになります。このシリーズではCinemachineのカメラを使っているので、カメラが揺れる処理を実装するの方法(Impulse SourceとImpulse Listener)が使えます。PlayerHealthのDamagedに、揺らす処理をつなぐだけです。
あとがき
今回でようやく負けるゲームになりました。HPを入れただけなのに、同じ操作がまるで違う緊張感になります。「囲まれたらやられる」と分かった瞬間から、移動も強化選びも真剣になるのが面白いところです!次回は時間で敵の種類と数を変えて、10分間を生き延びる形に仕上げたいと思います。
コメント
コメント一覧 (1件)
[…] 前回:#9 HPとゲームオーバーを作ろう […]