ヴァンサバ風ゲームの作り方#8|手応えのある攻撃の演出を作ろう

群れの中でダメージの数字が次々に飛び、当たった敵がのけぞって倒れていく様子

前回までで、遊びの仕組みはひととおり動くようになりました。でも今の状態で遊んでみると、なんだか手応えがありません。敵は音もなく消えるし、当たったのかどうかも分かりません。ゲームの気持ちよさは、実はこういう当たった感じで決まります。今回はヴァンサバ風サバイバーゲームのシリーズの8回目として、攻撃が当たったこと・敵が倒れたこと・レベルが上がったことを、目で分かるようにしてみたいと思います!

目次

なぜ演出が要るのか

ここまでは、敵のHPが10で武器の攻撃力も10でした。必ず一撃で倒れるので、演出が無くても「当たった=消えた」で分かります。

ところが次回以降、硬い敵(カチカチあめ)が出てきます。そうなると・・・

  • 3発当てないと倒れない
  • でも1発目と2発目は画面に何も起きない
  • 遊ぶ人は「当たってないのかな?」「この武器、効いてないのかな?」と思う

これは実際に作っていて何度もぶつかりました。自分でも当たっているか分からないものは、遊ぶ人にはもっと分かりません。演出は飾りではなくて、ゲームが今どうなっているかを伝える情報です。

今回作るもの

  • 攻撃が当たったら、ダメージの数字がポンと出る
  • 当たった敵がのけぞって、一瞬ふくらむ
  • 倒れた敵はしぼんで消える
  • レベルアップの文字がポンと出る
  • 敵のHPと武器の攻撃力を調整して、一撃では倒れないようにする

新しく作るスクリプトが3つ、直すスクリプトが3つです。

スクリプト役割付ける場所
DamageText(新しく作る)数字が上に浮かびながら消えるダメージ数字のプレハブ
DamageTextSpawner(新しく作る)数字を出して、使い回すDamageTexts
EnemyEffect(新しく作る)被弾でふくらむ・のけぞる・しぼんで消える敵のプレハブ
Enemy(直す)ダメージを受けたときに、演出を呼ぶ敵のプレハブ
EnemySpawner(直す)敵を出すときに、数字の係を渡すEnemySpawner
StatusView(直す)レベルアップの文字をポンと出すCanvas

前回の続きから

  • Unityのバージョンは6.3 LTS
    • 記事作成時点では6000.3.22f1で確認しています
  • #7の状態:レベルアップで3択の強化が出て、選ぶと強くなる

時間を少しずつ進める書き方(コルーチン)は6-11 時間を扱う処理で紹介しています。

ダメージの数字を出そう

まずは一番はっきり分かる演出、ダメージの数字です。

  • SurvivorsLike/Scriptsに「DamageText」を作る
using System.Collections;
using TMPro;
using UnityEngine;
using UnityEngine.Pool;

// 出たあと、上に浮かびながら消えるダメージの数字
[RequireComponent(typeof(TextMeshPro))]
public class DamageText : MonoBehaviour
{
    [SerializeField] private float riseSpeed = 1.2f; // 1秒に上がる高さ
    [SerializeField] private float life = 0.6f;      // 消えるまでの秒数
    [SerializeField] private float popScale = 1.4f;  // 出た瞬間の大きさ

    private TextMeshPro label;
    private IObjectPool<DamageText> pool;

    private void Awake()
    {
        label = GetComponent<TextMeshPro>();
    }

    // 出すときに、DamageTextSpawnerから呼んでもらう
    public void Show(Vector2 position, int amount, IObjectPool<DamageText> newPool)
    {
        transform.position = position;
        label.text = amount.ToString();
        pool = newPool;

        StopAllCoroutines();
        StartCoroutine(Play());
    }

