渋谷ほととぎす通信

新しいこと、枯れたこと問わずサムザップ大庭が興味を持ったものを調査、生活の効率を求める完全趣味の技術ブログ。基礎を大事にしています。※あくまで個人ブログであり所属組織とは関係ありません

開発初期に暇を持て余しているUnityエンジニアができる42のTips前編


f:id:esakun:20150730215258g:plain:w450

本記事は、サムザップ Advent Calendar 2019 #2の12/11の記事です。

※後編リリースしました。


株式会社サムザップでUnityエンジニアをしている大庭です。
グラフィックデザイナー、Flashデベロッパーを経て、現在はUnityエンジニアでスマホゲームを作っています。Unity歴は6年程です。


これから紹介していく内容は、開発初期に後回しにされがちな、でも初期にやっておいた方が良い事ばかりです。
僕はUnityエンジニアとしてプロジェクトを進める上で以下の事を重要視しています。

  • 動くものをいち早く作ってメンバーに共有
  • ワークフロー構築、開発手法を整える事が最優先
  • いきなり正解は出ないので、不都合が出てきたら修正していくスタイル
  • 修正見直しの数を繰り返して精度を上げていく

この思想も取り入れた形でリストアップしました。

本記事の導入

新規ゲームプロジェクトがスタートし、あなたはUnityエンジニアとして参画する事になったと想定します。しかし、ゲーム仕様はまだ真っ白な状態で、計画しているモック開発の要件も決まっていません。


「あ〜仕様も決まってないし、やる事がなくて暇だな〜」


って、


んな事ぁない!!


仕様が無くてもやる事はいくらでもあります。


今回のタイトルを見て「42Tips!?多すぎじゃない・・・!?」と思ったあなた!ぜひ読んでみてください。
今まで新規開発に携わる事の多かった僕が思いつく「仕様がなくてもUnityエンジニアが開発序盤にやれる事」をツラツラ紹介していきます。

開発思想・設計系

1.開発環境・開発思想を決める

本記事の読者はUnityエンジニアが多いと思いますので、開発環境はUnityをベースに考えていきます。

開発環境Unityについて

Unityのアップデートに関して、随時追いかけていくのが良いと考えています。バージョンが離れてしまうと、APIのインターフェースに変更が入っていたりして、思わぬ修正が発生する場合があります。ただし、メジャーアップデート直後はバグが多い可能性もあるので、アップデートの通知が来てもすぐにバージョンアップせず少し様子を見る事にしています。


20XX.5、20XX.6辺りまでマイナーバージョンが上がってくると、比較的安心してバージョンを上げる気持ちにさせてくれます。

ソースコードのエディタについて

  • コードフォーマットの共通化に時間をかけたくない
    • 自動でフォーマットする機能がある
  • Unity開発をする上で機能が充実している

ソースコードを書くメンバーは、以上の点を満たすRiderを使うようにしています。 コードフォーマットの設定はカスタマイズする時間がもったいないのでデフォルト状態で使用しています。
ただ、Riderのバージョンによって微妙にデフォルト状態が違うので揃えた方が安全です。

開発思想について

ここはプロジェクト次第で、一概には言えないため、僕がよく採用する開発思想を紹介します。

a.基本はUnityの機能を素直に使うようにする

Unityエンジンの設計思想に反しないように機能を使うようにします。 これはUnityがアップデートした時にトラブルが起き辛いようにする他、後から参画したメンバーが理解しやすくするためでもあります。
※ピーキーにチューニングしたい場合は、置き換え可能な部分に切り出して実装するなど工夫をするのが良いと考えています。

b.巨大なサードパーティフレームワークに依存しないようにする

各メンバーの学習コストが増えて即戦力にならない問題や、採用フレームワークがアップデートされなくなるなどの運用後のリスクを回避するためです。
新しい技術を使った方が便利だったり、一時的な開発効率が上がる場合もあるのですが、開発から運用後の事を想定して採用技術は選定していきたいところです。


