インディーゲーム開発の道のり:Flying Gorillaが400万ダウンロードに至るまで
こんにちは、Flying Gorilla StudiosのKazutaka Tottoriです。今日は、私たちの代表作『Flying Goril...
こんにちは、Flying Gorilla StudiosのテクニカルリードのChen Weiです。今回は、『Flying Gorilla』の開発で実際に使用した最適化テクニックを共有したいと思います。400万ダウンロードを支える技術の裏側をお見せします!
モバイルゲーム開発において、最適化は単なる「あったらいいもの」ではありません。必須です。なぜなら:
// 悪い例:個別のスプライトを読み込む
Sprite banana = Resources.Load<Sprite>("Sprites/banana");
Sprite coin = Resources.Load<Sprite>("Sprites/coin");
Sprite powerup = Resources.Load<Sprite>("Sprites/powerup");
// 良い例:アトラスから一括で読み込む
Sprite[] gameSprites = Resources.LoadAll<Sprite>("Sprites/GameAtlas");
Dictionary<string, Sprite> spriteDict = new Dictionary<string, Sprite>();
foreach(var sprite in gameSprites) {
spriteDict.Add(sprite.name, sprite);
}
結果: ドローコールが120から15に削減!
Flying Gorillaでは、バナナや障害物が頻繁に生成・破壊されます。これを最適化するためにオブジェクトプーリングを実装しました。
public class ObjectPool<T> where T : MonoBehaviour {
private Queue<T> pool = new Queue<T>();
private T prefab;
private Transform parent;
public ObjectPool(T prefab, int initialSize, Transform parent) {
this.prefab = prefab;
this.parent = parent;
for(int i = 0; i < initialSize; i++) {
T obj = GameObject.Instantiate(prefab, parent);
obj.gameObject.SetActive(false);
pool.Enqueue(obj);
}
}
public T Get() {
T obj = pool.Count > 0 ? pool.Dequeue() : GameObject.Instantiate(prefab, parent);
obj.gameObject.SetActive(true);
return obj;
}
public void Return(T obj) {
obj.gameObject.SetActive(false);
pool.Enqueue(obj);
}
}
結果: GCスパイクが90%削減、フレームレートが安定
遠くのオブジェクトは簡易モデルに切り替えます。
public class SimpleLOD : MonoBehaviour {
public Mesh highDetailMesh;
public Mesh lowDetailMesh;
public float switchDistance = 50f;
private MeshFilter meshFilter;
private Transform player;
void Start() {
meshFilter = GetComponent<MeshFilter>();
player = GameObject.FindWithTag("Player").transform;
}
void Update() {
float distance = Vector3.Distance(transform.position, player.position);
meshFilter.mesh = distance > switchDistance ? lowDetailMesh : highDetailMesh;
}
}
Unityのデフォルト機能ですが、適切に設定することが重要です。
// カメラの設定
Camera.main.farClipPlane = 100f; // 必要以上に遠くを描画しない
静的なオブジェクトにはOcclusion Cullingを設定します。
モバイル向けの軽量シェーダーを作成しました。
Shader "Mobile/SimpleDiffuse" {
Properties {
_MainTex ("Texture", 2D) = "white" {}
_Color ("Color", Color) = (1,1,1,1)
}
SubShader {
Tags { "RenderType"="Opaque" }
LOD 100
Pass {
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile_fog
#include "UnityCG.cginc"
struct appdata {
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f {
float2 uv : TEXCOORD0;
UNITY_FOG_COORDS(1)
float4 vertex : SV_POSITION;
};
sampler2D _MainTex;
float4 _MainTex_ST;
fixed4 _Color;
v2f vert (appdata v) {
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
UNITY_TRANSFER_FOG(o,o.vertex);
return o;
}
fixed4 frag (v2f i) : SV_Target {
fixed4 col = tex2D(_MainTex, i.uv) * _Color;
UNITY_APPLY_FOG(i.fogCoord, col);
return col;
}
ENDCG
}
}
}
// iOS向け
textureImporter.textureCompression = TextureImporterCompression.CompressedHQ;
textureImporter.format = TextureImporterFormat.PVRTC_RGB4;
// Android向け
textureImporter.format = TextureImporterFormat.ETC2_RGB4;
audioImporter.defaultSampleSettings = new AudioImporterSampleSettings {
loadType = AudioClipLoadType.CompressedInMemory,
compressionFormat = AudioCompressionFormat.MP3,
quality = 0.7f
};
静的なオブジェクトには必ずStatic flagを設定:
GameObject.isStatic = true;
// 頻繁に更新されるUI(スコアなど)
Canvas dynamicCanvas;
// 静的なUI(ボタンなど)
Canvas staticCanvas;
// クリックされないUIエレメントは無効に
image.raycastTarget = false;
text.raycastTarget = false;
// 不要な時はAnimatorを無効化
if (!isVisible) {
animator.enabled = false;
}
複雑なステートマシンが不要な場合:
public class SimpleRotate : MonoBehaviour {
public float speed = 30f;
void Update() {
transform.Rotate(Vector3.up * speed * Time.deltaTime);
}
}
public class PerformanceMonitor : MonoBehaviour {
private float deltaTime = 0.0f;
void Update() {
deltaTime += (Time.unscaledDeltaTime - deltaTime) * 0.1f;
}
void OnGUI() {
int fps = Mathf.Ceil(1.0f / deltaTime);
GUI.Label(new Rect(10, 10, 100, 20), "FPS: " + fps);
}
}
string memoryInfo = string.Format(
"Total: {0:F2} MB, Used: {1:F2} MB",
SystemInfo.systemMemorySize / 1024f,
GC.GetTotalMemory(false) / 1048576f
);
これらの最適化により、Flying Gorillaは:
if (SystemInfo.systemMemorySize < 2048) {
QualitySettings.SetQualityLevel(0); // Low Quality
Application.targetFrameRate = 30;
}
if (SystemInfo.graphicsMemorySize > 2048) {
QualitySettings.SetQualityLevel(2); // High Quality
// パーティクルエフェクト有効化
particleSystem.gameObject.SetActive(true);
}
モバイルゲームの最適化は、継続的なプロセスです。一度やって終わりではなく、新機能を追加するたびに見直す必要があります。
重要なのは:
これらのテクニックが、あなたのゲーム開発に役立つことを願っています。質問があれば、ぜひコメントやDiscordでお聞かせください!
Happy Optimizing! 🚀
Chen Wei
Technical Lead, Flying Gorilla Studios
ご意見、ご感想、お仕事のご依頼など、お気軽にお問い合わせください。