
前回までで、武器が勝手に敵を倒してくれるようになりました。でも今のままだと、倒しても何も起きません。ヴァンサバ風のゲームが面白いのは、倒す→強くなる→もっと倒せる、という輪が回り出すからです。今回はヴァンサバ風サバイバーゲームのシリーズの6回目として、倒した敵が金平糖の粒を落として、集めるとレベルが上がる仕組みを作ってみたいと思います!
- シリーズの目次:ヴァンサバ風ゲームの作り方
- 前回:#5 武器のデータをScriptableObjectでまとめよう
今回作るもの
- 倒れたコゲクッキーが、その場に金平糖の粒を落とす
- 近づくと金平糖が吸い寄せられてくる
- 集めると経験値がたまって、レベルが上がる
- 画面の上に経験値バーとレベルを出す
作る・直すスクリプトは6つです。
| スクリプト | 役割 | 付ける場所 |
|---|---|---|
| ExpGem(新しく作る) | 落ちた金平糖。近づくと吸い寄せられ、触れると経験値になる | 金平糖のプレハブ |
| GemSpawner(新しく作る) | 金平糖を出して、拾われたら使い回す | GemSpawner |
| PlayerLevel(新しく作る) | 経験値をためて、たまったらレベルを上げる | Player |
| StatusView(新しく作る) | レベルと経験値バーを画面に出す | Canvas |
| Enemy(直す) | 倒れたときに金平糖を落とす | 敵のプレハブ |
| EnemySpawner(直す) | 敵にGemSpawnerを教える | EnemySpawner |
前回の続きから
- Unityのバージョンは6.3 LTS
- 記事作成時点では6000.3.22f1で確認しています
- #5の状態:計量スプーンとホイッパーの2つの武器が、データ(ScriptableObject)を見て動いている
経験値をためよう(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();
}
}
}
whileで繰り返す理由- 金平糖1個ずつなら
ifで足りますが、後で「まとめてドカンと経験値が入る」ものを足したときに、レベルが1しか上がらないバグになります - 先に
whileにしておくと、そのときに直さずに済みます
- 金平糖1個ずつなら
eventでレベルアップを知らせるPlayerLevelは、レベルが上がったことを誰が受け取るかを知りません- 今回は画面の表示が受け取りますが、#7ではここに「強化を選ぶ画面」がつながります。増やすときに
PlayerLevelを直さなくて済みます
NeedExpの式- レベル1のときは
5 + 1 * 3で8、レベル2なら11・・・と、だんだん必要な数が増えます - 序盤にポンポン上がって、後半は落ち着く形です。数字はあとで好きに変えてください
- レベル1のときは
- 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);
}
}
- 当たり判定を使っていない
- 武器は
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のものは削除する

金平糖を出す係を作ろう(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をドラッグ


画面にレベルと経験値バーを出そう
ここからUIです。ゲームの世界の中ではなく、画面に貼り付ける表示を作ります。
- Hierarchyで右クリック>UI>Canvas
- Render Mode:Screen Space – Overlay
- Canvas ScalerのUI Scale Mode:Scale With Screen Size
- Reference Resolution:X 1920、Y 1080


- 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


- バーの画像は「まっさらな白い四角」を使う
- 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に付け替えてください。


表示を更新するスクリプト
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をドラッグ


動かして確認!


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





バーがたまっていくのを見ているだけで、なんだか楽しいです!
これで、倒す→金平糖が落ちる→拾う→レベルが上がるという輪がつながりました。この輪がゲームの背骨で、次回はここに「強くなる」をつなげます。
バランスを考えよう
数字はすべてExpGemとPlayerLevelにまとまっているので、遊びながら変えてみてください。
| 変えたいこと | 変える数字 |
|---|---|
| レベルアップを早くする | PlayerLevelのNeedExpの式(5 + Level * 3の数を小さく) |
| 金平糖を集めやすくする | ExpGemプレハブのMagnet Radius(1.5→3など) |
| 吸い寄せをキビキビさせる | ExpGemプレハブのAcceleration |
| 金平糖1個でもらえる経験値を変える | ExpGemプレハブのExp(すべての金平糖に効きます) |
Magnet Radiusを大きくすると、ゲームの手触りがはっきり変わります。広いと勝手に集まって気持ちいい代わりに、「拾いに行く」という判断がなくなります。実際に3や5を入れて見比べてみると面白いと思います。
あとがき
今回は倒した敵から金平糖を落として、集めるとレベルが上がるところまで作りました。画面にバーが出ただけで、一気にゲームらしくなったのではないでしょうか!次回はレベルが上がったときにゲームを止めて、3つの強化から1つ選ぶ画面を作りたいと思います。
コメント
コメント一覧 (1件)
[…] 前回:#6 金平糖を集めてレベルアップしよう […]