    private IEnumerator Play()
    {
        float timer = 0f;
        while (timer < life)
        {
            timer += Time.deltaTime;
            float t = timer / life;

            // 出た瞬間だけ大きく、すぐ元の大きさに戻る
            transform.localScale = Vector3.one * Mathf.Lerp(popScale, 1f, Mathf.Min(t * 4f, 1f));
            transform.position += Vector3.up * riseSpeed * Time.deltaTime;
            label.alpha = 1f - t; // だんだん透ける

            yield return null;
        }
        pool.Release(this);
    }
}
DamageTextのポイント
  • コルーチン(IEnumeratorとyield return null)
    • yield return nullは「ここでいったん終わって、次のフレームから続きをやる」という意味です
    • whileの中で少しずつ動かすと、アニメーションを作らなくても動きが付けられます
  • Mathf.Min(t * 4f, 1f)
    • 大きさだけ4倍の速さで元に戻します。出た瞬間だけポンと大きい、という形です
  • Time.deltaTimeで動かす
    • #7でTime.timeScale = 0にしたので、強化を選んでいる間はこの演出もきちんと止まります

数字のプレハブを作ろう

  • Hierarchyで右クリック>3D Object>Text – TextMeshPro、名前を「DamageText」にする
    • UIのText - TextMeshProではなく、ゲームの世界に置くほうの「3D Object」です。敵と同じ場所に出したいので、こちらを使います
    • Font Size:4、Alignment:中央・中央、Color:こい赤
    • Rect TransformのWidth・Heightは3・1ぐらい
    • Add Component>DamageText
    • InspectorのExtra Settings>Order in Layerを10にする(敵より手前)
  • SurvivorsLike/Prefabsにドラッグ&ドロップしてプレハブにし、Hierarchyのものは削除する
DamageTextのプレハブ。TextMeshProとDamage Textスクリプトの設定

出す係を作ろう

数字は敵を殴るたびに出るので、#3の敵や#6の金平糖と同じくプールで使い回します。

  • SurvivorsLike/Scriptsに「DamageTextSpawner」を作る
using UnityEngine;
using UnityEngine.Pool;

// ダメージの数字を出して、使い回す
public class DamageTextSpawner : MonoBehaviour
{
    [SerializeField] private DamageText textPrefab;

    private ObjectPool<DamageText> pool;

    private void Awake()
    {
        pool = new ObjectPool<DamageText>(
            createFunc: () => Instantiate(textPrefab, transform),
            actionOnGet: text => text.gameObject.SetActive(true),
            actionOnRelease: text => text.gameObject.SetActive(false),
            actionOnDestroy: text => { if (text != null) Destroy(text.gameObject); },
            defaultCapacity: 30,
            maxSize: 200);
    }

    // 敵がダメージを受けたときに、Enemyから呼んでもらう
    public void Show(Vector2 position, int amount)
    {
        pool.Get().Show(position, amount, pool);
    }
}
  • Hierarchyで右クリック>Create Empty、名前を「DamageTexts」にする
    • Positionは0, 0, 0
    • Add Component>DamageTextSpawner、Text PrefabにDamageTextのプレハブを入れる

敵に当たった感じを付けよう

  • SurvivorsLike/Scriptsに「EnemyEffect」を作る
using System;
using System.Collections;
using UnityEngine;

// 敵の見た目の演出。被弾でふくらみ、倒れるときはしぼんで消える
public class EnemyEffect : MonoBehaviour
{
    [SerializeField] private SpriteRenderer spriteRenderer;
    [SerializeField] private float hitScale = 1.25f;    // 被弾で一瞬大きくなる倍率
    [SerializeField] private float hitSeconds = 0.12f;  // 戻るまでの秒数
    [SerializeField] private float deathSeconds = 0.3f;  // しぼんで消えるまでの秒数

    // 出てくるたびに、見た目を元に戻す(使い回すので)
    public void ResetLook()
    {
        StopAllCoroutines();
        transform.localScale = Vector3.one;
        spriteRenderer.color = Color.white;
    }

    public void Hit()
    {
        StopAllCoroutines();
        StartCoroutine(HitRoutine());
    }

