60fpsのその先へ!

スマホの物量限界に挑んだSTG

「アカとブルー」の開発設計
2018/5/9
藤岡 裕吾
株式会社タノシマス/プログラマー兼Unityエンジニア
藤岡 裕吾 Yugo Fujioka
ゲーム会社に4年、フリー4年目ぐらい?
Unity暦は5年目
株式会社タノシマス / プログラマー兼Unityエンジニア
ボーンデジタル 出版
Unity ゲームプログラミング・バイブル
11章・19章
技術評論社 出版
WEB+DB PRESS vol.104
はじめてのUnity特集
@fujiokoact
アカとブルー
2.5Dのオーソドックスな縦スクロール型STG
2017年8月10日にiOS/Android版がリリース
作曲はWASi303
App Store/Google Playにてお値段なんと960円!
2018年高田馬場ゲーセン ミカド様のご協力の元
新基板“exA-Arcadia”にてアーケード版を開発中
iOS/AndroidがUnity 5.4系
アーケード版がUnity 5.6系
MadeWithUnityブースにてUnite特別版を展示中
Q.よくあるSTGじゃん

何で講演してんの?
A.皆大好き最適化事例
「Unityだから遅い」はもう古い
• 「Unityの使い方によって生まれる負荷」を削減、あるいは回避する
• 時にはUnityEngineのC#ラッパーと設計思想が合わないこともある
• C/C++時代の設計はエンジン時代でも十全に活かせる
• Particle Systemは(メインスレッドに対して)決して高価でない
• uGUIは処理速度と開発速度のトレードオフ(ようするに高価)
• ProfilerとFrameDebuggerとMemoryProfilerは三種の神器

- ベンダーのProfilerは最終工程
「最適化」とは?



What about “Optimize”, your product required?
何に対して「最適」なのか?
• ハードウェア(CPU/GPU)
→ AlphaTest、Vertex Color、Texture Format…
• プラットフォーム
→バッテリー、アプリサイズ、確保メモリ、帯域制限…
• ゲームデザイン
→可変/固定フレーム、処理落ち・スパイク、視認性、3D酔い…
• プロジェクト
→処理速度、並列処理、 可読・再利用性…
“ ”
最速⊆最適



Fastest might be Optimization, but is not Best.
“ ”
でも速さはパワーだ



May the Performance be with you.
最適化はいつやるべきか?



We ought to optimize at the end, is that correct ?
最適化を行うタイミングは開発の最初と最後
• プロダクトにおいて何が重要(ウリ)なのかは最初期で決まっている

- 予想出来る問題は設計段階から対策
• スタートアップメンバーは信頼された中堅~ベテランで構成される

- 他の人とのやり取りが少ないので集中しやすい
  設計する時はね、誰にも邪魔されず、自由でなんというか救われてなきゃぁダメなんだ…
• プロジェクトの最後で大きな変更は不具合のリスクが高い
• プロジェクト末期はアサインメンバーが多い

- 人的コストが高い

- 直接売上にならないので工数くれない

- デスマーチの召喚(成果の是非に関わらず給与は増えない)
アカとブルーで予想された対応項目(今回の解説内容)
① 物量によるCPU負荷改善

- Scriptingのオーバーヘッド削減

- Static Dataによる演算省略
② 物理演算やめたい

- 自前コリジョンの設計
③ 物量によるGPU負荷改善

- ParticleSystemのバッチング

- P-MAPによるDraw Pass削減
④ スパイク対策

- GC/シェーダーコンパイル/テクスチャアップロードの対策
①物量によるCPU負荷

-Scripting編-
Scriptingのポイント
• MonoBehaviourのStart、Updateといった関数は一切使用しない

- EntryPointコンポーネント唯一つだけ実装(OnRenderImage等はその限りでない)
• MonoBehaviourである必要がないクラスはMonoBehaviourにしない

- MonoBehaviourである時点で処理コストは発生する(はず)

- GameObjectやTransform参照する必要がないなら不要
• classよりstructの方が参照速度が速い