新しいものは、その分検証期間が必要ですし、見えていないトラブルもありえます。また、世の中に情報が溜まっていないので余計に時間がかかります。
開発時間に余裕のあるプロジェクトであれば新しい技術の採用を検討する価値はあり、逆に短納期の場合は速度を優先し、枯れた安定した技術を採用するという事も重要だと思っています。

2.Unityプロジェクトディレクトリ構成を決める

何をどこに格納するのが良いかは最初に方針だけでも決めておいた方が良いです。


僕が最近採用する構成は以下です。
※Assetsフォルダ配下の構成です。

┌ AssetBundleModule/ ・・・サブモジュール
    ├ AssetBundles/・・・AssetBundleにビルドされるものを格納
    └ Tools/・・・AssetBundleサブモジュール用のツール(主にアセットのチェッカー、ビューワー、非エンジニア向け機能)
├ Project/・・・アプリに含まれるアセット
    ├ Title/ 
        └ (タイトルシーンのコンテンツが格納される)
    ├ OutGame/
        ├ Common/・・・OutGameの共通アセット
        └ Display/・・・各画面
            ├ Home/ ・・・ホーム画面ソースコード、prefab
            ├ Quest/ ・・・Quest画面ソースコード、prefab
            └ Menu/
        └ Dialog/・・・ダイアログ(ポップアップ)
            ├ Alert・・・アラートダイアログソースコード、prefab
            └ Shop・・・ショップダイアログソースコード、prefab
    ├ Battle/
        └ (バトルシーンのコンテンツが格納される)
    └ Resources/ ・・・ Projectで唯一のResourcesフォルダ。
├ ExternalAssets/ ・・・外部アセット(アセットストアから落としたものとか)
├ Modules/・・・サブモジュール群
├ Preset/・・・プリセットデータ
├ ScriptTemplates/・・・スクリプトテンプレート
├ StreamingAssets/
├ Tools/・・・開発ツール
└ Sandbox/ ・・・テストコード格納
    └ ohba_shunsuke/ ・・・各開発者ごとにディレクトリを切ったテストコード

Resourcesフォルダは無闇に増やさない

どのようなリソースがアプリに含まれるか分からなくなるため、無闇にResourcesフォルダは作らない事にしています。

アセットストアからダウンロードしたものをまとめる

アセットストアからダウンロードしたものは、基本的にAssets配下に散らばってしまいフォルダ構成を汚し、視認性を悪くしてしまうため、ExternalAssetsにまとめています。

ソースコードとprefabを無理やり分けない

よく見かける以下のような区分けはしません。

フォルダ名 内容
Scripts ソースコードを格納
Prefabs Prefabを格納

視認性が悪くなるため、スクリプトとPrefabを別々に分けず、同じディレクトリに入れる事が多いです。機能単位でディレクトリを切ってそこにまとめるのが良いのではないかと思っています。

3..gitignoreの設定

本記事は細かい粒度で書いてしまっているので、当たり前やん!!みたいな事も書いていますが、.gitignoreを設定していなかったプロジェクトにぶち当たった事もあるので一応言及しておきます。
最初から.gitignoreを作成しておきましょう。

4.AndroidManifest.xmlの設定

初期状態ではファイル自体存在しないので、とりあえず用意はしておきます。
ゲーム開発でAndroidのマルチウィンドウに対応する事は少ないと思うので、マルチウィンドウをオフにする処理だけ追加しておきます。

5.コーディング規約を決める

コーディング規約は、メンバー間で宗教戦争みたいなものに発展する場合もあり、地味に時間を取られがちです。
ただコーディング規約決定に時間を割くのはナンセンスなので、Riderデフォルト設定のコーディング規約に準ずるという事で、今のプロジェクトは進めています。

「Riderさんがそう言ってるから仕方ないよね♥」

※ただしRiderのバージョンごとに微妙に設定内容が異なる可能性があるので注意です