    private IEnumerator HitRoutine()
    {
        float timer = 0f;
        while (timer < hitSeconds)
        {
            timer += Time.deltaTime;
            transform.localScale = Vector3.one * Mathf.Lerp(hitScale, 1f, timer / hitSeconds);
            yield return null;
        }
        transform.localScale = Vector3.one;
    }

    // 倒れる演出。終わったら onFinished を呼ぶ(そこでプールに戻す)
    public void PlayDeath(Action onFinished)
    {
        StopAllCoroutines();
        StartCoroutine(DeathRoutine(onFinished));
    }

    private IEnumerator DeathRoutine(Action onFinished)
    {
        float timer = 0f;
        while (timer < deathSeconds)
        {
            timer += Time.deltaTime;
            float t = timer / deathSeconds;
            transform.localScale = Vector3.one * Mathf.Lerp(1f, 0.2f, t);
            spriteRenderer.color = new Color(1f, 1f, 1f, 1f - t);
            yield return null;
        }
        onFinished();
    }
}
  • Action onFinishedで「終わったら教える」
    • 倒れる演出が終わるまでは、プールに戻してはいけません。戻した瞬間に消えてしまい、演出が見えなくなります
    • 「終わったらこれを呼んでね」と渡しておくのが、いちばん素直なやり方です
  • ResetLookを忘れない
    • プールで使い回すので、小さいまま・透けたままの敵が出てこないように戻します
白く光らせないのはなぜ?

被弾の演出というと「一瞬白く光る」が定番ですが、あれはシェーダー(材質の設定)を自分で用意する必要があります。SpriteRendererのColorは絵の色に掛け算されるので、白を入れても元の色に戻るだけで、明るくは出来ません。

このシリーズでは、大きさの変化と数字で当たったことを伝えます。シェーダーを使わずに済んで、しかもどれだけ効いたかまで分かるので、こちらのほうが情報量は多いです。

敵のプレハブに付ける

  • ProjectウインドでSurvivorsLike/PrefabsのEnemyをダブルクリックで開く
  • Add Component>EnemyEffect、Sprite Rendererの欄に自分自身のSprite Rendererを入れる
  • Max Hpを20にする(一撃では倒れないように。理由は後で書きます)
Enemyのプレハブ。Max Hp 20 と、Enemy Effectの設定

Enemyから演出を呼ぼう

Enemyを直します。のけぞりもここで付けます。

using UnityEngine;
using UnityEngine.Pool;

// こげたお菓子の敵。スライムちゃんを追いかけ、遠くに離れたらプールに戻る
[RequireComponent(typeof(Rigidbody2D))]
public class Enemy : MonoBehaviour
{
    [SerializeField] private float moveSpeed = 1.5f;      // 1秒に進むマス数
    [SerializeField] private float despawnDistance = 22f; // これより離れたら引っ込む
    [SerializeField] private float keepDistance = 0.5f;   // これ以上は近づかない
    [SerializeField] private int maxHp = 20;              // 体力
    [SerializeField] private float knockback = 2.5f;      // のけぞる強さ
    [SerializeField] private EnemyEffect effect;

    private Rigidbody2D body;
    private Transform target;
    private IObjectPool<Enemy> pool;
    private GemSpawner gemSpawner;
    private DamageTextSpawner damageTexts;
    private int hp;
    private bool dying;

    private void Awake()
    {
        body = GetComponent<Rigidbody2D>();
    }

    // 出てくるときに、EnemySpawnerから呼んでもらう
    public void Spawn(Vector2 position, Transform newTarget, IObjectPool<Enemy> newPool,
        GemSpawner newGemSpawner, DamageTextSpawner newDamageTexts)
    {
        transform.position = position;
        target = newTarget;
        pool = newPool;
        gemSpawner = newGemSpawner;
        damageTexts = newDamageTexts;
        hp = maxHp;   // 使い回すので、体力を戻しておく
        dying = false;
        effect.ResetLook();
    }