- ただし値渡しのオーバーヘッドがかさむと逆に遅くなる
• Collection型の配列は固定配列より参照速度が遅い
• 多次元配列[,]はforeachが使えるがジャグ配列[][]より圧倒的に遅い
• 構造体のリストを扱うforeachは値渡しのオーバーヘッドが発生
オブジェクトプーリング
• LinkedList<T>を使わず古き良き双方向連結リストでのタスクシステム(一部抜粋)
• 初期化以外でのメモリアロケートを完全回避
internal sealed class FTask<T> {
public T item = default(T);
public FTask<T> prev, next;
public void Attach(); // 待機リストから稼働リストへの接続
public void Detach(); // 稼働リストから待機リストへの接続
}
// タスクへの処理はdelegateで外部定義
public delegate bool OrderHandler<T>(T obj); // タスクへ処理 true:継続, false:終了
public void Order(OrderHandler<T> order) {
FTask<T> now = null;
for (FTask<T> task = this.top; task != null) {
now = task; task = task.next;
if (!order(now.item))
this.Detach(now);
}
}
https://github.com/yugofujioka/ObjectPool
オブジェクト処理
• Transform等や定数をキャッシュして処理をインライン化していく
// プールから呼び出された時の処理
public void OnAwake() {
this.gameObject.SetActive(true);
}
// プールへ戻す時の処理
public void OnSleep() {
this.gameObject.SetActive(false);
}
// 稼働中の更新処理
public void OnRun(float elapsedTime) {
this.transform.localPosition += Vector3.forward;
}
参照のキャッシュ
private GameObject go_ = null;
private Transform trans_ = null;
// プールに生成された時の処理
public void OnCreate() {
this.go_ = this.gameObject;
this.trans_ = this.transform;
}
public void OnAwake() {
this.go_.SetActive(true);
}
public void OnSleep() {
this.go_.SetActive(false);
}
public void OnRun(float elapsedTime) {
this.trans_.localPosition += Vector3.forward;
}
定数のキャッシュ
private GameObject go_ = null;
private Transform trans_ = null;
// Vector3の定数は別途キャッシュすることでプロパティのオーバーヘッドを省略できるが…
private static readonly Vector3 V_FORWARD = Vector3.forward;
public void OnInit() {
this.go_ = this.gameObject;
this.trans_ = this.transform;
}
public void OnAwake() {
this.go_.SetActive(true);
}
public void OnRun(float elapsedTime) {
Vector3 position = this.trans_.localPosition;
position += V_FORWARD; // 今回のケースは意味がないが座標計算はローカルに取り出して計算後に戻す方がベター
this.trans_.localPosition = position;
}
処理のインライン化
private Vector3 direct = new Vector3(XXX, 0f, ZZZ);
private float speed = SSS;
public void OnRun(float elapsedTime) {
Vector3 position = this.trans_.localPosition;
float move = speed * elapsedTime;
position.x += this.direct.x * move;
position.z += this.direct.z * move;
this.trans_.localPosition = position;
}
• 今回のSTGで最もオブジェクト数が多い弾丸は今回XZ平面での移動

→Y軸の移動は存在しないので計算量を削減
Vector3の正規化処理はスコープが非常に深い
• とにかく値渡しをしまくられる
public Vector3 normalized { get { return Vector3.Normalize(this); } }
public static Vector3 Normalize(Vector3 value) {
float mag = Magnitude(value);
if (mag > kEpsilon)
return value / mag;
else
return zero;
}
public static float Magnitude(Vector3 vector) {
return Mathf.Sqrt(vector.x * vector.x + vector.y * vector.y + vector.z * vector.z);
}
.normalizedを扱う関数はボトルネックになりがち
public static float Angle(Vector3 from, Vector3 to) {
return Mathf.Acos(Mathf.Clamp(Vector3.Dot(from.normalized, to.normalized), -1F, 1F)) * Mathf.Rad2Deg;
}
public static float SignedAngle(Vector3 from, Vector3 to, Vector3 axis) {
Vector3 fromNorm = from.normalized, toNorm = to.normalized;
float unsignedAngle = Mathf.Acos(Mathf.Clamp(Vector3.Dot(fromNorm, toNorm), -1F, 1F)) * Mathf.Rad2Deg;
float sign = Mathf.Sign(Vector3.Dot(axis, Vector3.Cross(fromNorm, toNorm)));
return unsignedAngle * sign;
}
public static Vector3 ClampMagnitude(Vector3 vector, float maxLength) {
if (vector.sqrMagnitude > maxLength * maxLength)
return vector.normalized * maxLength;
return vector;
}
どうせC#での定義なんだから

