Unity初心者向け:ResourceManagerを作ってゲーム内の資源を管理してみよう

C#

こんにちは、とまです。
本日は、ResourceManagerを作成したところで、
どうしてもコードが複雑になってしまうなと思い、この記事を書くことにしました。

今自分は、放置系終末世界の街づくりゲームを作っているのですが、
「木材」「石材」「お金」「食料」など、さまざまな資源を扱う必要が出てきます。

例えば、

  • 木材を集める
  • 建物を建てるために木材を消費する
  • 石材を生産する
  • お金を使って施設を建設する
  • 現在の所持数を画面に表示する

といった処理です。
こういった素材は改善やアップデートなどで膨らみがちになってしまい、
がさつに管理してしまうと、スクリプトがでっぷりと太ってしまうというリスクがあります。

加えて、こうした資源を、それぞれの建物やシステムがバラバラに管理してしまうと、ゲームが大きくなったときに非常に管理しづらくなります。

そこで今回は、ゲーム内の資源をまとめて管理する「ResourceManager」を
今後の環境も想定して、作っていきます。

今回使用するスクリプトはこちらです。

using System.Collections.Generic;
using UnityEngine;

public class ResourceManager : MonoBehaviour
{
    public static ResourceManager Instance { get; private set; }

    [System.Serializable]
    public class ResourceAmount
    {
        public ResourceData resource;
        public int amount;
    }

    [Header("初期資源")]
    [SerializeField] private List<ResourceAmount> startingResources = new();

    private Dictionary<ResourceData, int> resources = new();

    private void Awake()
    {
        if (Instance != null && Instance != this)
        {
            Destroy(gameObject);
            return;
        }

        Instance = this;

        InitializeResources();
    }

    private void InitializeResources()
    {
        resources.Clear();

        foreach (var entry in startingResources)
        {
            if (entry.resource == null)
                continue;

            resources[entry.resource] = entry.amount;
        }
    }

    public void Add(ResourceData resource, int amount)
    {
        if (resource == null)
            return;

        if (!resources.ContainsKey(resource))
            resources[resource] = 0;

        resources[resource] += amount;
    }

    public bool Has(ResourceData resource, int amount)
    {
        if (resource == null)
            return false;

        return GetAmount(resource) >= amount;
    }

    public bool Spend(ResourceData resource, int amount)
    {
        if (!Has(resource, amount))
            return false;

        resources[resource] -= amount;

        return true;
    }

    public int GetAmount(ResourceData resource)
    {
        if (resource == null)
            return 0;

        if (!resources.TryGetValue(resource, out int amount))
            return 0;

        return amount;
    }
}

今回はこのコードを、C#やUnityを始めたばかりの方でも分かるように、一つずつ見ていきましょう。

ResourceManagerとは?

まず、ResourceManagerという名前から見てみましょう。

Managerというのは、ゲーム開発ではよく使われる名前で、
簡単に言えば「管理するもの」という意味です。

つまり、今回のスクリプトは、

Resource(資源)を管理するクラス

ということになります。

今回のゲームで考えると、

  • 木材
  • 石材
  • 食料
  • お金

などの資源を管理する役割を持たせます。

例えばゲーム中に、

木材:150
石材:80
お金:1000

という状態だったとします。
この「現在いくつ持っているのか」をResourceManagerが管理します。

建物を建てるときには、

「木材を50持っていますか?」

とResourceManagerに確認し、持っていれば、

「では50使います」

という処理を行います。

このように、資源に関する処理をResourceManagerに集約するのが今回の目的です。

【スクショ指示:ここにUnityのHierarchyでResourceManagerを配置している状態のスクリーンショットを入れると分かりやすいです】

なぜ資源を一か所で管理するの?

例えばResourceManagerを作らず、建物ごとに資源を管理したとします。

木こり小屋が、

木材 +10

を管理し、石切り場が、

石材 +5

を管理し、建築システムが、

木材 -50
石材 -20

を管理する……。

このような作り方をすると、ゲームが大きくなったときに、

「今の木材の数は、どこに保存されているんだろう?」

という問題が起きてしまいます。

そこで、Resource Managerを起点にして、資源の管理場所を一つにします。

こうすることで、他のシステムはResourceManagerに問い合わせるだけでよくなります。

それでは、その仕組みについて、コードを分割しながら説明していきます。

ResourceManager : MonoBehaviourについて

最初の重要な部分がこちらです。

public class ResourceManager : MonoBehaviour

これは、

ResourceManagerというクラスを作ります

という意味です。

そして、

: MonoBehaviour

となっていることで、
UnityのGameObjectにこのスクリプトを付けられる(アタッチできる)ようになります。

