インディー開発者向けの開発事例です。Android版はベータリリース。iPhoneはEasyMobile対応待ち、Windowsは冬コミ向けです
開発環境
- unity2019.3.0f1(開発自体はb3から
- github private repository
- sourcetree
- dropbox
- jenkins
バージョン管理について
昔はさくらvpsにsubversionいれてましたが、githubの方が便利なので移行しました。
ただしファイルサイズ100mb制限があるのでそこだけ神経質になってます。alembicなどは簡単に大きくなるので尺を制限したり、30fpsにするなどで小さくします
dropboxはpsdやblendなどのグラフィック系の中間ファイルの簡易バージョン管理として使ってます。githubだとすぐにファイル制限にひっかかりますが、dropboxならその心配はありません。たまに上書きしてファイル台無しになったりしたので、バージョン管理しておいた方が安全です。
ブランチについて
develop一本にどんどんコミットしています。大きい破壊的仕様変更があった時は一時的にブランチ分けました。個人開発なので変にブランチ分けた運用はしないです。
グラッフィク仕様
モバイル版とデスクトップ版で一部のグラフィックリソースに違いがあります。
モバイル版
キャラクター 15000△までに抑える
敵 10000△までに抑える、少なければ少ないほどよい
- テクスチャは一律1k以下。
- ShaderはSimpleLitが基準
デスクトップ版
- 頂点数に制約無し
- LODは今のところ無し、負荷が厳しいようであればモバイル版をLowモデルとして使う
- テクスチャは2k以下
- ShaderはキャラのみリッチなLitベース
フォルダ構成
DropBox管理
Designer Planner
出力したpngやfbxやluaやjsonはrubyスクリプトでunity projectのAssetsに変更があった場合のみコピーします。
Git管理
Addressable Game MKFW(便利クラスまとめた自作フレームワーク 無数の外部アセット
失敗
外部アセットは自由に散らばせていたのですが無駄にHierarchyが長くなるので、せめてExternalAssets直下にまとめるなどしておけばよかったです。特にグラフィックやサウンド系はほぼ更新することはないので好き勝てに移動させてよいと思います。
Asset Import自動設定
Editor拡張でwindows, android, iphoneを設定するようにしmasita
。
windows開発機でエラー
しかしwindows開発機でiphoneの設定をしようとしたら、iphoneモジュールインストールしていないためエラーになった。そこでiphone設定部分だけ#if UNITY_IPHONEで囲いました。
全体設定の危険性
フォルダ指定ではなくプロジェクト全体で共通のテクスチャ設定にしていました。しかしそうするとPackageにあるtextureまで勝手に設定しました。必ず対象フォルダを設定しておくことをおすすめします(どうしても全体にするならせめてAssets文字が含まれているか判定しましょう
ローカライズ対応
unityにはpreviewですが一応ローカライズ機能あります。さすがにpreviewすぎるので使いませんでした。
自作のローカライズ系を使いました。
今回は、動的にフォントを切り替える方式を採用しました。各言語ごとにTextMeshProフォントアセット作りました。そして任意のタイミングでフォント(言語)を切り替えれるようにしました。しかし、この運用かなりめんどくさくて後悔してます。タイトルシーン以外での言語切り替え禁止方式にすればよかったです。
また言語ごとにTextmeshProフォントアセット作って都度読み込むのもめんどくさいです。全言語分ひとまとめにしてしまえばよかったです。googleが出してるNotoSansCJKであれば日本語、簡体字、繫体字、英語をサポートできるし、TextmeshProのfallbackを使えば追加で足せます。もちろんTMProの動的に文字追加するモードでの運用もアリです。
動的フォント切り替え方式のメリットとしてスクショ撮るときにフォントを一括で差し替えて全言語分のスクショを一度にとることができそう…なんですが、滅多に出番がないです。このためだけに動的フォント切り替えの仕組みでコード書くのはつらすぎます。
URP(Universal Rendering Pipeline
キャラクターだけSimpleLitにリムライトを追加した自作シェーダーです。それ以外は全てSimpleLitです。
最も基本的なメインカメラ一個、UIはUGUIのCamera overrideです
パフォーマンスを最適化するならUIはSpriteRendererベースがよいのですが、URPはカメラスタック未対応なので諦めました。
SRP Batcherを活かす
自作シェーダーもSRP Compatibleにしました。
今作は1マテリアル(1texture)、多数メッシュという背景づくりをしたため、かなり高速に描画できたんじゃないかなと思います。
キャラクターもたくさん表示されるのですが、SRP BatchがSkinningRendererにも対応したのでNPCの描画が効果的におこなわれています(学生が多くみんな制服という仕様も活きてます)。そのため当初予定していたMeshBakerによるNPCのメッシュ結合で1set pass目指すはやめました。
背景も考慮するとこのゲームはローポリゴンが超多いのですがBatchが効きまくるグラフィックリソースなのでさほど問題になりませんでした。
個人開発であまり最適化に時間とられるのもキツイので労せずパフォーマンス出る仕様が一番効きます
GPU Instancingは未使用
SRP Batchとの相性が悪いとのことでOFFにしました。Unity Blogにも書いてある通り、自動GPU instancing対応中とのことなのでそのうち自然にパフォーマンス上がりそうです。
Static batching, dynamic batching OFF
ロード時間が劇的に増えたり、SRP Batchrと相性悪いので無効化しました。
AssetStore素材
今回使ったアセットはURP未対応のアセット素材でした。
Edit>RenderPipeline>UniversalRenderingPipeline>Upgrade Selected Material to URP Material
これで変換することでURPで使えるようになりました。
しかし対応しているのがStandardshader→Litくらいでした。パーティクルは未対応だったので手動で変換しました
Addressable
エクスプローラー上はAddressable管理のリソースを全てAddressableというフォルダにまとめました。その中に色々なサブフォルダある感じです。
Residentは常駐用の一個の巨大なassetbundleです。フォルダを一個登録してあります。フォルダに新しいファイル追加しても自動的にaddressableに含まれるので楽です。
自動命名
今回はファイルたくさんではないのでAddressable Pathの自動設定系は使いませんでした。しかし衣装が増えてきそうなので自動登録できるようにしたくなってきました。
Shaderの重複
Unityのハマりどころです。今回URPのシェーダーはPackageの中にあります。PackageはAddressableの管理外のため何もしないと全てのAssetBundleにlit.shaderなどが含まれてしまいます。
SBPのCreateBuiltInShaderBundleを使う
ありがたいことにプロ技で解説ありましたね
//DefaultBuildTasks.Create(DefaultBuildTasks.Preset.AssetBundleCompatible);
var taskList = DefaultBuildTasks.Create(DefaultBuildTasks.Preset.AssetBundleBuiltInShaderExtraction);
こうするだけですね
間違いました。URP.Lit.shaderはbuild-in shaderではありません
そのため上記BuildInShaderのタスクは役に立ちません。ちなみに2019.4 VerifiedのAddressableであれば標準でBuildInShader.bundleを作っています。SBPの改造は必要ありません。
ShaderをAssetsに持ってくる
最も単純な対応です。今回はこっちにしました。(というよりSBPで用意されてるのに昨日まで気づきませんでした、後で変える予定です
ShaderVariantCollectionは使いませんでした
ShaderVariantCollectionは動的に発生するShader Compileによるカクツキを防止する手段です。今回はシェーダーの数が少なくカクツキも気にならないので使いませんでした。
無策にProject SettingsのGraphicsタブのSave to assetを使うと全shader varinatが参照されてしまい。巨大なアセットバンドルが生成されます。(僕の場合は20MB増えました。メモリに丸々のります。真面目にやるなら細かくShaderVarinatCollectionを分割します。
不要なリソースの削除
モバイルではリッチなデスクトップ版のリソースは必要ありません。デスクトップ版では体験版を作る必要があり要らないリソースがあります。またリリースではSRDebuggerは入れたくないです。今回は二種類の方法を組み合わせて対応しました。
ビルド前にファイルを直接削除
リリースビルドではruby scriptでSRDebuggerのフォルダを直接削除してます。これはjenkinsが行っています
SRPを改造しアセットを除く
pipelineの最初期ほど難易度が低いです。最初はアセットバンドルをStreamingAssetsにコピーするところではじこうかなと調べていたのですが、不可能っぽいので諦めました
ContentPipeline.BuildAssetBundlesの箇所で使用アセットを除くことでAssetBundleの作成自体をされないようにしました。
public static ReturnCode BuildAssetBundles(IBundleBuildParameters parameters, IBundleBuildContent content, out IBundleBuildResults result, IList<IBuildTask> taskList, params IContextObject[] contextObjects)
{
// Avoid throwing exceptions in here as we don't want them bubbling up to calling user code
if (parameters == null)
{
result = null;
BuildLogger.LogException(new ArgumentNullException("parameters"));
return ReturnCode.Exception;
}
// Avoid throwing exceptions in here as we don't want them bubbling up to calling user code
if (taskList.IsNullOrEmpty())
{
result = null;
BuildLogger.LogException(new ArgumentException("Argument cannot be null or empty.", "taskList"));
return ReturnCode.Exception;
}
// Don't run if there are unsaved changes
if (ValidationMethods.HasDirtyScenes())
{
result = null;
return ReturnCode.UnsavedChanges;
}
// ここが改造部分、この中で要らないアセットを除く処理をする note本当はIBuildTaskを継承したTaskにすべき
var config = new AssetConfigulation();
config.Config(content);
AssetDatabase.SaveAssets();
ReturnCode exitCode;
result = new BundleBuildResults();
using UnityEngine;
using UnityEditor;
using System.Collections.Generic;
using System.IO;
using System.Threading;
using UnityEditor.Build.Pipeline.Interfaces;
using UnityEditor.Build.Pipeline.Utilities;
using UnityEditor.Build.Utilities;
using System.Text.RegularExpressions;
namespace UnityEditor.Build.Pipeline
{
public class AssetConfigulation
{
public static BuildTargetGroup BuildTargetGroup;
public static bool Trial;
public static bool R18;
private readonly List<Regex> _ignoreRules = new List<Regex>();
private readonly List<GUID> _validAssets = new List<GUID>();// 作業用のバッファ
private readonly HashSet<GUID> _removedAssets = new HashSet<GUID>();
public void Config(IBundleBuildContent content)
{
InitIgnoreRules();
ParseAssets(content);
ParseBundleLayouts(content);
}
private void InitIgnoreRules()
{
_ignoreRules.Clear();
var paths = new List<string>();
if(Trial)
{
paths.Add(Application.dataPath + "/../_AB_trial_ignore_contents.txt");
}
if( BuildTargetGroup == BuildTargetGroup.Android || BuildTargetGroup == BuildTargetGroup.iOS)
{
paths.Add(Application.dataPath + "/../_AB_mobile_ignore_contents.txt");
}
foreach(var path in paths)
{
var rules = File.ReadAllLines(path);
foreach(string rule in rules)
{
var r = new Regex(rule);
_ignoreRules.Add(r);
}
}
}
/// <summary>
/// IBundleBuildContent.Assetsを構成に基づき間引く
/// </summary>
/// <param name="content"></param>
private void ParseAssets(IBundleBuildContent content)
{
_validAssets.Clear();
_removedAssets.Clear();
var assets = content.Assets;
foreach (var guid in assets)
{
var assetPath = AssetDatabase.GUIDToAssetPath(guid.ToString());
if (IsValidAssetPath(assetPath))
{
_validAssets.Add(guid);
}
else
{
_removedAssets.Add(guid);
}
}
assets.Clear();
assets.AddRange(_validAssets);
}
private bool IsValidAssetPath(string assetPath)
{
foreach(var rule in _ignoreRules)
{
if (rule.IsMatch(assetPath))
{
return false;
}
}
return true;
}
/// <summary>
/// BundleLayoutから要らないguidを除く
/// </summary>
/// <param name="content"></param>
private void ParseBundleLayouts(IBundleBuildContent content)
{
var bundleLayout = content.BundleLayout;
foreach (var kv in bundleLayout)
{
_validAssets.Clear();
var guids = kv.Value;
foreach (var guid in guids)
{
if (!_removedAssets.Contains(guid))
{
_validAssets.Add(guid);
}
}
guids.Clear();
guids.AddRange(_validAssets);
}
}
}
}
除くファイルについては正規表現一覧をtxtファイルにまとめました。
しかしDefaultBuildTaskを見ればわかる通り、SBPの作法としてはIBuildTaskを継承してこの処理を実装するのがよいみたいです。少し失敗しました
InputManager
New Input manager?
検証しました。xbox360コントローラーとPS4コントローラーを何もしなくても扱えるというのが神でした。Rewiredを使ったことある人ならこのありがたみが分かると思います。
でも結局Legacy Input Manager?
previewでバグが怖かったのと、メインプラットフォームがモバイルでバーチャルパッド操作だったのが理由です。input managerのunity blog記事読んだときは基盤システムをもう固定してたので今更感もありました。
設計
ざっくりこんな感じです。最終的に”button5″とかの文字列をLegacyInputManagerに問い合わせます
実際はモバイルでのバーチャルパッドやタップやマウスクリック操作をも抽象化するため、間にもう1レイヤーあります。
そのためInputManagerの設定はシンプルです。だいたいこの状態から変わることはありません。
ゲーム初回起動時にキーコンフィグさせるという仕様は、他の同人ゲームサークルでやっていたので、それにならいました。
失敗
コード上ではPad.GetButtonDown(Button.A)のように使いました。
しかしこれは失敗でした。ボタンの割り当ては開発中に何度も変わります。その都度コードを修正するのは苦痛でした。既存の流儀に従いPad.GetButtonDown(“Attack”)みたいに具体的なボタンに依存しない実装が正しいです。
記事執筆中にXboxController LRトリガー同時押しバグを引いてしまい修正に苦労しました。
Xbox系コントローラーとPS4コントローラー両対応作業はかなり苦しいものでした。まだバグがあるかもしれません。正直ここまでキツイのであれば大人しくNew Input Managerに移行した方がよかったです。Xbox PS4コントローラーを何もせずに同等に扱えるだけでめっちゃ楽に開発できます。
ゲーム全体の設計について
UI系はクリーンアーキテクチャとReduxアーキテクチャのハイブリッドです。
TPS系はさらにエージェントアーキテクチャが追加されます。
Jenkins
build jobはプラットフォームごとに分けました。プラットフォーム切り替えはだいたい時間かかるのでこういう実装にしました。ただUnity2019.3から高速になりました。僕のwindows環境では45秒(Defineが切り替わりコンパイルがはしる)でした!
また単体テストや自動プレイといったものは実装してません。単体テストは余計に開発時間かかるため。自動プレイも実装コストが高すぎるのでやめました。
処理の流れは以下の通り
ruby script
SRDebuggerフォルダを削除したり、ファイルの書き換えなど軽い処理を担当してます。
ScriptingDefineSymbols設定
- プラットフォーム
- 体験版フラグ
- リリースフラグ
これらに基づいて適切なdefineを設定します。これはコマンドラインからの呼び出しです。
public static void Build(){ PlayerSettings.SetScriptingDefineSymbolsForGroup(hogehoge BuildPipeline.BuildPlayer
こういう風にすると正常にビルドに反映されません、別分け必須です
AssetBundleビルド
体験版かもしれないのでここでビルドして間引きします。あとビルドが必要とするアセットバンドルのバージョンと差異が出ては困るのでまとめました。
Playerビルド
BuildPipeline.BuildPlayer呼ぶだけです。本当はここでAssetBundleのコピーをフックして不要な物はコピーしないとか仕込めればよかったのですが不可能っぽいです。
この後Standalone環境であればdocや実行ファイルなどをzip圧縮します。
Android iphoneであればDeployGateへアップロードしたりもします。
考察
プラットフォームごとだけでなく、体験版によってもジョブを分けてもよかったかなと思いました。なぜなら体験版作った後にリリース版作ると、一度アセットバンドルが体験版用に間引かれているため再ビルド発生しビルド時間が延びるようになります。
最適化
現在Android版絶賛最適化中です。
視界判定などで使うPhysics.Overlap系の数が多すぎるため高負荷になってます。
FixedUpdateが重すぎる罠
上記アセットのような自作のRigidbodyベースのキャラクター制御系を使っています。当然FixedUpdateで更新するのですが、ここで重い処理をいれすぎたため低スペック端末で極度の処理落ちが発生しました。
FixedUpdateが重すぎるとUpdateとレンダリングされません。そしておそらくmaximum allowed timestepに達するまでレンダリングされません。また処理が重すぎてこの値を超えそうな場合は物理演算とFixedUpdate処理が打ち切られます。ゲームとしては致命的です。
対策
NPC全員Update処理に切り替えました。もともとNavMeshに沿って動いていたのでFixedUpdateで動作させる必要はありません。
SphereCastやBoxOverlapなど処理がNPC全員分呼ばれ超重かったです。今は一時的に1フレームに呼ぶNPC数を制限してます。将来的には可能な限りJobSystemで別スレッドに逃がす予定です。
Animator負荷が高い
NPCが多くwindows版と同じ骨構造をしているためかなりリッチです。CPU負荷もかなりのものでした
対策
Animator.CullingMode CullCompletelyにしました。映っていない場合のCPU負荷が大幅に改善されました。しかしたくさん画面内に映った場合はどうしようもないです。骨の少ないLODモデルを用意するくらいしかありません。
GPU負荷が高い
CPUほどではないですが低スペック端末では描画負荷も高かったです。
固定解像度
基準となる横解像度は1280としました。タブレットでも同様です。横解像度1920のnexus5xでGPU負荷が半分になりました(
動的に3D部分の解像度を変える
オプションのクオリティ設定に応じてスケールかけるようにしました。一番修正工数が少ないからそうしたのですが、専用オプションでユーザーに設定させるでもよかったかなと思いました。
ポストプロセス
ON/OFFくらいは入れてもよかったかもしれないです
OcclusionCulling
学園内でオブジェクト数を大幅に減らせました。窓はもともと不透明なので学園内に入った時かなり効果的です
レイヤーカリング
机やいすなどの小物は壁に比べて早く消えてほしかったので専用レイヤーにし早めにカリングされるようにしました。効果的です