注:当ブログでは広告を利用しています。

Unity軽量化作戦:複数のUpdate記述を最小限に抑えメモリ消費を抑えよ

おはよう。数日悩みながらやったプログラムがある。
いいゲームを作っていくと、Updateの個数も多くなる。

複数のスクリプトに複数のUpdate。
色々調べたところ「void Update量」を減らすと、メモリにも優しいとあった。

そこでメモリに優しくサクサク動かすために、
Updateを抑えるための方法を書いていく。

 

マネージャーを作ってUpdate個数を抑える

今回の記事を作るに至り、二つの参照サイトが参考になった。
最初、思うようにうまくいかず、混乱したが……。

下記サイトの作者には本当、お礼を述べたい。

今から作った内容は二つの参照サイト(特に後者)を基に作った、
Update記述を少しでも減らし、メモリに優しいプログラムを創り上げていく。

私がやった行動は下記参照サイトに基づき、マネージャーシーン作成だった。

参照:MonoBehaviourのUpdateに頼らないクラス設計 

参照:Unity の 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();も使えよ」エラーが出る。

一つの機能かつすべて使わなければいけないと思って、基本一つに絞っておこう。

参照:interfaceが理解できない方へ「Unityで使うinterfaceの5つの使い方」

 

インスタンスと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だ。

参照:UnityでListを使ってみよう

参照:Listや配列を使ったときのメモリ量

参照:Lengthと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関数でないシーン移動直前でもいい。

敵なら敵を倒した際にいれるなど、少しでもプログラムを楽させるよう、考えよう。

参照:UnityのOnEnable、OnDisable、OnDestroyメソッドについて

 

プロファイラーかつテストプレイで比較してみよう

不具合

最後にプロファイラーを起動させ、メモリやFPSなどを確かめてみよう。

FPSは動画の滑らかさを示す数値で、高いほど滑らかだ。

赤く囲った部分はGCで、プレイ中に使ったものをゴミ箱へポイするために動くそうだ。
この時の重たさは仕方ない。

またEditorLoopもUnityエディターでの動きだから無視ししていい。

見るべき部分はPlayerLoopだ。

Updateを実装してメモリやFPSに大きな変化が出る場合ならやるべきだが、
あまり変化がないなら使わなくてもいい。

むしろ自分の場合、色々入れたら複数の敵が意図しない動きをとるなど、
個人的に不具合を感じた。まだ改良の余地がある。

何でもかんでも入れていいわけでない。
スクリプトによってはNull参照エラーが起きるので、
一つずつ入れながら、テストプレイを通して確認しておこう。

 

不具合例:原因はSetActive(キャラチェンジ)

setactive update

ひとまとめUpdateに失敗する例として、
ボタンを押せばキャラクターを変える(キャラチェンジ)操作だ。

私の場合、SetActiveを通して別オブジェクトに変える
変えたときに攻撃ボタンを押すと、前のキャラの攻撃が出てきた。

なんでもかんでもやっていいわけでないとわかった。
SetActiveを使わないオブジェクトはやっても問題ないみたいだ。

一部動きに変な展開が生じるので、別の部分をいじらねばならぬ。

もちろんUpdate関数の前に事前キャッシュを行って、
少しでもスクリプトに優しい処理を行わなければならない。

ついでに複数かぶる・共通項目があるなら継承もつかんでおこう。
継承したうえでUpdate量を減らし、さらにUpdateマネージャーを使えば、かなり減らせる。

ぜひ参考にして、パソコンにもUnityにも優しいプログラミングを心がけてほしい。

お願い

めがびちゃんからお知らせ♪

お知らせ

megabe-0へ訪問した"本当"の理由

まさか記事の書き形一つでこうなるとは…

お願い1

Writer軽い自己紹介

ティラノスクリプトや小説家になろう、ピクシブ他で物語を書きながら、 「私が気になった事件」の裏側を作家の視点で書いているおっさん。

プロフィール画像は自画像でなく、Megabe-0ブログのマスコット、めがびちゃん。

 

雷が苦手で、光を見ると頭が固まる(元から固い)。 月初めは墓参りと神社参拝を行い、賽銭箱へ1万円を入れた際、とても気持ちがすっきりした。

 

■ 簡単な自分史 ■

0歳:釧路のある病院で生まれる。暇さえあれば母乳を吸って、ご飯を4膳食べても体重が落ちるほど、母のダイエットにものすごく貢献したらしい

 

3歳:行方不明になり、全裸で海を泳ごうとしたところ、いとこのお姉さんに発見され、この世へ留まる

 

8歳:自分のお金でおもちゃのカードを初めて買い、経済を知る。なぜか父親に怒られ、家出するがすぐに見つかる。

 

12歳:学校で給食委員長になる。委員長として初めて全校生徒の前にて演説する際、原稿用紙を忘れてアドリブで笑いを誘いながらも何とかやり過ごし、多くの生徒に名前と顔を覚えてもらう。また、運動会の騎馬戦では変なアドリブを行い、多くの笑いを誘った。

 

18歳:初めて好きな人ができたけれど、告白が恥ずかしくてついにできず、別れたことを今でも根に持っている(妻となる人にははっきり言えてよかった)

 

21歳:大学在学中、アルバイトを始める。人手不足かつとても忙しい日々を過ごしながら「どうせなら自分から楽しいことをしていきたいなあ⇒起業って選択肢があるのか」働き方の選択肢を見つける

 

27歳:自分で作った会社がうまくいかず、一度たたんで都落ち。実家でとことん自分を責める日が続く。「何をやっても駄目だな、お前は」など。自分を責めても自殺ができず、体中から毒素があふれ出て苦しい日々を送る。寝るのも怖かった日々。

 

28歳:「このままじゃいけない」決心を決め、小学校からの勉強をやり直す。高校の勉強で躓きながらも、学び直すうちに「自分は何もわかっていなかったんだなあ」大切な教えに気づかされる。 加えて、小説やイラストなど「今までの自分が手を出さなかった分野」に手を伸ばしてみた。

 

29歳:「定義」と「自己肯定」こそが生き方を決めると気づかされ、不安な日々が起きても、心が強くなったと感じる。でも子供の誘惑にはめっぽう弱くなる。

 

35歳:人生初の交通事故(物損)に出会う。冬道の運転で車を上下に大回転(スピンではない)を体型氏、何とか命を取り留め、なぜ生きているのかわからない状態に陥る。

自分の生き方はすべて自分が握っている。わずかな瞬間にしか現れない「自分の真実」を表に引きずり出し、ピンチからチャンスを生み出す発想や視点をブログやメルマガ他で提供中。