自分で作ればいいじゃない
そして生まれる暗黒再車輪(一部抜粋)
// 正規化処理
public static float Normalize(ref Vector3 v) {
// Magnitude関数のインライン化
float mag = (float)Math.Sqrt(v.x * v.x + v.y * v.y + v.z * v.z);
v.x /= mag; v.y /= mag; v.z /= mag;
// 正規化と同時にスカラー値を返して同じ処理を繰り返さない
return mag;
}
// 正規化したベクトルを返す、インスタンスの値渡しは行わない
public static float Normalize(ref Vector3 v, out Vector3 vv) {
float mag = (float)Math.Sqrt(v.x * v.x + v.y * v.y + v.z * v.z);
vv.x = v.x / mag; vv.y = v.y / mag; vv.z = v.z / mag;
return mag;
}
// 弾丸用のY座標省略
public static float NormalizeXZ(ref Vector3 v) {
float mag = (float)Math.Sqrt(v.x * v.x + v.z * v.z);
v.x /= mag; v.z /= mag; v.y = 0f;
return mag;
}
そして生まれる暗黒再車輪(一部抜粋)
// 2つのベクトル間の角度を求める
public static float Angle(ref Vector3 a, ref Vector3 b) {
// 事前に正規化している前提で正規化処理を省略する
// 誤差の閾値はVector3定義に合わせる
Debug.AssertFormat(Mathf.Abs(1f - a.magnitude) <= Vector3.kEpsilon, "a正規化エラー");
Debug.AssertFormat(Mathf.Abs(1f - b.magnitude) <= Vector3.kEpsilon, "b正規化エラー");
// 内積計算をインライン化
float dot = a.x * b.x + a.y * b.y + a.z * b.z;
if (dot < -1)
dot = -1;
else if (dot > 1f)
dot = 1f;
return (float)System.Math.Acos(dot) * FMath.Rad2Deg;
}
Scripting については大体既出なのでよろしくどうぞ
• Unite LA 2016 - Tools, Tricks and Technologies for Reaching Stutter Free 60 FPS in INSIDE
- https://www.youtube.com/watch?v=mQ2KTRn4BMI
• Unite 2017 Tokyo - パフォーマンス向上のためのスクリプトのベストプラクティス

- https://www.youtube.com/watch?v=h_JVSLGRWUQ
• Unite 2016 Tokyo - ハードウェア性能を引き出して60fpsを実現するプログラミング・テクニック

- https://www.youtube.com/watch?v=VNVDtUT_4rs
• Unity道場 - パフォーマンス最適化のポイント

- https://speakerdeck.com/unitydojo/unitydao-chang-pahuomansuzui-shi-hua-false-pointo
• 10000 Update() calls – Unity Blog(日本語ページ有)

- https://blogs.unity3d.com/2015/12/23/1k-update-calls/
• Understanding optimization in Unity – Unity Manual(日本語ページ有)
- https://docs.unity3d.com/Manual/BestPracticeUnderstandingPerformanceInUnity.html
• UnityCsReference
- https://github.com/Unity-Technologies/UnityCsReference/branches
①物量によるCPU負荷

-Static Data編-
SerializedField編~移動パス~
• カメラパスは自作のベジェ曲線
• Sceneウィンドウ上で制御点設定
• イベントポイントも制御点に設定
- 移動の加減速
- 規定制御点まで任意秒での移動
- 画面の横スライド量
- FogのON/OFF
- SkyboxのON/OFF
- 特殊敵の発生(僚機も含む)
- ボスの発生
- etc...
設定データはSerializeでPrefab保存
[System.Serializable]
public struct Anchor {
public Vector3 point; // 通過点
public float power; // 制御点の長さ
public Quaternion rotate; // 制御点の向き
public float percent; // 始点からの近似距離
}
[SerializeField] private Anchor[] anchors = null; // 制御点
• 設定した制御点情報をシリアライズ
• 向きはGUI上ではEulerでの表示・設定

補完時にジンバルロックが起きるのでQuaternionで保存
ベジェ曲線の正規化(等速化)の事前準備
[SerializeField] private float[] samplingTime = null; // 進行率
[SerializeField] private Vector3[] samplingPosition = null; // 座標
[SerializeField] private Vector3[] samplingDirect = null; // 方向
[SerializeField] private Quaternion[] samplingRotation = null; // 回転
float delta = 1f / this.sampling; // 制御点間におけるサンプリング間の進行率(サンプリング数が高いほど滑らか)
Vector3 vs = sa.point;
for (int n = 0; n < this.sampling; ++n) {
int samplingIndex = anchorSamplingIndex + (n + 1);
Vector3 saForward = sa.forward;
Vector3 eaBack = ea.back;
Vector3 ve = Bezier(ref sa.point, ref saForward, ref eaBack, ref ea.point, delta * (n + 1));
distance += Vector3.Distance(vs, ve);
direct = (ve - vs).normalized;
this.samplingTime[samplingIndex] = distance;
this.samplingPosition[samplingIndex] = ve;
this.samplingDirect[samplingIndex] = direct;
vs = ve;
}
曲線計算を事前に行ってしまうことで計算量を減らす
曲線計算の省略
1. 与えられた進行度(0.0~1.0)を制御点(Anchor)で検索
2. 制御点間のサンプリング点で進行度検索
3. HITしたサンプリング点間で線形補完

