ヴァンサバ風ゲームの作り方#9|HPとゲームオーバーを作ろう

敵に囲まれて画面が赤く光りHPが減っていき、0でゲームオーバーになるまでの様子

前回で攻撃に手応えが出ました。でもこのゲーム、まだ一度も負けません。敵に囲まれても平気なので、逃げる理由も、強化を選ぶ意味もありませんでした。今回はヴァンサバ風サバイバーゲームのシリーズの9回目として、スライムちゃんにHPを持たせて、やられる側の演出とゲームオーバーを作ってみたいと思います!

目次

今回作るもの

  • スライムちゃんに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();
        }
    }
}
PlayerHealthのポイント
  • 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を入れる
PlayerのPlayer Health。Max Hp 100、Invincible Seconds 0.5

減らないときは、当たり判定が届いているかを見る

敵は#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が受け止めてしまい、強化のカードが押せなくなります。「カードが反応しない」ときは、まずここを見てください。

Hierarchy。Canvasの下にExpBar、HpBar、DamageFlash、UpgradePanel、GameOverPanelが並んでいる

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);
    }
StatusViewのポイント
  • Time.unscaledDeltaTimeで消す
    • Time.deltaTimeにすると、やられてtimeScaleが0になった瞬間に画面が赤いまま固まります
    • #8のレベルアップの演出と同じで、止まっている間も消えてほしいUIの演出はこちらを使います
  • StopCoroutine(nameof(...))にした理由
    • #8でStopAllCoroutines()を使いましたが、StatusViewにはレベルアップのポップも動いています
    • 全部止めると、被弾のたびにレベルアップの演出が消えてしまいます。名前を指定して、赤い光だけを止め直します
    • StopCoroutineに名前を渡して止められるのは、名前で始めたコルーチンだけです。名前で止めるときは、始めるときもStartCoroutine(nameof(...))と名前で書きます
スライム

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);
    }
}
GameOverViewのポイント
  • Time.timeScale = 1fを戻すのを忘れない
    • timeScaleはシーンを読み込み直しても元に戻りません。0のまま次のゲームが始まって、「何も動かない」と悩むのは、よくあるつまずきです
  • 同じシーンを読み込み直す
    • 敵も金平糖も強化も、全部まとめて最初の状態に戻ります。作り込んだリセット処理を書かずに済む、いちばん手軽なやり直し方です
    • タイトル画面からのちゃんとした流れは、#11で作ります
  • HierarchyでCanvasを選び、Add Component>GameOverView
    • Player Health:Player、Panel:GameOverPanel、Retry Button:RetryButton
画面が暗くなり、赤い「GAME OVER」と「もう一度」のボタンが出ている
スライム

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

フェアリー

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

動かして確認!

被弾の瞬間。画面全体が赤く光り、HPゲージが減っている

#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に、揺らす処理をつなぐだけです。

リンク先はCinemachine 2のころの記事です。このシリーズで入れたCinemachine 3では、スクリプトのusing Cinemachine;がusing Unity.Cinemachine;に変わり、Impulse ListenerはCinemachineCameraのAdd Extensionから付けます。

あとがき

今回でようやく負けるゲームになりました。HPを入れただけなのに、同じ操作がまるで違う緊張感になります。「囲まれたらやられる」と分かった瞬間から、移動も強化選びも真剣になるのが面白いところです!次回は時間で敵の種類と数を変えて、10分間を生き延びる形に仕上げたいと思います。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

コメント

コメント一覧 (1件)

コメントする

目次