ヴァンサバ風ゲームの作り方#6|金平糖を集めてレベルアップしよう

倒したコゲクッキーが金平糖を落とし、拾うと画面上の経験値バーがたまっていく様子

前回までで、武器が勝手に敵を倒してくれるようになりました。でも今のままだと、倒しても何も起きません。ヴァンサバ風のゲームが面白いのは、倒す→強くなる→もっと倒せる、という輪が回り出すからです。今回はヴァンサバ風サバイバーゲームのシリーズの6回目として、倒した敵が金平糖の粒を落として、集めるとレベルが上がる仕組みを作ってみたいと思います!

目次

今回作るもの

  • 倒れたコゲクッキーが、その場に金平糖の粒を落とす
  • 近づくと金平糖が吸い寄せられてくる
  • 集めると経験値がたまって、レベルが上がる
  • 画面の上に経験値バーとレベルを出す

作る・直すスクリプトは6つです。

スクリプト役割付ける場所
ExpGem(新しく作る)落ちた金平糖。近づくと吸い寄せられ、触れると経験値になる金平糖のプレハブ
GemSpawner(新しく作る)金平糖を出して、拾われたら使い回すGemSpawner
PlayerLevel(新しく作る)経験値をためて、たまったらレベルを上げるPlayer
StatusView(新しく作る)レベルと経験値バーを画面に出すCanvas
Enemy(直す)倒れたときに金平糖を落とす敵のプレハブ
EnemySpawner(直す)敵にGemSpawnerを教えるEnemySpawner

前回の続きから

  • Unityのバージョンは6.3 LTS
    • 記事作成時点では6000.3.22f1で確認しています
  • #5の状態:計量スプーンとホイッパーの2つの武器が、データ(ScriptableObject)を見て動いている

UIはこのシリーズで初めて使います。基本は9-1 CanvasとUIの基本、9-3 TextMeshPro、9-4 スクリプトからUIを更新しようで紹介しています。

経験値をためよう(PlayerLevel)

まずは経験値を受け取る側から作ります。このあと作る金平糖(ExpGem)がPlayerLevelを使うので、先にPlayerLevelがないとコンパイルエラーになり、Add Componentも出来ません。

  • SurvivorsLike/Scriptsに「PlayerLevel」を作る
using System;
using UnityEngine;

// 経験値をためて、たまったらレベルを上げる
public class PlayerLevel : MonoBehaviour
{
    public int Level { get; private set; } = 1;
    public int Exp { get; private set; }

    // 次のレベルまでに必要な経験値。レベルが上がるほど増えていく
    public int NeedExp => 5 + Level * 3;

    // レベルが上がったことを、ほかのスクリプトに知らせる
    public event Action LevelUp;

    public void AddExp(int amount)
    {
        Exp += amount;

        // 1回でまとめて2レベル上がることもあるので、足りている間は繰り返す
        while (Exp >= NeedExp)
        {
            Exp -= NeedExp;
            Level++;
            LevelUp?.Invoke();
        }
    }
}
PlayerLevelのポイント
  • whileで繰り返す理由
    • 金平糖1個ずつならifで足りますが、後で「まとめてドカンと経験値が入る」ものを足したときに、レベルが1しか上がらないバグになります
    • 先にwhileにしておくと、そのときに直さずに済みます
  • eventでレベルアップを知らせる
    • PlayerLevelは、レベルが上がったことを誰が受け取るかを知りません
    • 今回は画面の表示が受け取りますが、#7ではここに「強化を選ぶ画面」がつながります。増やすときにPlayerLevelを直さなくて済みます
  • NeedExpの式
    • レベル1のときは5 + 1 * 3で8、レベル2なら11・・・と、だんだん必要な数が増えます
    • 序盤にポンポン上がって、後半は落ち着く形です。数字はあとで好きに変えてください
  • HierarchyでPlayerを選び、Add Component>PlayerLevel