※補完については線形での近似が結局一番ガタつかなかった

※座標はLerp、向きと回転はSlerpで補完
public Vector3 Bezier(Vector3 p, Vector3 a, Vector3 b, Vector3 q, float t) {
float tt = (1f - t);
float tt3 = tt * tt * tt;
float tt2 = tt * tt * t * 3f;
float tt1 = tt * t * t * 3f;
float tt0 = t * t * t;
return tt3 * p + tt2 * a + tt1 * b + tt0 * q;
}
ランタイム中の処理は検索走査と線形補完のコストのみ
検索コストも前回HITしたインデックスをキャッシュして高速化
敵の移動における任意曲線も同様の実装
• 地上敵及び空中敵の移動曲線も同様に静的に保持
• 敵の移動パスは短いのでサンプリング数は控え目
• こちら用に反転・回転・オフセット機能を実装
シリアライズ保持の利点
• ランタイム中の計算処理を大幅に削減

- 事前に計算できるものはしておくことでCPU負荷を下げる 

• UnityEditor内で完結

→エクスポート・インポート実装が不要

→デシリアライズ速度は気にならないレベル
課題
• 制御点とサンプリング数が増えるとUndo処理が死ぬほど遅い

- Undoのスタックを制御点のみに急きょ修正(要改善)

- サンプリングデータはScriptableObjectで外部ファイル化すべき
• 真上進行や宙返り等の移動が発生

- カメラの上方向が進行方向からは判別できない(e.g. 背面飛行が出来ない)

- 現在は宙返りフラグで対応
• Inspectorウィンドウは基本縦長なので見づらい
 →EditorWindow化
• カメラパス作るなら新機能のTimeLineの方が優秀では?
 →カメラパスはちょっとの誤差が画面のガタつきにつながってしまうので

  TimeLineが優秀ならそっちの方がいいかも
ScriptableObject編~会話イベント~
• 本作ではゲーム進行の状況に応じた会話が発生 
(e.g. 画面旋回時に「迂回する!」)
• カメラパスの進行度とスクリプトからイベント発生
【利点】
• ゲームプレイ中に調整可能
• データのインポートエクスポートを意識しない
• Deserializeの処理負荷は気にならない
【課題】
• ボイス数多くてEnumPopupのスクロールがだるい
SerializedField or ScriptableObject ?
• ちょっとした設定値はSerializedField

- public変数は極力避ける
• データテーブルはScriptableObject

- レベルデザイン、スタッフリスト等々

- エンディングでも採用
• 今回パスはSerializedField

- サンプリングデータは大きすぎて失敗
おまけ:固定の回転値は定数扱い
const int WAY_COUNT = 3; // WAY数
const float WAY_ANGLE = 60f; // WAY間隔(deg.)
static readonly Quaternion WAY_START_ROT = Quaternion.AngleAxis(-0.5f * (WAY_ANGLE * (WAY_COUNT - 1)), Vector3.back);
static readonly Quaternion WAY_ROT = Quaternion.AngleAxis(WAY_ANGLE, Vector3.back);
// nWAY射撃
private void WayShot(float passedTime) {
Vector3 dir = Vector3.forward;
dir = WAY_START_ROT * dir; // 初回は端の角度
for (int outer = 0; outer < WAY_COUNT; ++outer) {
// dir方向に弾丸の射撃
…
dir = WAY_ROT * dir; // WAY間の角度分ズラす
}
}
• Quaternion計算も一部事前計算してランタイム中の計算量を減らしている
②物理演算やめたい
Colliderの衝突判定は従来のSTGの感覚にはならない
• 今回はジャンルとして伝統的なゲームデザイン・プレイフィールを踏襲
• 物理演算挙動は当然メインスレッドに同期しないので人の目に映った現象と異なる

- シビアな回避を要求する場合「何故か被弾した」が増える

- 移動補完(すり抜け対策)はジャンル上あり得ない