    // 攻撃が当たったときに、武器のほうから呼んでもらう
    public void TakeDamage(int damage)
    {
        if (dying) // もう倒れている(同じ瞬間に2発当たったとき用)
        {
            return;
        }

        hp -= damage;
        damageTexts.Show(transform.position + Vector3.up * 0.5f, damage);

        if (hp > 0)
        {
            effect.Hit();
            Knockback();
            return;
        }

        dying = true;
        body.linearVelocity = Vector2.zero;
        gemSpawner.Drop(transform.position);
        effect.PlayDeath(() => pool.Release(this)); // 演出が終わってからプールに戻す
    }

    // 攻撃はプレイヤーのほうから飛んでくるので、プレイヤーと反対に押す
    private void Knockback()
    {
        if (target == null)
        {
            return;
        }

        Vector2 away = body.position - (Vector2)target.position;
        if (away.sqrMagnitude < 0.0001f)
        {
            return;
        }

        body.linearVelocity = away.normalized * knockback;
    }

    private void FixedUpdate()
    {
        if (target == null || dying)
        {
            return;
        }

        Vector2 toTarget = (Vector2)target.position - body.position;
        float distance = toTarget.magnitude;

        // 真上に乗ってしまわないよう、一定の距離まで近づいたら止まる
        body.linearVelocity = distance > keepDistance ? toTarget.normalized * moveSpeed : Vector2.zero;

        if (distance > despawnDistance)
        {
            pool.Release(this);
        }
    }
}
Enemyのポイント
  • dyingを足した理由
    • 倒れる演出をしている0.3秒のあいだ、敵はまだ画面にいます。そのままだと追いかけ続けるし、もう一度ダメージも受けてしまいます
    • 「もう倒れた」という印を付けて、動きもダメージも止めます
  • のけぞりはlinearVelocityを一瞬だけ書き換える
    • 次のFixedUpdateでまた追いかける速度に戻るので、押された分だけ後ろにずれて、すぐ向き直る動きになります
  • Spawnの引数が5つになった
    • 増えてきたので、そろそろ「敵に渡すもの」を1つにまとめたくなる頃です。まとめ方は#11の「作り直すなら」で触れます
  • Enemyのプレハブを開き、EnemyのEffectの欄に、同じプレハブに付けたEnemyEffectを入れる

Effectの欄を入れ忘れると、敵が出てきた瞬間にNullReferenceExceptionになります。Spawnの中でeffect.ResetLook()を呼んでいるためです。エラーが出たら、まずプレハブのEnemyのInspectorを見てください。

EnemySpawnerも、数字の係を渡すように直します。

    [SerializeField] private DamageTextSpawner damageTexts;
        Enemy enemy = pool.Get();
        enemy.Spawn(position, target, pool, gemSpawner, damageTexts);
  • HierarchyでEnemySpawnerを選び、Damage TextsにHierarchyのDamageTextsをドラッグ

レベルアップの文字をポンと出そう

#6で作った「LEVEL UP!」は、出て消えるだけでした。出た瞬間に大きくして、目に留まるようにします。

その前に、#7で困ったことが1つ起きています。レベルアップすると強化のパネルも同時に出るので、「LEVEL UP!」がパネルの暗幕の裏に隠れて見えません。

スライム