MonoBehaviourを継承することで、UnityからAwake()などのイベントも呼び出してもらえるようになります。

InstanceでResourceManagerにどこからでもアクセスする

次に、この部分です。

public static ResourceManager Instance { get; private set; }

これは今回のコードの中でも、少し難しい部分です。
しかし、ひとつづつ見ていけば、怖くはありません。

まず、ここでは

ResourceManagerをどこからでも利用しやすくするための入口
を作っていると考えてください。

例えば建築システムから、

ResourceManager.Instance.Spend(wood, 50);

と書くことで、

「ResourceManagerさん、木材を50使ってください」

とお願いできます。

このような仕組みをシングルトンと呼びます。

今回のゲームでは、「ゲーム全体で資源を管理する人は一人」という考え方なので、
この方法と相性が良いです。

ResourceAmountは「資源+数量」のセット

続いてこちらです。

[System.Serializable]
public class ResourceAmount
{
    public ResourceData resource;
    public int amount;
}

ここではResourceAmountという小さなクラスを作っています。

このクラスは、

「どの資源を、いくつ持っているか」

という情報をセットで扱うためのものです。

例えば、

木材:100

という情報を一つのセットとして扱えます。

ここで登場するResourceDataは、資源そのものを表すデータです。

  • 木材
  • 石材
  • 食料
  • お金

といったものを用意しておくイメージです。
一方、ResourceAmountは、

  • 木材 + 100
  • 石材 + 50
  • お金 + 1000

のように、資源と数量を組み合わせる役割を持っています。

[System.Serializable]って何?

ResourceAmountの上に、

[System.Serializable]

と書かれています。

これは、

「このクラスをUnityのInspector上で扱えるようにしてください」

という意味です。

そのため、後述するstartingResourcesと組み合わせることで、
UnityのInspectorから初期資源を設定できるようになります。

例えば、

といった設定ができます。

startingResourcesはゲーム開始時の資源

次はこちらです。

[Header("初期資源")]
[SerializeField] private List<ResourceAmount> startingResources = new();

まず、

List<ResourceAmount>

は、

ResourceAmountを複数まとめて保存するリスト

です。

つまり、

木材 100
石材 50
お金 1000

という複数のデータをまとめて持てます。

そして、

[SerializeField]

が付いているため、privateな変数ですがUnityのInspectorから設定できます。

つまり、コードを書き換えなくても、

「このゲームは最初に木材を50個持っていることにしよう」

と設定できるわけです。

Dictionaryで現在の資源量を管理する

そして、資源管理の中心となるのがこちらです。

private Dictionary<ResourceData, int> resources = new();

Dictionaryは、

「キー」と「値」をセットで保存するデータ構造

です。

今回の場合、

「この資源は何個持っているか」

を保存できます。

今回のResourceManagerでは、このDictionaryがゲーム中の現在の資源量を保存する場所になります。

Awake()で初期化する

次に、

private void Awake()
{
    if (Instance != null && Instance != this)
    {
        Destroy(gameObject);
        return;
    }

    Instance = this;

    InitializeResources();
}

を見ていきます。

Awake()はUnityがGameObjectを初期化するときに呼び出してくれる特別な関数です。

ここでは主に、

  1. ResourceManagerが複数存在しないようにする
  2. Instanceを設定する
  3. 初期資源を読み込む

という3つを行っています。

ResourceManagerが2つあったら?

まず、

if (Instance != null && Instance != this)
{
    Destroy(gameObject);
    return;
}

です。

これは、

ResourceManagerがすでに存在するなら、重複したResourceManagerを削除する

という処理です。

例えば間違って、

Hierarchy

ResourceManager
ResourceManager

と2つ配置してしまった場合、片方を削除します。

ゲーム全体の資源を管理するものなので、基本的には一つだけ存在していれば十分です。

InitializeResources()で初期資源を登録する

次に、

InitializeResources();

が呼ばれています。

この関数の中身はこちらです。

private void InitializeResources()
{
    resources.Clear();

    foreach (var entry in startingResources)
    {
        if (entry.resource == null)
            continue;

        resources[entry.resource] = entry.amount;
    }
}

ここでは、Inspectorで設定したstartingResourcesを、実際に使用するresourcesへ登録しています。

例えばInspectorで、

木材 100
石材 50
お金 1000

と設定していたら、

resources

木材 → 100
石材 → 50
お金 → 1000

という状態になります。

foreachで初期資源を一つずつ処理する

ここが、

foreach (var entry in startingResources)

です。

foreachは、

リストの中身を一つずつ取り出して処理する

ための構文です。

例えば、

startingResources

木材 100
石材 50
お金 1000