金平糖の粒を作ろう(ExpGem)

次に、落ちる金平糖そのものを作ります。

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

// 倒れた敵が落とす金平糖の粒。近づくと吸い寄せられて、触れると経験値になる
public class ExpGem : MonoBehaviour
{
    [SerializeField] private int exp = 1;               // 拾ったときにもらえる経験値
    [SerializeField] private float magnetRadius = 1.5f; // ここまで近づくと吸い寄せられる
    [SerializeField] private float magnetSpeed = 2f;    // 吸い寄せられ始める速さ
    [SerializeField] private float acceleration = 8f;   // 1秒あたり、どれだけ速くなるか
    [SerializeField] private float pickupRadius = 0.3f; // これより近づいたら拾えたことにする

    private PlayerLevel playerLevel;
    private IObjectPool<ExpGem> pool;
    private float speed;
    private bool taken;

    // 落とすときに、GemSpawnerから呼んでもらう
    public void Drop(Vector2 position, PlayerLevel newPlayerLevel, IObjectPool<ExpGem> newPool)
    {
        transform.position = position;
        transform.localScale = Vector3.one; // 使い回すので、大きさも戻しておく
        playerLevel = newPlayerLevel;
        pool = newPool;
        speed = magnetSpeed;
        taken = false;
    }

    private void Update()
    {
        if (taken)
        {
            return;
        }

        Vector2 toPlayer = (Vector2)playerLevel.transform.position - (Vector2)transform.position;
        float distance = toPlayer.magnitude;
        if (distance > magnetRadius) // まだ遠いので、その場で待つ
        {
            return;
        }

        if (distance <= pickupRadius)
        {
            taken = true;
            playerLevel.AddExp(exp);
            pool.Release(this);
            return;
        }

        // 近づくほど速くなって、小さくなる。これで「吸い込まれた」ように見える
        speed += acceleration * Time.deltaTime;
        transform.position += (Vector3)(toPlayer.normalized * speed * Time.deltaTime);

        float near = Mathf.InverseLerp(pickupRadius, magnetRadius, distance);
        transform.localScale = Vector3.one * Mathf.Lerp(0.3f, 1f, near);
    }
}
ExpGemのポイント
  • 当たり判定を使っていない
    • 武器はColliderで当てましたが、金平糖は吸い寄せるために距離をどのみち測っているので、そのまま拾う判定にも使いました
    • 画面に何百個も落ちるものなので、Colliderを付けないほうが軽くもなります
  • takenで二重取りを防ぐ
    • pool.Releaseを呼んでも、そのフレームのUpdateがもう一度走ることがあります。拾ったら印を付けて、同じ金平糖で2回経験値が入らないようにしました
  • 吸い込まれる動き
    • 近づくほど速くなり、同時に小さくなります。この2つだけで、吸い込まれた感じが出ます
    • Mathf.InverseLerpは「距離がmagnetRadiusのとき1、pickupRadiusのとき0」に変える計算です。それをMathf.Lerpに渡して、大きさを1→0.3に変えています
  • speedと大きさは落ちるたびに戻す
    • 使い回すので、Dropで元に戻しています。戻し忘れると、2回目からいきなり速くて小さい金平糖になります

金平糖のプレハブを作ろう

  • ProjectウインドのSurvivorsLike/Spritesから、gem_zarameをHierarchyにドラッグ&ドロップ
  • 名前を「ExpGem」にリネーム
  • Sprite RendererのOrder in Layerを-1にする(敵やスライムちゃんより奥。踏んでも隠れないように)
  • Add Component>ExpGem(さっき書いたスクリプト)
  • SurvivorsLike/Prefabsにドラッグ&ドロップしてプレハブにし、Hierarchyのものは削除する
ExpGemのプレハブ。Sprite Rendererのgem_zarameと、Exp Gemスクリプトの設定

金平糖を出す係を作ろう(GemSpawner)