-「今までの感じ」でなく「今っぽい感じ」(ざらめと白糖の違い)

- 古臭いメインスレッドでの自前接触判定が必要
• MwUで言ったColliderが初回アロケートは再現しませんでしたごめんなさい

- 速度を上げたい理由で自作判定するのはどのみちお勧めしない
自前コリジョンを用意する
• メインスレッドで操作/移動/判定という旧来フローを再現
• 精度と速度のトレードオフ

→シンプルな判定形状(円形・矩形)かつ平面のみの制限

→コリジョン判定のダブルバッファリング(負荷が半減)
• ダブルバッファリングによって判定をすり抜けるフレームが発生

→従来固有のクセである”嘘避け”の再現

→ゲームデザインを損なわずに偶発するプレイヤーの利益

→処理負荷が下げれてプレイヤーも嬉しい、Win-Win
• 当たり判定に関しては精度や処理速度としての最適化より

アーケードスタイルのSTGというゲームデザインとしての最適化を重要視
自前コリジョンの判定①
• 判定形状は円系と矩形の2種類のみ
• 矩形同士の判定

- 線分交差、判定数が最大4x4=16回

- 矩形は最大長の円を準備し、距離判定で削減
• 円形と矩形の判定

- 矩形を中心に傾きをつぶした状態でw/h判定
最長範囲に入った場合に
線分交差判定
自前コリジョンの判定②
複雑な形状は複数個で表現
複数個の塊を「部位」として管理
ザコ敵は1部位のみ
ボス・中ボスは複数部位を持つ
コリジョンはプール制
1. 設定値を事前に部位コンポーネントに設定
2. ランタイムでプールから取得
3. コリジョンの座標をコンポーネントから更新
4. 判定処理はプールがバッファ同士を走査

※タスクシステムをここでも採用
5. 部位の停止時に返却
自前コリジョンの判定③
Enemy Part
Type : Circle
Radius : 100pix
position
1. 2.3.
Collision Manager
Enemy
Player
Bullet
4.
5.
• 弾丸用のプールはダブルバッファ
• 奇数F・偶数Fに関わらず、

リクエスト毎に取得するバッファをスイッチ

(同一フレームで大量に呼ばれた際の処理分散)
• 全体として見ると60fpsでの判定
• 弾丸1発あたりの判定は30fps
Collision Manager
…
Enemy Bullet
奇数F判定

バッファ
偶数F判定

バッファ
自前コリジョンの判定④
Enemy Bullet
Type : Circle
Radius : 10pix
position
Enemy Bullet
Type : Circle
Radius : 10pix
position
③物量によるGPU負荷

-ParticleSystem編-
ParticleSystemのバッチング(同一マテリアル)
• Componentが分かれていてもMaterialが同一であればバッチングされる
• SortingLayerで描画順を固定することでバッチングを促せる
ParticleSystemのバッチング(Texture Sheet)
• 同一シェーダーで描画するTexture Sheet AnimationのParticleは

同一テクスチャにまとめることでバッチング可能
• 行毎にアニメーションパターンを指定して行番号で変更する
• Unity 2017以降はSpriteが指定できるのでどちらが速いかは要検証
ParticleSystemのバッチング(同一モジュール)
• 単発でBurstするだけのものはSimulation SpaceをWorldにして

モジュールを使いまわすことで1つのParticleSystemだけ生成すれば良くなる
• 制約は多いので使える場所は限定的だがメモリ消費が減る

- 処理速度も速く…なっているといいなぁ…
③物量によるGPU負荷

-P-MAP編-
加算と半透明のバッチング対応
• 爆発等、加算描画は多用されるのでDraw Passが跳ね上がりがち…
• CEDEC2015でナツメアタリ株式会社、宮部様が講演された

「加算合成コストが0になる!?すぐに使えるP-MAPブレンドテクニック」

のP-MAPテクスチャを採用
• 半透明と加算描画を同一の事前乗算シェーダーで行える→バッチング可能

負荷の関係上、本来出来ない描画順序の表現が可能
https://www.slideshare.net/toshiyasumiyabe/cedec2015pmap
P-MAP例:爆発(ParticleSystem)
• [煙(半透明)<炎(加算)<発光(加算)<火花(加算)]<[…]
P-MAP例:アイテム(SpriteRenderer)
• [発光(加算)<メダル(半透明)]<[発光(加算)<メダル(半透明)]<[…]
P-MAP例:弾丸オーラ(事前合成)
• 扱える状況はほとんどないがフィルレートが抑えられるので最速
P-MAP用のシェーダー
• Particles/Alpha Blended Premultiplyシェーダーで試せる
• 余計なShader Variantがついてくるので正式採用するなら自作した方がいい
• 下記の定義以外はただのTransparentシェーダー
Blend One OneMinusSrcAlpha
fixed4 frag (v2f i) : SV_Target {
return i.color * tex2D(_MainTex, i.uv) * i.color.a;
}
デメリット
• ファイル管理が大変