なら、

1回目 → 木材100
2回目 → 石材50
3回目 → お金1000

というように処理されます。

Add()で資源を増やす

ここからは、実際に資源を操作する関数を見ていきます。

まずはAdd()です。

public void Add(ResourceData resource, int amount)

名前の通り、

指定した資源を増やす

ための関数です。

例えば木材を50増やしたい場合、

ResourceManager.Instance.Add(wood, 50);

とします。

木こり小屋などの生産施設から、この関数を呼び出すことができます。

Has()で資源が足りるか確認する

次は、

public bool Has(ResourceData resource, int amount)

です。

これは、

指定した資源を必要な数だけ持っているか?

を確認します。

例えば建物の建築に木材50が必要だったとします。

ResourceManager.Instance.Has(wood, 50);

現在、木材 100なら、

100 >= 50

なのでtrueが返ります。

逆に、木材 30しかなければ、

30 >= 50

なのでfalseになります。

この仕組みを使えば、

「資源が足りないから建築できない」

という処理を簡単に作れます。

Spend()で資源を消費する

そして、建物を建てるときなどに使うのが、

public bool Spend(ResourceData resource, int amount)

です。

これは、

指定した資源を消費する

ための関数です。

例えば、

ResourceManager.Instance.Spend(wood, 50);

とすると、木材を50消費します。

ただし、この関数には重要なポイントがあります。

if (!Has(resource, amount))
    return false;

という部分で、

必要な資源が足りない場合は、消費処理を行わない

というようになっています。

そのため、足りていない素材を消費しようとしても、素材は減りません。

そして、

return false;

によって、

「消費できませんでした」

と呼び出し側に伝えます。

逆に消費できれば、

return true;

が返ります。

そのため、

if (ResourceManager.Instance.Spend(wood, 50))
{
    // 建築成功
}

のような書き方ができます。

これは街づくりゲームの建築システムと非常に相性の良い仕組みです。

GetAmount()で現在の資源量を取得する

最後が、

public int GetAmount(ResourceData resource)

です。

これは、

現在の資源量を取得する

ための関数です。

例えば、

int woodAmount = ResourceManager.Instance.GetAmount(wood);

とすれば、現在の木材の数を取得できます。

この機能は、ゲーム画面に資源量を表示するときに特に重要です。

例えばUIに、

木材:150
石材:80
お金:1000

と表示したい場合に利用できます。

ResourceManagerの4つの基本機能

ここまでの内容を整理すると、ResourceManagerには大きく4つの役割があります。

資源を増やす

Add()

木材を生産したときなどに使用します。

資源が足りるか確認する

Has()

建物を建てられるか確認するときなどに使用します。

資源を消費する

Spend()

建物の建築費などを支払うときに使用します。

現在の資源量を取得する

GetAmount()

UIに資源数を表示するときなどに使用します。

この4つを覚えておくだけでも、ResourceManagerの基本的な使い方がかなり見えてきます。

実際のゲームではどう使う?

例えば、木材50個が必要な家を建てるとします。

まず、

ResourceManager.Instance.Has(wood, 50)

で木材が足りているか確認します。

足りていれば、

ResourceManager.Instance.Spend(wood, 50);

で木材を消費します。

そして建物を配置します。

流れとしては、

建物を建てたい
↓
木材50を持っている?
↓
YES
↓
木材50を消費
↓
建物を建築

となります。

逆に木材が30しかなければ、

建物を建てたい
↓
木材50を持っている?
↓
NO
↓
建築しない

となります。

このように、ResourceManagerは建築システムそのものではありません。

あくまで、

「資源がいくつあるか」「増やす」「減らす」

という資源管理だけを担当します。

このように役割を分けておくことで、後からゲームを大きくしやすくなります。

今回のResourceManagerのまとめ

今回作ったResourceManagerは、ゲーム全体の資源を管理するためのクラスです。

重要なポイントをざっくりとまとめると、

  • ResourceManagerが資源を一元管理する
  • ResourceDataで資源の種類を表す
  • Dictionaryで現在の所持数を管理する
  • startingResourcesで初期資源を設定する
  • Add()で資源を増やす
  • Has()で資源が足りるか確認する
  • Spend()で資源を消費する
  • GetAmount()で現在の資源量を取得する
  • Instanceを使って他のシステムからResourceManagerを利用する

という構造になっています。ここまでがResourceManagerの全貌でした。

お疲れ様です。いかがでしょうか?
もしここが分からない!というところがありましたら、コメントしていただけるとありがたいです。

ではでは、ここまでお付き合いいただきありがとうございました…!

またお会いしましょう~。

コメント

タイトルとURLをコピーしました