せっかくの「LEVEL UP!」が、ずっと暗幕の裏に隠れていたなんて・・・

  • HierarchyでLevelUpLabelを、UpgradePanelより下にドラッグして並べ替える
    • UIはHierarchyで下にあるものほど手前に描かれます。カードより手前に出したいので、一番下にします
  • Pos Yを330にする(カードの上に出るように。#6では200でした)

StatusViewのShowLabelを書き換えます。

    [SerializeField] private float labelPopScale = 1.6f; // 出た瞬間の大きさ

    private void ShowLabel()
    {
        levelUpLabel.SetActive(true);
        labelTimer = labelSeconds;
        StopAllCoroutines();
        StartCoroutine(PopLabel());
    }

    private IEnumerator PopLabel()
    {
        float timer = 0f;
        float popSeconds = 0.2f;
        while (timer < popSeconds)
        {
            timer += Time.unscaledDeltaTime;
            float scale = Mathf.Lerp(labelPopScale, 1f, timer / popSeconds);
            levelUpLabel.transform.localScale = Vector3.one * scale;
            yield return null;
        }
        levelUpLabel.transform.localScale = Vector3.one;
    }
  • using System.Collections;を先頭に足してください
止まっている間も動かしたい演出はunscaledDeltaTime

#7でレベルアップ中はTime.timeScale = 0にしました。Time.deltaTimeはこの影響を受けるので0になり、ポンと大きくなったまま固まってしまいます。そこでここだけTime.unscaledDeltaTime(timeScaleの影響を受けない時間)を使います。

反対に、「何秒出しておくか」を数えるlabelTimerはTime.deltaTimeのままにします。こうすると、強化を選んでいる間は減らないので、カードの上に「LEVEL UP!」が出たままになり、選び終わってから1秒で消えてくれます。

同じ演出の中でも、止まってほしい時間と止まってほしくない時間があります。敵のダメージ演出はゲームの一部なので、全部Time.deltaTimeのままでOKです。

強さを調整しよう

演出が付いたので、一撃では倒れない強さに直します。ここまでは「当たったかどうか」を見るために、わざと一撃で倒れるようにしていました(#5でお約束した調整です)。

直すもの前後ねらい
Enemyのプレハブ Max Hp1020計量スプーンなら2発。数字が出てから倒れる
SpoonData Damage1010そのまま。基本の1発
WhipperData Damage106回っている間に何度も当たるので控えめに
StrainerData Damage10123秒に1回しか出ないので強めに

同じ「10ダメージ」でも、当たる回数が違えば強さはまったく違います。ホイッパーは0.5秒ごとに当たるので、実際は1秒に12ダメージ。ストレーナーは3秒に1回だけなので12でも1秒あたり4です。「1秒あたりいくつ削れるか」で考えると、バランスを取りやすくなります。

スライム

数字が出るようになっただけで、ぜんぜん違うゲームみたいです!

フェアリー

「効いてる」が見えるとうれしいんだよね。演出は飾りじゃなくて、遊ぶ人への説明なんだ!

動かして確認!

スプーンが当たったコゲクッキーの頭の上に、赤い「10」の数字が出ている
ダメージの数字が浮かびながら消え、敵がしぼんで消えていく様子

#8の動作確認

  • 攻撃が当たると、敵の頭の上にダメージの数字がポンと出て、浮かびながら消える
  • 当たった敵が一瞬ふくらんで、後ろにのけぞる
  • 何発か当てるとしぼんで消え、その場所に金平糖が落ちる
  • 倒れている途中の敵には、もうダメージが入らない(数字が重ならない)
  • レベルアップの「LEVEL UP!」が大きく出てから元の大きさになる
  • 強化を選んでいる間(Time.timeScale = 0)は、数字も演出も止まっている
  • HierarchyのDamageTextsの下で、数字が使い回されている(非表示のまま残る)
3枚のカードの上に、オレンジの「LEVEL UP!」が大きく出ている

演出を足すときの考え方

今回入れたものは、どれも数行で書けるものばかりです。凝ったものを作らなくても、次の3つを押さえるだけで手応えは大きく変わります。

伝えたいこと使った手
当たった大きさが変わる・のけぞる
どれだけ効いた数字が出る
終わった(倒れた)しぼんで消える

大事なのは「変化があること」です。音を足すともっと良くなるので、#11で効果音を付けるときに、この3か所に鳴らす予定です。

あとがき

今回は演出だけの回でした。派手なことは何もしていませんが、遊んでみると別のゲームのように気持ちよくなっているはずです。演出は最後に足すおまけだと思われがちですが、実際は「今どうなっているか」を伝えるための仕組みで、ゲームの分かりやすさそのものだと思います!次回はスライムちゃんにHPを持たせて、敵に触られるとダメージを受けるようにします。今度は自分がやられる側の演出です。

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

コメント

コメント一覧 (1件)

コメントする

目次