- 元のテクスチャとPMAPテクスチャと2重に管理する必要がある

- 元データを更新・修正した際に再度P-MAP化しなければならない

- 頻繁に採用する際は開発フローを要検討
④スパイク対策
スパイクとは?
• 瞬間的に高負荷な処理が発生し、一瞬~最悪1秒ぐらい画面が止まってしまう現象
• 主な原因は3つ

- ガベージコレクション

- シェーダーコンパイル

- テクスチャアップロード
• モバイルの場合はOS側のスパイクも予想される

- これについてはOSバージョン、端末、ユーザー設定に依存するので諦め
• その他ありがちな要因は別スレッドに分けるべき処理を分けない

- 送受信データのメインスレッドでの圧縮・展開
スパイクによって起きる問題と対策
• ワンミスが大きいゲームでは強制的にプレイヤーにミスを与えてしまう

- 例)音ゲーでノーツが押せなかった

- 例)ジャストガードが失敗した

- 例)STGで回避操作が出来なかった

→クソゲー
• インゲーム中のメモリアロケートを極限まで廃止
• 起動時にインゲームで使用している全ShaderVariantをプリロード
• インゲーム開始前に使用するテクスチャの全使用
④スパイク対策

-GC編-
GCを抑える為に…
• GCしたくないシーンでのメモリアロケートを避ける

- 初期化時にキャッシュ、プールする
• Unity Profiler(Deep Profile)で「GC Alloc」を確認

- EditorでのProfileはあまり信用しない(ザクっと見る分にはいい)

- モバイルであってもStandaloneでの動作対応をしておくと作業が速い
• 理屈は簡単、でもちょっと手抜きしただけで全部が無駄になりがち
• 今回はよくあるメモリアロケートをご紹介
ありがちなメモリアロケート~Debug.Log~
• Debug.Logは上書きしてアトリビュートで潰す
• PlayerSettingsでログ出力は消えるがAPIのコールは消えない