6.MonoBehaviourクラスのAwake、Startメソッドは極力使わない

Unity特有のコード規約みたいな話です。

Awake、Startを無闇に使用してしまうと、、、

  • 実行順がわからない(保証されない)※Script Execution Orderを無闇に変更したくない
  • 知らない所で処理が走られてしまうのが嫌だ(管理できない)

以上の理由からMonoBehaviourが提供するAwakeやStartなどのイベント関数は、極力使わないようにしています。
後から入ったメンバーも処理の流れを理解しやすいというメリットにも繋がります。

7.大人数で開発できるような設計

人数が集まれば開発速度、単純に上がるという事はありません。

  • 開発のフェーズ依存
  • 参画するメンバーのスキル依存

以上の理由の他にもう一つ大きな要因として、大人数で開発できる設計になっているか?です。

  • 疎結合な実装になっているか
  • Viewとロジックが別れているか

ソースコードだと上記のような話になってきます。
Unityの場合は、GameObjectの設計も重要です。


例えばアウトゲームであれば、合計で100画面あるUIが一つのPrefabになっていたりすると、20画面同時に修正箇所が発生しても同時に1人しか作業ができません。
※Prefabは競合するとうまくマージされないため


当たり前かもしれませんが、各画面を1Prefabずつ分けておくと良いです。
また、その画面を構成するパーツ要素も細かくprefab化できていると大人数で開発をしやすくなります。


できる限りシーンを触らなくても開発できるようにしておくのも重要です。

8.文字列をソースコードやPrefabへの直書き回避とローカライズ対応

任意の文字列をソースコードやPrefab(Textコンポーネント等)に直書きすると、以下の状況で困ります。

  • 文字列を一括で置き換える必要が出た時大変
  • ローカライズする時にどこで使用しているか探すのが大変

そんな時は、キーと値をセットにしたデータを用意し、そこから文字列を取得するようなヘルパークラスを用意する手が考えられます。

 lang,JP,EN
 yes,はい,YES
 no,いいえ,NO
 cancel,キャンセル,CANCEL
 hp,HP,HP

上記のようなcsvを実行時にロードできるようにしておきます。

 // 日本語をセット
 Localization.SetLanguage("JP");
 var str = Localization.Get("yes");
 Debug.Log(str); // output : はい

このような感じでキーを引数にして文字列を取得するように実装しておくと、文字列がソースコードに散らばらなくなって良いです。


Localization.SetLanguage("EN");とすれば、機械的に英語に文字列が置き換わるようになるので、ローカライズ対応の手助けになるかもしれません。

基盤開発系

9.Unityのシーン遷移基盤開発

ある程度大きなUnityプロジェクトになってくると、シーンを分ける事があります。
分けた際に、そのシーン間を遷移させる仕組みを設計、実装しておくと良いです。

SceneManager.LoadSceneAsync("シーン名", LoadSceneMode.Single);

上記のUnity標準機能を個々に書いていっても実現できますが、シーン間に演出、表現をはさみたい場合、このAPIだけでは不足しています。こういう場合はSceneManagerのラッパークラスを作って対応します。

どんな演出が考えられるか?

前述した通り、演出が挟まるのであれば、その分の余白を残して実装する事が重要になります。

  • Tips表示
  • アセットローディングバーの表示
  • 暗転させる

などなど。


それらを仕様が無いながらも、他ゲーム、今までの経験を参考に拡張できるようなシーン遷移管理クラスを作っておくと後々楽になります。

10.画面遷移システムの作成

先程はシーン to シーンの話でしたが、今回は1つのシーン内での画面遷移についてです。


画面遷移は必ず必要になります。デザイン次第で大きく変わりますが、とりあえず動くものを作っておきます。

 // 全画面をEnumで定義する
 public enum DisplayType
 {
     None = 0,
     Home = 1,
     Quest = 2,
     Gacha = 3,
     Menu = 4
 }

画面をEnumで定義します。

