こんにちは、とまです。
今回は前の記事に掲載したResourceManagerを利用した生産設備を作成してみたので
その原理となるスクリプトを紹介していきます!
ResourceManagerについてはこちらで詳しく紹介しています!
Unity初心者向け:ResourceManagerを作ってゲーム内の資源を管理してみよう | とま開発室
それでは説明に入っていきます!!
ProductionBuildingとは?
今回作るのは、放置型の街づくりゲームなどで使える「生産設備」のスクリプトです。
例えば、ゲームの中に「木材生産所」という建物があるとします。
木材生産所には、
- 木材を生産する
- 1回につき1個生産する
- 1秒ごとに生産する
- 稼働している間は自動的に生産する
という仕組みが必要になります。
そこで登場するのがProductionBuildingです。
このスクリプトは簡単に言うと、
「この建物は何を、何個、何秒ごとに生産するのか」を管理するスクリプト
です。
建物を配置したり、資源そのものを管理したりするのではなく、「生産する」という部分に役割を絞っています。
最初にスクリプト全体を見てみよう
まずは今回のスクリプト全体を確認してみましょう。
using UnityEngine;
public class ProductionBuilding : MonoBehaviour
{
[Header("生産設定")]
[SerializeField] private ResourceData productionResource;
[SerializeField] private int productionAmount = 1;
[SerializeField] private float productionInterval = 1f;
private bool isProducing;
private float productionTimer;
public ResourceData ProductionResource => productionResource;
public int ProductionAmount => productionAmount;
public float ProductionInterval => productionInterval;
/// <summary>
/// 生産を開始する
/// </summary>
public void StartProduction()
{
if (productionResource == null)
{
Debug.LogWarning($"{name}: 生産資源が設定されていません。");
return;
}
isProducing = true;
productionTimer = 0f;
}
/// <summary>
/// 生産を停止する
/// </summary>
public void StopProduction()
{
isProducing = false;
}
private void Update()
{
if (!isProducing)
return;
productionTimer += Time.deltaTime;
if (productionTimer >= productionInterval)
{
Produce();
productionTimer -= productionInterval;
}
}
private void Produce()
{
if (ResourceManager.Instance == null)
{
Debug.LogWarning("ResourceManagerが存在しません。");
return;
}
ResourceManager.Instance.Add(
productionResource,
productionAmount
);
}
}一見すると少し難しく見えますが、やっていることを順番に分解していけば、大丈夫です!
生産する資源と生産量を設定する
まずはこちらの部分です。
[Header("生産設定")]
[SerializeField] private ResourceData productionResource;
[SerializeField] private int productionAmount = 1;
[SerializeField] private float productionInterval = 1f;ここでは、この建物の生産内容を設定しています。
productionResourceは「何を生産するのか」を表します。
例えば木材生産所なら木材、石材生産所なら石材を設定します。
productionAmountは「1回の生産で何個作るのか」です。
初期値は1なので、設定を変更しなければ1回につき1個生産します。
productionIntervalは「何秒ごとに生産するのか」です。
初期値は1fなので、1秒ごとに生産する設定になっています。
例えば、
木材・生産量2・生産間隔5秒
と設定すれば、5秒ごとに木材を2個生産する建物になります。
SerializeFieldを使う理由
ここでUnity初心者が疑問に思いやすいのが、
「privateなのにInspectorから設定できるの?」
という点です。
例えば、
[SerializeField] private int productionAmount = 1;となっています。
通常、privateな変数はUnityのInspectorから設定できません。
しかし、[SerializeField]を付けることで、
privateのままInspectorから値を設定できる
ようになります。
これは非常に便利です。
例えばUnityのInspector上で、
「木材生産所は5秒ごとに3個生産する」
といった設定を、コードを書き換えずに変更できます。
建物ごとに異なる生産能力を設定したい場合にも役立ちます。
生産中かどうかを管理する
次に、この2つの変数を見てみましょう。
private bool isProducing;
private float productionTimer;isProducingは、現在その建物が生産中なのかを管理します。
bool型なので、値はtrueかfalseのどちらかです。
生産中ならtrue、生産していなければfalseになります。
もう一つのproductionTimerは、生産開始からどのくらい時間が経過したのかを記録するタイマーです。
例えば生産間隔が5秒なら、
0秒、1秒、2秒、3秒、4秒、5秒……
と時間を計測し、5秒になったら生産します。
つまり、
isProducingは「生産するかどうか」
productionTimerは「次の生産まで何秒経ったか」
を担当しています。
StartProductionとStopProduction
生産を開始するときにはStartProduction()を使います。
public void StartProduction()
{
if (productionResource == null)
{
Debug.LogWarning($"{name}: 生産資源が設定されていません。");
return;
}
isProducing = true;
productionTimer = 0f;
}最初に、生産する資源が設定されているか確認しています。
もし設定されていなければ、警告を表示して処理を終了します。
これは、
Inspectorで生産資源を設定し忘れた場合などに、ゲームがエラーになってしまうのを防ぐためです。
いわゆる、ヌル(参照なし)チェックというやつですね。
問題がなければ、
isProducing = true;によって生産状態にします。
さらに、
productionTimer = 0f;によってタイマーを0秒に戻します。
反対に、生産を停止するときはStopProduction()を使います。
public void StopProduction()
{
isProducing = false;
}とてもシンプルですね。
falseにすることで、後ほど説明するUpdate()が生産処理を行わなくなります。
Updateで時間を計測する
このスクリプトの中心となるのがUpdate()です。
private void Update()
{
if (!isProducing)
return;
productionTimer += Time.deltaTime;
if (productionTimer >= productionInterval)
{
Produce();
productionTimer -= productionInterval;
}
}UnityのUpdate()は、ゲーム中に何度も繰り返し呼び出されます。
そのため、ここで時間を計測します。
まず、
if (!isProducing)
return;によって、生産中でなければ何もしないようにしています。
生産中の場合だけ、
productionTimer += Time.deltaTime;を実行します。
Time.deltaTimeは、前のフレームから今回のフレームまでに経過した時間です。
これを毎フレーム加算することで、実際に経過した時間を計測できます。
そして、
if (productionTimer >= productionInterval)で、「生産する時間になったか?」を確認します。
例えば生産間隔が1秒なら、タイマーが1秒以上になったところでProduce()が呼び出されます。
Produceで実際に資源を増やす
実際の生産処理はProduce()にまとめています。
private void Produce()
{
if (ResourceManager.Instance == null)
{
Debug.LogWarning("ResourceManagerが存在しません。");
return;
}
ResourceManager.Instance.Add(
productionResource,
productionAmount
);
}ここではまず、
ResourceManager.Instanceが存在するか確認しています。
ResourceManagerは、ゲーム全体の資源を管理するクラスです。
例えば、
- 木材が10個ある。
- 石材が5個ある。
- 食料が20個ある。
といった情報を管理します。
生産設備自身が「木材を100個持っている」といった管理をするのではなく、資源の管理はResourceManagerに任せます。
そして、
ResourceManager.Instance.Add(
productionResource,
productionAmount
);によって資源を追加します。
例えば、
「生産する資源=木材」
「生産量=2」
だった場合、木材を2個追加する処理になります。
今回はわかりやすいように
private void Produce()
{
if (ResourceManager.Instance == null)
{
Debug.LogWarning("ResourceManagerが存在しません。");
return;
}
ResourceManager.Instance.Add(
productionResource,
productionAmount
);
Debug.Log(
$"{name}: {productionResource.name}を{productionAmount}個生産しました。" +
$" 現在の所持数: {ResourceManager.Instance.GetAmount(productionResource)}"
);
}デバッグログ(確認用のログ)
を埋め込んでみましたので、テストをしてみましょう。
Consoleを開くと、一秒ごとにwoodが増えていることが確認できました!
タイマーを少しだけ戻している理由
Update()の最後に、
productionTimer -= productionInterval;という処理があります。
ここは少し分かりにくいポイントです。
例えば、生産間隔が1秒で、タイマーが1.02秒になったとします。
この場合、
1.02秒経過
↓
生産する
↓
1秒分を差し引く
↓
0.02秒残る
という処理になります。
この「0.02秒」を残しておくことが重要です。
もし生産後にタイマーを完全に0にしてしまうと、フレーム処理の影響で少しずつ
生産タイミングがずれていく可能性があります。
そのため、
productionTimer -= productionInterval;として、余った時間を次の生産に引き継いでいます。
このスクリプトの役割を整理しよう
ここまでの内容を整理すると、ProductionBuildingの仕事は次のようになります。
1. 生産する資源を決める
木材なのか、石材なのかなどを設定します。
2. 生産量を決める
1回につき何個生産するのかを設定します。
3. 生産間隔を決める
何秒ごとに生産するのかを設定します。
4. 生産中かどうかを管理する
StartProduction()とStopProduction()で切り替えます。
5. 時間を計測する
Update()でタイマーを進めます。
6. 資源を追加する
設定した時間が経過したらResourceManagerに資源を追加します。
このように、建物の「生産」に関係する処理だけを担当しています。
他のシステムと役割を分けることが大切
今回のようなゲームを作るときは、1つのスクリプトにすべての処理を書かないことが大切です。
例えば、
ProductionBuilding
建物が何をどのくらいの間隔で生産するのかを管理する。
ResourceManager
木材や石材など、ゲーム内の資源を管理する。
BuildingPlacement
建物をマップ上に配置する処理を担当する。
UI
プレイヤーに資源量や生産状況などを表示する。
というように、それぞれに役割を持たせます。
こうしておくと、後からゲームの仕様を追加するときにも管理しやすくなります。
例えば将来的に、
「木材生産所をアップグレードしたい」
「電力がないと生産できないようにしたい」
「原材料として木材を消費して別の資源を作りたい」
といった機能を追加する場合でも、どのスクリプトを変更すればよいのか分かりやすくなります。
最後に
以上が、このスクリプト全体の流れでした!
いかがだったでしょうか。少し難しいと思う個所もあったかと思います。
どうしても、このような生産系のスクリプトは大きな一つのまとまりになりやすく、
しっかりと設計し、機能ごとに責任分けをすることが大事なのだと、今回学ぶことができました。
もし分からない点や、難しい点などがあれば、コメントして頂ければ幸いです。
ここまでお付き合いいただきありがとうございました!
またお会いしましょう~


コメント