金平糖は敵を倒すたびに出てきて、拾われると消えます。#3の敵とまったく同じ事情なので、ObjectPoolで使い回します。

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

// 金平糖の粒を出して、拾われたら使い回す
public class GemSpawner : MonoBehaviour
{
    [SerializeField] private ExpGem gemPrefab;
    [SerializeField] private PlayerLevel playerLevel;

    private ObjectPool<ExpGem> pool;

    private void Awake()
    {
        pool = new ObjectPool<ExpGem>(
            createFunc: () => Instantiate(gemPrefab, transform),
            actionOnGet: gem => gem.gameObject.SetActive(true),
            actionOnRelease: gem => gem.gameObject.SetActive(false),
            actionOnDestroy: gem => { if (gem != null) Destroy(gem.gameObject); },
            defaultCapacity: 50,
            maxSize: 500);
    }

    // 敵が倒れたときに、Enemyから呼んでもらう
    public void Drop(Vector2 position)
    {
        ExpGem gem = pool.Get();
        gem.Drop(position, playerLevel, pool);
    }
}
落ちっぱなしの金平糖は、拾うまで消えない

ヴァンサバでも、取り残した経験値の粒は地面に残り続けます。10分も戦うと数百個になるので、1個ずつInstantiateしていたら重くなります。ObjectPoolにしておけば、拾われた粒がそのまま次の粒として出てくるので、作られる数は「同時に落ちている一番多いとき」の数で止まります。ObjectPoolの仕組みは#3 敵を出して追いかけさせようで作りました。

  • Hierarchyで右クリック>Create Empty、名前を「GemSpawner」にする
    • Positionは0, 0, 0にしておく(落とした金平糖は、この下にまとめられます)
  • Add Component>GemSpawner(さっき書いたスクリプト)
    • 欄の設定は、Enemyを直した後でまとめてやります

倒れたら金平糖を落とそう(Enemyを直す)

敵が倒れた瞬間に、GemSpawnerへ「ここに落として」と頼みます。

    private GemSpawner gemSpawner;

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

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

        hp -= damage;
        if (hp <= 0)
        {
            gemSpawner.Drop(transform.position); // 倒れた場所に金平糖を落とす
            pool.Release(this);
        }
    }

EnemySpawnerからは、敵を出すときにGemSpawnerも一緒に渡します。

    [SerializeField] private GemSpawner gemSpawner;
        Enemy enemy = pool.Get();
        enemy.Spawn(position, target, pool, gemSpawner);
  • 敵に直接プレハブを持たせない
    • Enemyが自分でInstantiateしても金平糖は出せますが、それだとプールが使えません
    • 「出す係」を1つにまとめて、敵は「落として」と頼むだけにしています
  • HierarchyでEnemySpawnerを選び、Gem Spawnerの欄にHierarchyのGemSpawnerをドラッグ
スライム

敵が自分で金平糖を作るんじゃないんですね

フェアリー

作る係をひとつにまとめておくと、あとで「2個落とす」「たまにレアな金平糖」みたいな話も1か所で済むんだよ!

最後に、GemSpawnerの欄を埋めます。

  • HierarchyでGemSpawnerを選び、Gem PrefabにExpGemのプレハブ、Player LevelにHierarchyのPlayerをドラッグ
GemSpawnerのInspector。Gem PrefabにExpGem、Player LevelにPlayerが入っている

画面にレベルと経験値バーを出そう

ここからUIです。ゲームの世界の中ではなく、画面に貼り付ける表示を作ります。

  • Hierarchyで右クリック>UI>Canvas
    • Render Mode:Screen Space – Overlay
    • Canvas ScalerのUI Scale Mode:Scale With Screen Size
    • Reference Resolution:X 1920、Y 1080
