おはよう。数日悩みながらやったプログラムがある。
いいゲームを作っていくと、Updateの個数も多くなる。
複数のスクリプトに複数のUpdate。
色々調べたところ「void Update量」を減らすと、メモリにも優しいとあった。
そこでメモリに優しくサクサク動かすために、
Updateを抑えるための方法を書いていく。
マネージャーを作ってUpdate個数を抑える
#unity もう11時30分を越えてた。#プログラミング 一環としてメモリ消費削減対策をやりまくった結果だが……本当に軽くなっているのだろうか。メモリ平均は3から5ミリ秒、FPSは30ちょいいって、60あたりが平均。まだ重たいのか、削れる要素があるのか…… pic.twitter.com/z1VSECo92s
— せんけん (@megabi0) August 8, 2023
今回の記事を作るに至り、二つの参照サイトが参考になった。
最初、思うようにうまくいかず、混乱したが……。
下記サイトの作者には本当、お礼を述べたい。
今から作った内容は二つの参照サイト(特に後者)を基に作った、
Update記述を少しでも減らし、メモリに優しいプログラムを創り上げていく。
私がやった行動は下記参照サイトに基づき、マネージャーシーン作成だった。
マネージャースクリプト全体像
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System;
//
public interface IUpdatable{void OnUpdate();}
public interface EUpdatable{void OnUpdateE();}
public class UpdateManager : MonoBehaviour
{
//外部スクリプト、ただ一つのスクリプトは直接代入
[SerializeField] SaveStage saveStage;
[SerializeField] GameManager gameManager;
//staticインスタンス化
private static UpdateManager updateM;
public static UpdateManager uInstance(){ return updateM;}
//リスト化し複数のスクリプトで動かせるようにする。
//new()の数字はあなたが入れる予定のスクリプト数を想定して数を入れる。
//List系は()の中に数字を入れると、メモリ消費を抑えてくれる
private List<IUpdatable>iUpdatable = new(10);//Door用
private List<EUpdatable>eUpdatable = new(100);//Enemy,Boss,Mob用
private readonly byte intervals = 60;
private byte i , ie , ip;
//インスタンス宣言、ここでも違うスクリプトでも使えるようにしておく
void Awake(){updateM = this;}
//それぞれのStart関数におく(後で提示)
public void Register(IUpdatable i){iUpdatable.Add(i);}
public void RegisterE(EUpdatable i){eUpdatable.Add(i);}
//シーン移動直前にでもおく、リストを消してUpdateを止める
public void UnRegister(IUpdatable i){iUpdatable.Remove(i);}
public void UnRegisterE(EUpdatable i){eUpdatable.Remove(i);}
//Updateはここにおいて、他のスクリプトから"元Update関数"をもってくる
void Update(){
//まずは唯一のオブジェクトから。唯一とはヒエラルキー上にただ一つ。シーンは複数あり。
gameManager.UpdatePlusT();//タイムを止める
if (Time.frameCount % intervals == 0){//およそ1秒おきに更新
saveStage.UpdatePlus();
gameManager.UpdatePlus();
}
//ここから配列、プレハブ化かつ複数のオブジェクトとスクリプトに対し、行う。
//例えば敵を作るとき、基本複数なので共通項目を「継承」をさせておく
for( i=0; i < iUpdatable.Count; ++i){
if (Time.frameCount % intervals == 0)iUpdatable[i].OnUpdate();
}
for( ie=0; ie < eUpdatable.Count; ++ie){
eUpdatable[ie].OnUpdateE(); }
}//Update
}
二つの参照サイトを基に作成した。スクリプト名は「UpdateManager」だ。
複数スクリプトの解説を後に回すとして、
最初に唯一ここでしか使わないスクリプトのUpdateは、
[SerializeField]で直接スクリプトファイルを持ち込んだ方が早い。
※事前にゲームオブジェクトにスクリプトファイルをアタッチしておく。
後は元がvoid Update()だった場所を、
gameManager.UpdatePlus();
置き換えれば終わりだ。
アイテム及び敵のような複数でなく、一つしか使わない部分は直で入れたほうが早い。
次からは敵やアイテム、ドアなど複数だ。
まずinterfaceって?
public interface IUpdatable{void OnUpdate();}
MonoBehaviourより前に一つ関数を置いている。
MonoBehaviourを置いている箇所はシーン移動時に全て消える。
そこでシーンを移動しても「消さない」ためにMonoBehaviourをいれない。
(saveファイル作成時にも重要!)
interfaceを置くと、大切なデータ処理を外部から守れるという。
また「自分はこれをするよ」他プログラマへの宣言でもある。
下記参照サイトにも記してある通り、一つの機能しか使えない。
複数の変数を置いた場合、「これもないとエラーだからな」叱られた。
例:public interface EUpdatable{void OnUpdateE();void OnUpdateP();}
OnUpdateE();しか使ってないと「void OnUpdateP();も使えよ」エラーが出る。
一つの機能かつすべて使わなければいけないと思って、基本一つに絞っておこう。
インスタンスとStatic
//UpdateManager.cs
private static UpdateManager updateM;
public static UpdateManager uInstance(){ return updateM;}
void Awake(){updateM = this;}
//ここから別のスクリプトに置く関数、必ずpublicをつける
public void Register(IUpdatable i){iUpdatable.Add(i);}
//--------------------//
//別の関数(例えばEnemyManager.cs)で
void Start(){UpdateManager.uInstance().Register(this);}
現時点で試していないが、私はこれでもいいと思っている。
詳しくはわからないんだよね。わざわざprivateで宣言した後、publicにする意味が。
public static UpdateManager updatemanager;
static関数でもprivateがつくと、そのスクリプトではどこでも自由に扱えるうえ、
どこでも値をサクサク入れてくれる。
※privateだと「ここには値が入るけど、別なところは入らんよ」が発生。
プログラミング君が混乱しないよう、privateとpubllic配慮しているのだろう。
上記インスタンスはいろいろなところで使うと思う。
私はsave、bgm管理で使用している。ぜひおさえておこう。
リストListと容量と配列
後はリストだが、こちらを見てもらった方が早い。
Listで重要な部分としてメモリがある。
private List<IUpdatable>iUpdatable = new(10);
new中に数字を入れている。数字を入れたほうがメモリに優しい。
※専門用語でキャパシティ(最大データ容量)という。
次にUpdate内に記述した配列だ。
for( i=0; i < eUpdatable.Count; ++i){Updatable[i].OnUpdate(); }
forでもforeachでもいいが、foreachはあまり使っていない。
ゲームオブジェクト系ならforeachが楽かもしれない。
public void Register(IUpdatable i){iUpdatable.Add(i);}
ここで次々とUpdatable[i]のi番を数えていった。
数える際、Countを使っている。同じ数えとしてLengthがある。
下記参照サイトによるとCountは動的(途中でリストを追加)、
Lengthは静的(最初からあり途中追加がない状態)に使うそうだ。
今回はaddで途中追加しているのでCountだ。
間隔を設けてUpdate頻度を減らす
private int intervals = 3;
if (Time.frameCount % intervals == 0)
後はUpdate更新頻度間隔だ。
Time.frameCountは経過したフレーム数をとる。
真ん中の「%」は割った余り(mod)を導き出す。
今回の場合、0より「割り切れた数」を示している。
上記例ではintervalsが3になっているので、3の倍数だけ更新するとわかる。
すでに参照したUnity最適化にあった。
中尉は一つ。作成したOnUpdate関数内にTime.deltaTimeがある場合、
フレームによって更新されるので、通常より時間が遅くなる。
通常なら3秒で発動するプログラムがあった場合、
倍の5秒、6秒ほどかかってしまう。
deltaTImeのある部分だけはframeを使用しないなど対策を練ってほしい。
個別にやるので、それぞれのスクリプトに任せるのが一番だ。
別スクリプトへの代入法
public class MobManager : MonoBehaviour ,IUpdatable//←これ(,EUpdatable)を必ずいれる
{
//キャッシュ省略
private readonly byte interval = 3;
//startにリスト登録代入
void Start(){UpdateManager.uInstance().Register(this);}
//前までvoid Update()だった場所、この中身がUpdateManagerに行く。
public void OnUpdate(){
if(GameManager.gameplaying != GameManager.nowPlay) return;
UpdateInside();
//ここにあなたが入れたいUpdate関数の中身を書く。通常用
//こちらは数秒おき更新用
if (Time.frameCount % interval == 0)UpdatePlus();
}//OnUpdate
注目すべきは最初の宣言だ。
public class MobManager : MonoBehaviour ,IUpdatable
{//以下。スクリプトの詳細を書く}
MonoBehaviourの隣に必ず「,Updatable」を代入する。
Updatableはinterface部分で作った関数名を入れる。
例えばpublic interface Obake{void OnUpdate();}なら
public class MobManager : MonoBehaviour ,Obake{}
次にStart関数にUpdateManagerでインスタンス化した関数を入れる。
Updateリストを追加するための関数を入れて、
UpdateManager関数に働いてもらうためだ。
そして元「void Update(){}」だった箇所を、
public void OnUpdate(){}
そこらにあるただの関数へ設定する。
必ずPublicをつけないと向こうへ反映されない。
するとMobManagerに元々あったvoid Updateが一つ減り、メモリの節約へとつながる。
どこでリストを開放させるか
//例:シーン変化時に起動させるなら
void Ondestroy(){UpdateManager.uInstance().UnRegister(this);}
リストを追加したら、最後にどこかで消さなければならない。
シーンを移動してもリストを追加したままなので不具合を起こす。
基本はOnDestroy関数を呼び出し(テストプレイを止めたり、シーン移動時に発動)
Updateを止めるだけなので、SetActive(false)前やコルーチン時、
OnDestroy関数でないシーン移動直前でもいい。
敵なら敵を倒した際にいれるなど、少しでもプログラムを楽させるよう、考えよう。
プロファイラーかつテストプレイで比較してみよう

最後にプロファイラーを起動させ、メモリやFPSなどを確かめてみよう。
FPSは動画の滑らかさを示す数値で、高いほど滑らかだ。
赤く囲った部分はGCで、プレイ中に使ったものをゴミ箱へポイするために動くそうだ。
この時の重たさは仕方ない。
またEditorLoopもUnityエディターでの動きだから無視ししていい。
見るべき部分はPlayerLoopだ。
Updateを実装してメモリやFPSに大きな変化が出る場合ならやるべきだが、
あまり変化がないなら使わなくてもいい。
むしろ自分の場合、色々入れたら複数の敵が意図しない動きをとるなど、
個人的に不具合を感じた。まだ改良の余地がある。
何でもかんでも入れていいわけでない。
スクリプトによってはNull参照エラーが起きるので、
一つずつ入れながら、テストプレイを通して確認しておこう。
不具合例:原因はSetActive(キャラチェンジ)

ひとまとめUpdateに失敗する例として、
ボタンを押せばキャラクターを変える(キャラチェンジ)操作だ。
私の場合、SetActiveを通して別オブジェクトに変える。
変えたときに攻撃ボタンを押すと、前のキャラの攻撃が出てきた。
なんでもかんでもやっていいわけでないとわかった。
SetActiveを使わないオブジェクトはやっても問題ないみたいだ。
一部動きに変な展開が生じるので、別の部分をいじらねばならぬ。
もちろんUpdate関数の前に事前キャッシュを行って、
少しでもスクリプトに優しい処理を行わなければならない。
ついでに複数かぶる・共通項目があるなら継承もつかんでおこう。
継承したうえでUpdate量を減らし、さらにUpdateマネージャーを使えば、かなり減らせる。
ぜひ参考にして、パソコンにもUnityにも優しいプログラミングを心がけてほしい。
