こんにちは、とまです。
本日は、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 : MonoBehaviourについて
- InstanceでResourceManagerにどこからでもアクセスする
- ResourceAmountは「資源+数量」のセット
- [System.Serializable]って何?
- startingResourcesはゲーム開始時の資源
- Dictionaryで現在の資源量を管理する
- Awake()で初期化する
- ResourceManagerが2つあったら?
- InitializeResources()で初期資源を登録する
- foreachで初期資源を一つずつ処理する
- Add()で資源を増やす
- Has()で資源が足りるか確認する
- Spend()で資源を消費する
- GetAmount()で現在の資源量を取得する
- ResourceManagerの4つの基本機能
- 実際のゲームではどう使う?
- 今回のResourceManagerのまとめ
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を初期化するときに呼び出してくれる特別な関数です。
ここでは主に、
- ResourceManagerが複数存在しないようにする
Instanceを設定する- 初期資源を読み込む
という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の全貌でした。
お疲れ様です。いかがでしょうか?
もしここが分からない!というところがありましたら、コメントしていただけるとありがたいです。
ではでは、ここまでお付き合いいただきありがとうございました…!
またお会いしましょう~。


コメント