CanvasのInspector。Render ModeはScreen Space - Overlay、UI Scale ModeはScale With Screen Size
  • Screen Space – Overlay
    • カメラについていかず、いつも画面の同じ場所に出る表示です
    • スライムちゃんが動くとカメラも動きますが、経験値バーは動いてほしくないので、これを選びます
  • Scale With Screen Size
    • 画面の大きさが変わっても、1920×1080のときと同じ見た目になるように伸び縮みします

経験値バーを作る

バーは「土台」と「伸びる部分」の2枚重ねで作ります。

  • Canvasの上で右クリック>UI>Image、名前を「ExpBar」にする(土台)
    • Rect TransformのAnchor Presetsでtop・centerを選ぶ
    • Pos X:0、Pos Y:-40、Width:900、Height:24
    • Source Image:ui_white、Color:こい灰色
  • ExpBarの上で右クリック>UI>Image、名前を「ExpBarFill」にする(伸びる部分)
    • Anchor Presetsでstretch・stretch(右下のもの)をAltキーを押しながら選び、土台いっぱいに広げる
    • Source Image:ui_white、Color:水色
    • Image Type:Filled
    • Fill Method:Horizontal、Fill Origin:Left、Fill Amount:0
ExpBarFillのInspector。Image TypeがFilled、Fill MethodがHorizontal、Fill Amountが0
  • バーの画像は「まっさらな白い四角」を使う
    • Unity付属のUISpriteは角が丸く、横に伸ばすと端がゆがんで見えます
    • 配布素材に入っているui_white(ただの白い四角)を使うと、まっすぐなバーになります。色はImageのColorで付けます
  • FilledにするとfillAmountで伸び縮みする
    • fillAmountは0〜1の数字です。0.5なら半分だけ表示されます
    • 経験値バーは「今の経験値 ÷ 次に必要な経験値」を入れるだけで作れます

レベルの数字と「LEVEL UP!」を作る

  • Canvasの上で右クリック>UI>Text – TextMeshPro、名前を「LevelText」にする
    • Anchor Presetsでtop・left、Pos X:120、Pos Y:-40
    • Text:Lv.1、Font Size:48、Color:こい茶色、Alignment:左・中央
      • 床が明るい色なので、白い文字だと読めません
  • 同じようにText – TextMeshProをもう1つ作り、名前を「LevelUpLabel」にする
    • Anchor Presetsでmiddle・center、Pos Y:200
    • Text:LEVEL UP!、Font Size:72、Color:オレンジ、Alignment:中央・中央
    • Inspectorの一番上のチェックを外して、最初は非表示にしておく

文字が出ないときは、TMP Essentialsが入っていない

Text – TextMeshProを初めて作ると、TMP Importerという窓が開きます。ここでImport TMP Essentialsを押さないと、文字を入れても何も表示されません。閉じてしまった場合は、Window>TextMeshPro>Import TMP Essential Resourcesから入れ直せます。くわしくは9-3 TextMeshProをどうぞ。

EventSystemに付いているモジュールを確かめる