https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/328771/d586912a-db7d-a3ce-411c-4c33b0444013.png

上の画像のように、Enum値と画面prefabを紐付けておきます。

// 画面遷移開始
DisplayManger.Instance.Goto(DisplayType.Home);

上記のようなコードで画面が遷移するように実装しています。
すごくざっくりな話になっていますが、表示中の画面Prefabを指定したPrefabに入れ替える実装をしています。

11.どのシーンからでも開発できるような仕組みの設計

9.Unityのシーン遷移基盤開発の内容に関連しますが、どのシーンからでも起動できるようにしておくと、開発の効率は上がります。

それを実現するために、各シーンのエントリーポイントとなる共通クラスを継承したクラスをシーンのルートに配置しておくというルールを設けて実装しています。

例えば

  • タイトルシーン
  • アウトゲームシーン
  • バトルシーン

という3シーン構成であれば、それぞれにSceneEntryクラスを継承したのコンポーネント(TitleSceneEntry、OutGameSceneEntry、BattleSceneEntry)を各シーンのルートに配置します。

シーンエントリーポイントクラスの仕事とは?

  • 通常起動とそれ以外の起動の処理における初期化処理の共通化
  • クラス名の通り、そのシーンのエントリーポイントとして動くようにする(そのために、Awake、Startをあえて使わないようにしてます)
    ※参考 : 「6.MonoBehaviourクラスのAwake、Startメソッドは極力使わない」

以下各シーン毎の起動例を紹介します。

タイトルシーンから起動した場合(通常起動)
  • ユーザーが存在しなければ利用規約のダイアログを出す(ユーザーが存在すれば省略してログイン)
    • 利用規約を許可したらユーザーを作成
  • マスターデータの取得
  • アセットのダウンロード
タイトルシーン以外から起動した場合(非通常起動)
  • ユーザーが存在しなければ、利用規約など出さずにユーザー作成。存在すればログインする。
  • マスターデータの取得
  • アセットのダウンロード

という処理フローになり、通常起動と違って色々省略されています。


これらの処理の分岐をシーンエントリーポイントクラスが担っており、このクラスを使う事でどのシーンからでも起動できるようにしています。

開発効率がとても良くなるのでオススメで、

保守していきたいところです。

12.サウンド再生基盤

ゲーム開発する上ではサウンド再生は必ず必要になります。
開発序盤ではどんな技術を採用するかは決まらない可能性が高いです。

  • Unity標準サウンド
  • CRI

など。


そこで何を採用しても大丈夫なように再生部分を以下のようなインターフェースにしておきます。

 public interface ISoundPlayer
 {
     int Play(string soundName);
     
     void Stop(int id);
     
     void SetVolume(float volume);
     
     void SetMute(bool isMute);
 }

開発初期はUnity標準サウンドで実装しておいて、仕様が決まってきたら必要な技術に内部実装を置き換えていく想定です。

13.動画再生基盤

動画を再生させる可能性がある場合は、事前に再生検証をしておきたいです。
12.サウンド再生基盤の動画版なので内容は割愛します(考え方は同じです)。

一点、ゲーム内設定のBGMボリュームを動画再生時に反映するというのを忘れやすいので注意です。


実機で再生できるかどうかの検証はもちろんですが、動画は比較的ファイル容量が大きくなるため、ファイルをサーバーに置いて、ダウンロード時間の確認(実機で)を早めにしておくとよいでしょう。

14.ゲームエフェクトの再生と再利用機構

バトル中に大量のゲームエフェクトが表示される事が想定される場合、エフェクトオブジェクトを再利用できる機構を事前に作っておくと良いです。

  • 大量のエフェクトを毎度生成するとGCが発生しやすい
  • Instantiateがそもそも処理的に重いため、スパイクの原因になる

ユーザー体感をより良くするためにできる限りエフェクトを再利用できるように設計しておきます。
またバトル開始前のローディング中にあらかじめ必要なエフェクトをロードする機能を追加しておくと良いかもしれません。