→引数の文字列結合は発生
#if !UNITY_EDITOR
public static class Debug {
[System.Diagnostics.Conditional(“DEBUG")]
public static void Log(object message) { UnityEngine.Debug.Log(message); }
… // LogWarningやLogError、LogAssertも同様
}
#endif
Int hoge = 0;
Debug.Log(”TEST LOG : ” + hoge);
ありがちなメモリアロケート~string~
• 空文字(“”)はstring.Emptyで扱う
• TagやLayerは事前に定義クラスを自動生成する

- 名前(文字列)からID検索は避ける(タイポ対策でもある)
public static class LAYERLIST {
public const int DEFAULT = 0;
public const int TRANSPARENTFX = 1;
public const int IGNORE_RAYCAST = 2;
public const int WATER = 4;
public const int UI = 5;
}
void Start() {
//int layer = LayerMask.NameToLayer(“Default”);
gameObject.layer = LAYERLIST.DEFAULT;
}
public static class SORTINGLAYERLIST {
public const int DEFAULT = 0;
public const int ADDITIVE = 1;
}
…
void Start() {
var sp = GetComponent< SpriteRenderer>();
//int id = SortingLayer.GetLayerValueFromName("Additive");
sp. sortingLayerID = SORTINGLAYERLIST.ADDITIVE;
}
ありがちなメモリアロケート~string~
• TagとLayerの一覧取得
public static string[] GetTagNames() {
string[] names = InternalEditorUtility.tags;
int length = names.Length;
for (int i = 0; i < length; ++i) names[i] = names[i].Replace(" ", "_");
return names;
}
public static int[] GetLayerIDs() {
string[] names = InternalEditorUtility.layers;
int length = names.Length;
int[] ids = new int[length];
for (int i = 0; i < length; ++i) ids[i] = LayerMask.NameToLayer(names[i]);
return ids;
}
ありがちなメモリアロケート~string~
• SortingLayerも同様に事前に定義クラスを生成

- 何故かTag/Layerのようにすんなりいかないので無理矢理取得
public static string[] GetSortingLayerNames() {
Type utilType = typeof(InternalEditorUtility);
PropertyInfo prop = utilType.GetProperty("sortingLayerNames", BindingFlags.Static | BindingFlags.NonPublic);
return (string[])prop.GetValue(null, new object[0]);
}
public static int[] GetSortingLayerUniqueIDs() {
Type utilType = typeof(InternalEditorUtility);
PropertyInfo prop = utilType.GetProperty("sortingLayerUniqueIDs", BindingFlags.Static | BindingFlags.NonPublic);
return (int[])prop.GetValue(null, new object[0]);
}
ありがちなメモリアロケート~string~
• MaterialPropertyはランタイム時に初期化
• アプリケーション起動直後(Unity初期化中)は取得できない
int SHADER_PROPERTY_COLOR = 0;
MaterialPropertyBlock mpb = null;
MeshRenderer meshRender = null;
void Start() {
SHADER_PROPERTY_COLOR = Shader.PropertyToID("_Color");
mpb = new MaterialPropertyBlock();
meshRender = this.GetComponent<MeshRenderer>();
}
void Update() {
mpb.SetColor(SHADER_PROPERTY_COLOR, Color.white);
meshRender.SetPropertyBlock(this.materialProperty);
}
ありがちなメモリアロケート~delegate~
• 関数ポインタ的な実装をすると暗黙でdelegateのインスタンスを生成
• static関数で回避可能(※ただし5.6以降、5.5はforeachを考えると大丈夫かも?)
public class HogeClass {
delegate void Callback(int val, HogeClass hoge);
int hogeInt = 0, result = 0;
void Function(Callback call) {
hogeInt++;
call(hogeInt, this);
}
void MemberFunction(int val, HogeClass hoge) { hoge.result = val; }
static void StaticFunction(int val, HogeClass hoge) { hoge.result = val; }
void Start() {
Function(MemberFunction); // ①NG
Function(StaticFunction); // ②OK(※5.6/2017の最新版で確認)
Function((v, h) => MemberFunction(v, h)); // ③NG(※確認用に無駄なラッパー)
Function((v, h) => StaticFunction(v, h)); // ④OK(※確認用に無駄なラッパー)
Function((v, h) => this.result = v); // ⑤NG
Function((v, h) => h.result = v); // ⑥OK
}
}
余談
ParticleSystemのトリビア
• Unity5.3系はParticleSystemの引数のwithChildrenがtrueの場合

自分の子ノードをforeachで検索

→Play/Stop/Pause/IsAliveの引数にfalseをつけないとメモリアロケート

→5.4~5.6系はこっそりfor文に修正されている
• 2017系はさらにinternalコードに吸収

- よくわかんない(中身見れない)けど2倍以上速くなってる

- trueでも子ノードが存在しない場合の差は誤差レベル
• どのver.にせよfalse指定した方が(検索を省略する分)速い

- 子ノードがぶら下がっている状態で子検索すると如実に差が出る

- 引数省略はラッパーのオーバーヘッドが余計にかかる
5系のParticleSystemはオーバーヘッドが高い
• 5系のPlay/Stop/IsAliveとかは関数のスコープが結構深い(3つ関数をもぐる)
• 今作ではParticleSystemを乱用しているのでちょっと気になった
• そうだ…Internal関数を直接叩けば…
public delegate bool ParticleCallback(ParticleSystem self);
public static ParticleCallback ParticlePlay = null;
var playMethod = typeof(ParticleSystem).GetMethod("Internal_Play", System.Reflection.BindingFlags.Static |
System.Reflection.BindingFlags.NonPublic, null, new System.Type[] { typeof(ParticleSystem) }, null);
ParticlePlay = System.Delegate.CreateDelegate(typeof(ParticleCallback), playMethod) as ParticleCallback;
…
ParticleSystem particle = this.GetComponent<ParticleSystem>();
ParticlePlay(particle);
ReflectionでUnityEngineを参照する場合の注意
• リンカーから参照されていないと認識されてバイナリから除外されてしまう
• InternalCodeを勝手に削り落とされないようlinkファイルを指定
link.xml
<linker>
<assembly fullname="UnityEngine">
<type fullname="UnityEngine.GeometryUtility" preserve="all"/>
<type fullname="UnityEngine.ParticleSystem" preserve="all"/>
</assembly>
</linker>
これでParticleSystemのAPIは当社比3倍速!
2017系以降使えない&速度差も誤差レベル



5系時代のバッドノウハウです
閑話休題
④スパイク対策

-Shader Compile編-
Shaderのコンパイルによるスパイク
• ランタイム中に初めて描画されるタイミングでシェーダーがコンパイルされる

- Standaloneは気にしなくていいかも?
• モバイルでは顕著
• Fogやライトマップの有無でVariantが随時切り替わるので

スパイクが走ると

「あ、裏読みしたな」とか「Unityっぽい動きする」

みたいに思われてとてもダサい
Shader Variant Collection
• Graphics Settingsにプリロード設定があるのでShaderVariantCollectionを作成して設定
• コアシーンのシェーダーを登録しておく
• Shader.WarmupAllShadersはロード済(準備中)のシェーダーのみ対象

- 見えないポリゴンを描画してコンパイルを呼ぶらしい(公式リファレンス談)

- これを呼べば存在するシェーダーを全部コンパイルするぜ!ってAPIではなかった
Variantによるシェーダー切り替え
• プリセットのシンボルを使って状況に応じたVariantに切り替えられる

e.g.) 落ち影の有無でSHADOWS_SCREENが有り/無しのShaderへ
• .shadervariantsへの設定は直編集した方が早い
④スパイク対策

-Texture Upload編-
TextureのGPUアップロード
• Shader同様初めて表示される際にグラフィックメモリに展開される

- モバイルは1024の圧縮テクスチャ(2MB)ぐらいからバレがち

- 事前にカメラ内に映さないと行われない
• QualitySettingsのAsync Upload Buffer Sizeは事前にテクスチャの最大サイズへ

- 2048x2048の圧縮なら8MB程度、未圧縮なら16MB

- 設定より大きいテクスチャ読込時はバッファの再割り当てが起きる
• QualitySettingsのAsync Uplode Time Sliceはお好みで
Textureを事前に画面に出す(Sprite Atlas)
• 今回SpriteAtlasは2048x2048が数枚存在

- 常駐物は起動シーンで先に画面内表示

- 白い矩形(8x8)のSpriteを用意してこっそり仕込んでおく

- 1024x1024の大きいParticleSystemテクスチャもSpriteRendererで準備
• - 1フレーム描画したらGameObjectをSetActive(false) ※Destroyしない
Textureを事前に画面に出す(Stage)
• 各ステージのシーンの初期化時に1Fだけステージ全体を映すようカメラを移動
• 画面ワイプの裏側でシーン内のテクスチャを全て使用済みにする
• ボス(3D)もプリロード用に配置しておく(実際のオブジェクトはプールから)
結論

「テクスチャのスパイク対策は力技」
おまけ:Meshも事前にGPUに準備できる(ようだ)
• Mesh.UploadMeshData(bool markNoLogerReadable)

- メインメモリからグラフィックメモリに渡す(らしい)

- OpenGLESでいうVBOの作成に相当(するはず)

- バッファオブジェクトの生成と考えれば一度だけ呼ぶ(と信じてる)

- 引数はメッシュを書き換えない場合はtrue一択(のはず)
• Mesh.MarkDynamic()

- ポリライン等、メッシュをCPU側から書き換える場合に呼ぶ(多分)

- OpenGLESでいうVBOにDYNAMIC設定に相当(すると思う)

- UploadMeshDataより先に呼ぶ(といい気がする)
• 全然わからない、俺は雰囲気でMeshを制御している

- 問題なく動いてるからいいんじゃないかな?
• 大きなメッシュが画面内に初めて映る際に微妙なスパイクが走ってしまう場合は

上記のAPIをいじってみるとよいかもしれません
まとめ
① すぐCPU負荷が上がるんだけど…

- 過去のUniteのスライドを漁る(いっぱいある!)

- リアルタイム計算する必要のないデータは事前計算してシリアライズがオススメ(Unityっぽい)
② 接触判定だけに物理演算は重いんじゃない?

- アーケードライクという商品のコンセプトから自作

- 今風のゲームならColliderで大丈夫、Unityを信じろ
③ 描画物多いんだけど…

- ParticleSystemは優秀

- 半透明と加算による描画パスの増加には事前乗算(P-MAP)というアプローチもある
④ たまに画面が一瞬止まるんだけど…

- 原因はGC/Shader Compile/Texture Uploadの3つが主

- 妥協は重要
Thank you!
ご静聴ありがとうございました!

【Unite 2018 Tokyo】60fpsのその先へ!スマホの物量限界に挑んだSTG「アカとブルー」の開発設計