Canvasを作ると、一緒にEventSystem(ボタンを押す仕組み。#7で使います)も作られることがあります。このプロジェクトはInput Systemで動いているので、EventSystemに付いているのはInput System UI Input Moduleでなければいけません。

古いStandalone Input Moduleが付いていると、再生した瞬間からYou are trying to read Input using the UnityEngine.Input classというエラーが毎フレーム出続けます。自分が試したときは、たった数十秒で2万件を超えました。付いていたら消して、Add Component>Input System UI Input Moduleに付け替えてください。

Hierarchy。Canvasの下にExpBar(子にExpBarFill)、LevelText、LevelUpLabelが並んでいる

表示を更新するスクリプト

  • SurvivorsLike/Scriptsに「StatusView」を作る
using TMPro;
using UnityEngine;
using UnityEngine.UI;

// レベルと経験値バーを画面に出す
public class StatusView : MonoBehaviour
{
    [SerializeField] private PlayerLevel playerLevel;
    [SerializeField] private Image expBarFill;
    [SerializeField] private TextMeshProUGUI levelText;
    [SerializeField] private GameObject levelUpLabel;
    [SerializeField] private float labelSeconds = 1f; // 「LEVEL UP!」を出しておく秒数

    private float labelTimer;

    private void OnEnable()
    {
        playerLevel.LevelUp += ShowLabel;
    }

    private void OnDisable()
    {
        playerLevel.LevelUp -= ShowLabel; // 付けたら必ず外す
    }

    private void Update()
    {
        expBarFill.fillAmount = (float)playerLevel.Exp / playerLevel.NeedExp;
        levelText.text = $"Lv.{playerLevel.Level}";

        if (!levelUpLabel.activeSelf)
        {
            return;
        }

        labelTimer -= Time.deltaTime;
        if (labelTimer <= 0f)
        {
            levelUpLabel.SetActive(false);
        }
    }

    private void ShowLabel()
    {
        levelUpLabel.SetActive(true);
        labelTimer = labelSeconds;
    }
}
  • OnEnableで付けて、OnDisableで外す
    • eventは、付けたままにしておくと同じ処理が2回呼ばれたり、消えたはずのものが呼ばれたりします
    • セットで書く、と覚えてしまうのが楽です
  • (float)を忘れない
    • ExpもNeedExpも整数なので、そのまま割ると答えも整数になり、バーが0か1しか動きません
    • 片方を(float)にして、小数で計算させます
  • HierarchyでCanvasを選び、Add Component>StatusView
  • 4つの欄に、HierarchyからPlayer・ExpBarFill・LevelText・LevelUpLabelをドラッグ
CanvasのInspector。Status Viewの4つの欄にPlayerとUIの部品が入っている

動かして確認!

落ちている金平糖がスライムちゃんに吸い寄せられ、小さくなりながら消えていく様子

#6の動作確認

  • 敵を倒すと、倒れた場所に金平糖の粒が落ちる
  • 落ちた金平糖は、近づくまでその場で待っている
  • 近づくと吸い寄せられ、だんだん速くなってスライムちゃんに消える
  • 拾うたびに、画面上のバーが少しずつ伸びる
  • バーが満タンになるとレベルの数字が増え、バーは0に戻る
  • 「LEVEL UP!」が1秒だけ出て消える
  • 拾った金平糖は、HierarchyのGemSpawnerの下に灰色(非表示)のまま残る。次に落ちるときはこれが使い回される
Gameビュー。中央にオレンジの「LEVEL UP!」、左上はLv.5、経験値バーは空に戻っている
スライム

バーがたまっていくのを見ているだけで、なんだか楽しいです!

これで、倒す→金平糖が落ちる→拾う→レベルが上がるという輪がつながりました。この輪がゲームの背骨で、次回はここに「強くなる」をつなげます。

バランスを考えよう

数字はすべてExpGemとPlayerLevelにまとまっているので、遊びながら変えてみてください。

変えたいこと変える数字
レベルアップを早くするPlayerLevelのNeedExpの式(5 + Level * 3の数を小さく)
金平糖を集めやすくするExpGemプレハブのMagnet Radius(1.5→3など)
吸い寄せをキビキビさせるExpGemプレハブのAcceleration
金平糖1個でもらえる経験値を変えるExpGemプレハブのExp(すべての金平糖に効きます)

Magnet Radiusを大きくすると、ゲームの手触りがはっきり変わります。広いと勝手に集まって気持ちいい代わりに、「拾いに行く」という判断がなくなります。実際に3や5を入れて見比べてみると面白いと思います。

あとがき

今回は倒した敵から金平糖を落として、集めるとレベルが上がるところまで作りました。画面にバーが出ただけで、一気にゲームらしくなったのではないでしょうか!次回はレベルが上がったときにゲームを止めて、3つの強化から1つ選ぶ画面を作りたいと思います。

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

コメント

コメント一覧 (1件)

コメントする

目次