15.ローカルファイル保存の仕組み

ローカルに保存するのはよくあるので、事前に仕組みを用意しておく事ができます。 内部の実装は置き換えられるように一旦ラッパークラスを定義します。よく使いそうなメソッドを用意しておきます。

  • SetInt
  • SetFloat
  • SetString
  • SetBool
  • Set<T>
  • GetInt
  • GetFloat
  • GetString
  • GetBool
  • Get<T>

コード例

LocalStorage.SetString("key", "Sumzap");

var value = LocalStorage.GeString("key");
Debug.Log(value);// output : Sumzap

16.外部アセットを想定したロード処理

アプリ外部からアセットをダウンロード、ロードして使用する場合は、アセットバンドルや、今だとアドレッサブルアセットシステム(以下:AAS)を使用すると思います。
開発序盤ではそのシステムが無い、または何を使用するかを決めていない場合もあると思います。
ただ、決まっていないからといって、実装は後回しにすると手戻りが多く発生する可能性があるため、そうならないような実装を最初から心がけたいところです。

インターフェースを利用して抽象化

 public interface IAssetBundleManager
 {
     void Load<T>(string assetBundleName, System.Action<T> onComplete, System.Action onError = null);
     
     void Release(string assetBundleName);
 }

このような外部アセットをロードと解放を実装したインターフェースを定義しておきます。 開発序盤はResources.Loadで実装しておき、後から実際のAssetBundleまたはAASに置き換えていくと良いかもしれません。


後に出てくる32.アセットバンドル名を自動で設定されるようにするであえてアセットバンドルのパスをAssetBundles/Resources/のようにResouresをわざわざ挟んでいるのは、アセットバンドル名とResources.Loadのパスを合わせる事で、本実装時の手戻りを最小限にするためです。

17.Androidバックキー設計

後に回すと面倒なのがAndroidバックキー対応です。


バトル中やバトルのリザルトでは不要な場合がほとんどなので、Androidバックキーが必要になるのは主にアウトゲーム側です。

Androidバックキーを実行した時は以下の処理を想定して実装しておく

  • ダイアログ表示中だったらダイアログを閉じる(閉じると不都合なダイアログが存在するので、分岐できるように設計しておく)
  • 画面遷移システム側で履歴を保持して前の画面に戻る
  • ホーム画面でバックキーが押されたらタイトルへ戻す(遷移履歴を消す)
  • タイトルではアプリを終了する

Androidバックキーの連打対応

画面遷移演出中、API実行中などAndroidバックキーが動いては不都合な状態が存在するので、演出中、API実行中などAndroidバックキーが動いてはいけない状態をフラグとして取得できるようにして、trueだったらAndroidバックキーを押しても動かないようにしておきます。

18.ローカルPush通知の実装

ローカルPush通知もスマホゲーム開発なら必ず実装する機能なので、先に手を付けておきます。 以下のような要件が想定されます。

  • アプリを終了した時、サスペンドした時にローカルPushを指定の時間後に予約する
  • アプリを起動した時、ローカルPushの予約を全て消す
  • 設定からON/OFFできる事が多いので、設定値を取得できるようにする
  • Push通知の許可タイミングを任意のタイミングにできるようにしておく

サクッと無料で実装しておきたい場合は、Unity公式のMobile NotificationsというパッケージがPackage Managerからインストールできます。

続きは後編へ

今回は42のTipsのうち18までを紹介しました。
まだ半分ですが「うへぇ〜〜」と思ったあなた!これでも暇しなくて済みますね。
後編はサムザップ #1 Advent Calendar 2019 - Qiitaの12/24に公開致します。


そして今回紹介した内容を実装し終えたとしても、それでFIXではありません。
プロジェクトが進む中で見直し、修正して適正な姿に変化させていく必要があります。

まずは手を動かし形にしてみてください。これらのTipsが皆様の開発のお役に立てたなら幸いです。

明日は@kazuhiro1128さんの記事です。