sh1’s diary

プログラミング、読んだ本、資格試験、ゲームとか私を記録するところ

Unity ローカルプッシュ通知のやり方 (Android)

f:id:shikaku_sh:20200910152454p:plain

この記事は Unity で(ローカル)プッシュ通知をするやり方をメモした記事です。

ローカルプッシュ通知とは、端末に通知を送る際にサーバーを使用しません。端末の中に通知予定を入れて、あとから自動的に通知を発生させることが可能です。

プッシュ通知の雰囲気は以下の Android 端末の画像でわかるかと思います。

f:id:shikaku_sh:20200916104208j:plain:h300
こんな感じで、できました

よくあるのは、おつかいを設定して 30 分後になったら、おつかい完了の通知を発生させるようなやつです。

注意:この記事は Android 向けにテストしています。iOS も変わらないと思いますが、テストしていません。


Unity Mobile Notifications Package をインストールする

メニューの「Package Manager」から「Mobile Notifications」をインストールします。

f:id:shikaku_sh:20200916104329p:plain:w600

  • Window > Package Manager > (Menu) Mobile Notifications


プロジェクト設定

Android の通知に表示されるアイコンを設定します。

f:id:shikaku_sh:20200916105010p:plain:w600

  • Edit > Project Settings > (Menu) Mobile Notification Settings

用意するアイコンのサイズは、次のふたつをそれぞれ Type の icon_small と icon_large に設定します。

  • 48x48
  • 192x192

f:id:shikaku_sh:20200916104830p:plainf:id:shikaku_sh:20200916104833p:plain
サンプルで使った画像

48x48 の画像は透明部分をくりぬいた白い画像が表示されました。

設定した画像の identifier はデフォルトだと「icon_0」や「icon_1」になっていますが、このテキストの値はあとで使用するので、きちんと識別子となる名前(テキスト)を設定したほうがよいと思います。

番号をつけても、これは(正確には)連続性がないので、「Unity 開発に関する 50 の Tips 〜ベストプラクティス〜(2016 Edition)」だと Tips 68 のようになっています。やっぱり、推奨とはいかないです。

Android のアイコンアセットには公式が挙げている命名規則がありますので、「アイコン デザイン ガイドライン」をここでは利用することにします。


アイコン アセット共通の命名規約の例

表にすると、こんな感じかと。

アセットタイプ 接頭辞
アイコン ic_ ic_star.png
ランチャー アイコン ic_launcher ic_launcher_calendar.png
メニュー アイコン、アクションバー アイコン ic_menu ic_menu_archive.png
ステータスバー アイコン ic_stat_notify ic_stat_notify_msg.png
タブアイコン ic_tab ic_tab_recent.png
ダイアログ アイコン ic_dialog ic_dialog_info.png

なので、ここでは次のテキストとします。

  • ic_stat_notify_small
  • ic_stat_notify_large

なお、アイコンに使う画像はテクスチャーとして利用しないので「Texture Type」を「Editor GUI and Legacy GUI」にして最適化を防ぐような設定にしました。

f:id:shikaku_sh:20200916104405p:plain


スプリプトの実装

LocalNotification を機能を実行するためのインターフェースを用意しました。この機能は AndroidiOS で別々に実装したほうがよいので、インターフェースを挟んでおいたほうが後々便利です。

using System;

public interface ILocalNotification
{
    /// <summary>
    /// Small Icon タイプのアイコン ID を表すテキストを取得または設定します。
    /// </summary>
    string SmallIcon { get; set; }

    /// <summary>
    /// Large Icon タイプのアイコン ID を表すテキストを取得または設定します。
    /// </summary>
    string LargeIcon { get; set; }

    void CreateChannel(string cannelId, string cannelName, string description);
    void CancelAll();

    int Schedule(string title, string text, DateTime fireTime);
    int ScheduleInSeconds(string title, string text, int seconds);
    int ScheduleInMinutes(string title, string text, int minutes);
    int ScheduleInHours(string title, string text, int hours);
}

Android のコードです。ここがこの記事で一番コアな部分です。

using System;
using System.Collections;
using System.Collections.Generic;
using Unity.Notifications.Android;
using UnityEngine;

public class LocalNotificationForAndroid : ILocalNotification
{
    public string ChannelId { get; set; } = "channel-id";
    public string SmallIcon { get; set; } = "icon_0";
    public string LargeIcon { get; set; } = "icon_1";

    public void CreateChannel(string cannelId, string cannelName, string description)
    {
        ChannelId = cannelId;

        var cannel = new AndroidNotificationChannel
        {
            Id = ChannelId,
            Name = cannelName,
            Importance = Importance.High,
            Description = description,
        };

        if (SmallIcon == "icon_0")
        {
            Debug.LogWarning($"{nameof(SmallIcon)} プロパティの値が初期値のままです。アイコンを正しく表示できない恐れがあります。");
        }

        if (LargeIcon == "icon_1")
        {
            Debug.LogWarning($"{nameof(LargeIcon)} プロパティの値が初期値のままです。アイコンを正しく表示できない恐れがあります。");
        }

        AndroidNotificationCenter.RegisterNotificationChannel(cannel);
    }

    public void CancelAll()
    {
        AndroidNotificationCenter.CancelAllScheduledNotifications();
        AndroidNotificationCenter.CancelAllNotifications();
    }

    public int Schedule(string title, string text, DateTime fireTime)
    {
        var notification = new AndroidNotification
        {
            Title = title,
            Text = text,
            FireTime = fireTime,
        };

        notification.SmallIcon = SmallIcon;
        notification.LargeIcon = LargeIcon;

        return AndroidNotificationCenter.SendNotification(notification, ChannelId);
    }

    public int ScheduleInSeconds(string title, string text, int seconds)
        => Schedule(title, text, DateTime.Now.AddSeconds(seconds));
    public int ScheduleInMinutes(string title, string text, int minutes)
        => Schedule(title, text, DateTime.Now.AddMinutes(minutes));
    public int ScheduleInHours(string title, string text, int hours)
        => Schedule(title, text, DateTime.Now.AddHours(hours));
}

このクラスを使って、通知をやってみることにします。注意する点として、登録・通知をしたクラス(ここだと LocalNotificationForAndroid)のインスタンスを保持しておかないと、正しく端末に通知が発生しませんでした。なので、このクラスを singleton にするか singleton クラスのプロパティとして持つようにしてみます。


singleton クラスで保持する例

簡単な singleton パターンを適用したクラスに通知用のクラスを用意しました。ポイントは、アイコンのプロパティに設定しているテキスト値が、「アイコン アセット共通の命名規約の例」で決めた2つのテキストです。

このテキストが、通知されるアイコンと紐づいてしまいます。(テキスト以外の方法で設定をやりたいけど、仕様上やりづらい+ここだけ設定すれば変更することはもう無いので、こうしました)

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public sealed class NotificationManager
{
    public static NotificationManager Instance { get; } = new NotificationManager();

    public ILocalNotification Notification { get; private set; }

    private NotificationManager() 
    {

    }

    public void Initialize()
    {
        ILocalNotification notification = new LocalNotificationForAndroid();

        notification.SmallIcon = "ic_stat_notify_small";
        notification.LargeIcon = "ic_stat_notify_large";

        notification.CreateChannel("sample0916", "sampleName", "sampleDescription");

        Notification = notification;
    }
}

ここで突然 ChannelID、CannelName、Description をサンプル的な値で埋めていますが、別にこのコードでも Android で動作するのを確認しています。Android の通知仕様を確認するとわかりやすいかもしれません。(おそらくこれをアダプトしてるのだと思います)

Channel ID は、チャンネル登録の関数と、スケジュール登録の関数で同じ値が利用されています。アイコンのように Unity プロジェクト内と紐づくものは無いと思います。

拡張性の観点から挙げるポイントは、以下のコードブロックです。

ILocalNotification notification = new LocalNotificationForAndroid();

下コードの感じにするだけで、切り替えることができると思います。インターフェースでメソッドを定義してあるので、通知予約のメソッドも共通化できているので便利。(それ以外は LocalNotificationForDummy とかもいいかも)

#if UNITY_ANDROID
    ILocalNotification notification = new LocalNotificationForAndroid();
#elif UNITY_IOS
    ILocalNotification notification = new LocalNotificationForiOS();
#else
    ILocalNotification notification = new LocalNotificationForDummy();
#endif

あと、singleton パターンはとても強力なので、設計のやり甲斐があります。NotificationManager だと他に持つ機能がないかもしれません。EnvironmentManager のようなものに統合してもいいかもしれないです。


通知をテストする(+使い方)

アプリケーションの開始直後に設定をして、あとはそのインスタンスを使いまわす感じにしてみます。なので、アプリケーションのエントリーポイントでやってみることにします。

アプリケーションのエントリーポイントは次の記事で説明しています。

こんな感じになりました。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class App
{
    [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
    public static void EntryPoint()
    {
        // 初期化
        var manager = NotificationManager.Instance;

        manager.Initialize();

        // サンプル用コード
        manager.Notification.ScheduleInSeconds("テストのタイトル", "テストのテキストです。", 10);

        Debug.Log("EntryPoint Called.");
    }
}

実行結果は、こうなります。問題なさそうですね。

f:id:shikaku_sh:20200916104208j:plain:h500
こんな感じで、できました


サンプル

GitHub に「unity-local-notification-practice」サンプルを公開しています。


参考

Unity 変数の値が変化したとき“1度だけ”コードを実行したい「ObservedValue」パターン

f:id:shikaku_sh:20200910152454p:plain

変数の値が変化したとき“だけ”コードを実行したいというような、ゲーム中ではよくあるイベントをどのように実装するのか、という話についてです。

この内容は「Unity 開発に関する 50 の Tips 〜ベストプラクティス〜(2016 Edition)」の Tips 40 の内容に挙がっていた「記事」を個人的に訳をした内容です。

ObservedValue クラスは、著者の作成したクラスですが、とてもシンプルなクラスだったので、この記事の最後にクラスのコードを加筆しています。


「ObservedValue」はどういうものか(The new class in Extensions, ObservedValue: what is it for and how to use it)

もし、あなたがたくさんのゲームのコードを書いたことがあるなら、こんなコードを書いていたことがあるのではないか:

class MyBehaviour : MonoBehaviour
{
   int count;
   
   public void Start()
   {
       count = CalculateCurrentCount();
   }

   public void Update()
   {
       var currentCount = CalculateCurrentCount();
       
       if(currentCount != count)
       {
           count = currentCount;
           DoSeomtingExpensive();
       }
   }
}

これは、ある値を(たとえば、フレームごとに)チェック or 計算したりしているが、値が変化しときに“だけ”なんらかのアクションを実行する、という考えです。

上のコードのサンプルでは、それほど悪いものではないですが、ファイルの内にこんなコードをいくつか作っているなら、それは面倒を起こす可能性があります。

また、特に値を複数の場所で更新する場合や、Update() メソッドの代わりに値の変更後にすぐチェックを実行したいなら、エラーを作りやすいです。

ObservedValue (Assets Store) は、このコードをきれいにパッケージ化することを目的としています。これは、ひな形 (boiler plate) の部分を担うと共に、値が変更されたとき、アクションを問題なく実行し、同じコード中で使い分けることができます。

class MyBehaviour : MonoBehaviour
{
   ObservedValue<int> count;
 
   public void Start()
   {
      count = new ObservedValue(CalculateCurrentCount());
      count.OnValueChanged += DoSeomtingExpensive;
   }
 
   public void Update()
   {
      count.Value = CalculateCurrentCount();
   }
}

これは、最初のバージョン(おそらく Assets のバージョンの話)と同じ機能のことをしていますが、よりクリーンなコードになりました。

重要な使用例のひとつとしては、ユーザーが値を変更したとき、あなたがなにか(別の値など)を変更したい場合、すぐに GUI (エディター)を使用することがあります。実際、私たちがクライアントのために開発したレベルエディターには、画面に描画されるものに影響をあたえる変数が20程度あって、ツールを改良するうちに多くのバグに遭遇しはじめたため、この ObservedValue クラスを書きました。

もうひとつ役に立つ戦略は、フレームごとに複数の計算をしないようにするため、state machine パターンと組み合わせることです。(これを普通にやると、見苦しいフラグ管理になる)以下は、state machine を使わない例:

class MyBehaviour : MonoBehaviour
{
   bool isDirty;
   ObservedValue<int> count1;
   ObservedValue<int> count2;
 
   public void Start()
   {
      isDirty = false;
      count1 = new ObservedValue(CalculateCurrentCount1());
      count1 += () { isDirty = true; };
 
      count2 = new ObservedValue(CalculateCurrentCount2());
      count2 += () { isDirty = true; };
   }
   
   public void Update()
   {
       count1.Value = CalculateCurrentCount1();
       count2.Value = CalculateCurrentCount2();
 
       if(isDirty)
       {
           DoSomethingExpensive();
           isDirty = false;
       }
   }
}

例によって、この単純なケースではそれほど悪いコードになっていません。複数のクラスにまたがっていたり、見苦しくなるコードの箇所がいくつもあったり、見苦しさのレベルがひどかったりすると、コードはより複雑になってしまいます。

つぎのコードは、state machine パターンを使ってどのように見えるのかを示す:

class MyBehaviour : MonoBehaviour
{
   public enum DirtyState {Dirty, Clean};
 
   StateMachine dirtyManager;
   ObservedValue<int> count1;
   ObservedValue<int> count2;
 
   public void Start()
   {
      dirtyManager = new StateMachine();
 
      dirtyManager.AddState(DirtyState.Clean);
      dirtyManager.AddState(
          DirtyState.Dirty, 
          null, 
          () => 
          {
             DoSomethingExpensive();
             dirtyManager.SetState(DirtyState.Clean);
          }); 
 
      count1 = new ObservedValue(CalculateCurrentCount1());
      count1 += () { dirtyManager.SetState(DirtyState.Dirty); };
 
      count2 = new ObservedValue(CalculateCurrentCount2());
      count2 += () { isDirty = true; };
 
   }
   
   public void Update()
   {
       count1.Value = CalculateCurrentCount1();
       count2.Value = CalculateCurrentCount2();
       
       dirtyManager.Update();
   }
}

上くらいシンプルなサンプルだと、state machine パターンを使用してもメリットはありません。恩恵を得ることができるのは、状況がもっと複雑になってからです。もちろん、上のコードにはひな形 (biler plate) になる部分があるので、あなたが DirtyManager で処理してもよいでしょう。(DirtyManager は、将来の拡張ライブラリとしてよい手です)

つぎに、そういったクラスのドラフトを示します:

public class DirtyManager
{
   private enum DirtyState {Dirty, Clean};
 
   private StateMachine dirtyManager;
   public event OnShouldCleanDirt; //needs a better name!
 
   public DirtyManager()
   {
      dirtyManager = new StateMachine();
 
      dirtyManager.AddState(DirtyState.Clean);
      dirtyManager.AddState(
          DirtyState.Dirty, 
          null, 
          () => 
          {
             if(OnShouldCleanDirt != null) 
             {
                 OnShouldCleanDirt();
             }
 
             dirtyManager.SetState(DirtyState.Clean);
          }); 
   }
 
   public void Update()
   {
       dirtyManager.Update();
   }
 
   public void Observe(ObservedValue observedValue)
   {
       observedValue.OnValueChanged += SetDirty;
   }
   
   private void SetDirty()
   {
       dirtyManager.SetState(DirtyState.Dirty);
   }
}

サンプルはこうなります:

class MyBehaviour : MonoBehaviour
{
   DirtyManager dirtyManager;
   ObservedValue<int> count1;
   ObservedValue<int> count2;
 
   public void Start()
   {
      count1 = new ObservedValue(CalculateCurrentCount1());
      count2 = new ObservedValue(CalculateCurrentCount2());
 
      dirtyManager = new DirtyManager();
 
      dirtyManager.Observe(count1);
      dirtyManager.Observe(count2);
 
      dirtyManager.OnShouldCleanDirt += DoSomethingExpensive; 
   }
   
   public void Update()
   {
       count1.Value = CalculateCurrentCount1();
       count2.Value = CalculateCurrentCount2();
       
       dirtyManager.Update();
   }
}

コードはよりクリーン(わかりやすく)なり、追加する複雑さに対してより強かになりました。

最後に、テストコードは state machine パターンが本当に必要ですか? 答えは「必要ありません」。簡単なコードを保つように、bool を使います。

しかし、より複雑なケースでは、state machine パターンは価値を提供することができます。見苦しさのレベル、特別な状況に対処するためなどに使えます。(たとえば、なにか他のことが起こるまで、計算を遅らせたい場合など)


補足(ObservedValue のコード確認)

以下の内容は、新しく私のブログで補足として調べた内容です。この記事のメインコンテンツである ObservedValue は UniRx の一部分(ObserveEveryValueChanged など)のようなものですが、下記コードのように実装はとてもシンプルのようです。

Gamelogic Extensions
Version 2.5 Feb 21, 2019:
Assets>Gamelogic>Extensions>Plugins>Scripts>Patterns

public class ObservedValue<T>
    {
        private T currentValue;
        public event Action OnValueChange;

        public ObservedValue(T initialValue)
        {
            currentValue = initialValue;
        }

        public T Value
        {
            get { return currentValue; }

            set
            {
                if (!currentValue.Equals(value))
                {
                    currentValue = value;

                    if (OnValueChange != null)
                    {
                        OnValueChange();
                    }
                }
            }
        }

        public void SetSilently(T value)
        {
            currentValue = value;
        }
    }

基本的には、Value プロパティに対して値を設定するのがこのクラスの用途のようですね。このときに、値が一致しなければ !Equals(value)OnValueChange イベントを実行するという仕組みです。

値を設定するときに、このイベントを実行したくないときは、SetSilently() メソッド経由で現在値を更新しています。こんな感じで、趣味でプログラミングをしていて UniRx を無免許運転するくらいなら、車輪の再開発をして、こうしたコードテクニック自体をちょっとずつ覚え、地に足をつけたコーディング力を身につけるのも悪くないと私は思います。


パフォーマンスに関する意見

見てわかるとおり、もともとのコードは if 文をひとつ入れて処理していた内容が、クラスを定義して作ったプロパティは、if 文が2つにイベントを実行すると、パフォーマンスの上では間違いなく(僅かに)低くなった実装になっていることが伺えます。

このテクニックが良いことか悪いことかを考えてみます。この答えのひとつを挙げると、リファクタリングの善し悪しを、審美的なものにして、個人の好みとするのは微妙です。

単なる好みではない面からいくと、一般的な意味での良いコートとは、どれだけ変更が容易なのかを示す柔軟性・拡張性で決まっています。なので、ほんの少し低速になる代わりに、(この頻出するだろう問題に対して)健全なコードになるようリファクタリングし、(元のコードよりも)生産性をよりよい状態にした ObservedValue はよいコードといえるかもしれません。

僅かなパフォーマンスとリファクタリングの関係についてもっと知りたい場合は、リファクタリング関係の本を読むといいです。

最初の説明にもあったように、シンプルなコードなら、こうしたリファクタリングは不要だと思います。しかし、見た目に冗長であること+コード設計として劣化を早めやすい傾向のコードなら、すくなくとも注意が必要です。

コンピューターがプログラムを実行する際に、CPU サイクルを余分に少し必要にするという事態よりも、コーダーの都合を大切にするようなコーディングを選択するような選択は年々増えてきています。

パフォーマンスを要求するゲームでも、細かい部分は作業性・メンテナンス性をとりますよ、と。(そもそも Unity + C# だし、そりゃそうだよね)


参考

増補改訂版Java言語で学ぶデザインパターン入門

増補改訂版Java言語で学ぶデザインパターン入門

  • 作者:結城 浩
  • 発売日: 2004/06/19
  • メディア: 大型本

Unity 開発に関する 50 の Tips 〜ベストプラクティス〜(2016 Edition)

f:id:shikaku_sh:20200910152454p:plain

この記事は「Unity開発に関する50のTips 〜ベストプラクティス〜(翻訳)」を読んでとても参考になったので、その続編になる「50 Tips and Best Practices for Unity (2016 Edition)」の内容を超訳(勉強のため個人的に訳)したものです。

Qiita にもあげているので、読みやすいほうを利用ください。

変なことを書いていたり、誤訳等あったらごめんなさい。仕事から帰ってきてから趣味でやっていたので、疲れたところなんかは、翻訳ツールが増えてます。まわりくどく感じた文を省略や、省略による文脈の欠落もあるかもしれません。(そのあたりは、2012 年版の翻訳と同じで、諸々あらかじめご了承ください) - コメントに書いているのは、個人的にメモとして加筆しています。 - (括弧)の内容は、訳した元々の文章から括弧をされていたり、個人的に加筆したものがあります。



前書き

以下の記事の内容は、Gamasutra のコミュニティメンバーによって書かれたものです。考えや意見は執筆者のものであって、Gamasutra や Unity(公式)のものではありません。

約4年前にオリジナルの「50 Tips for working with Unity」の記事を公開しました。その内容の多くはいまでも関連性がありますが、多くのことが変わっています:

  • Unity がよくなったこと
    たとえば、今では FPS カウンターを信用しています。(Tips 42 のこと)Property Drawer が使えるようになったことで、カスタムエディターを書く必要がなくなりました。Prefab の働きによって、明示的にネストされた Prefab や代替案を用意する必要が減りました。Scriptable object はより便利になりました。
  • Visual Studio との統合が改善された
    デバッグが各段に楽になり、引っ掻き回すようなデバッグが減りました。
  • サードパーティ製のツールやライブラリーが改善された
    Assets Store には、デバッグやロギングを改善するよいものがあります。
  • バージョン管理がよくなった
    Prefab の複製やバックアップコピーを用意する必要がなくなった。(というか、バージョン管理の効果的な使い方がわかってきた)
  • 私がより多くの経験を得た
    過去4年間、私は多くの Unity で仕事をしました。その中には、たくさんのゲームのプロトタイプ(30日で30個のゲーム)を作った経験、Father.IO のような(執筆者が作った)プロダクションゲームの経験、そして、私たちの代表的な Unity Assets である Grids(現在は非推奨になって使えない)が含まれます。

ここでの Tips は、上述のすべてを考慮してオリジナルの Tips を修正したものです。Tips に入るまえに、ここで免責事項を述べておきます。(基本的にオリジナルと同じ)

これらの Tips はすべての Unity プロジェクトに当てはまるわけではないとした上で:



ワークフロー (Workflow)

1. 最初にスケールを決定して、すべて同じスケールになるようにビルドする

そうしないと、あとでアセットを作り直す必要がでるかもしれません。(たとえば、アニメーションのスケールはいつも正しいとは限りません)3Dゲームの場合、通常は1unity unit = 1m がベストです。

ライティングや物理を使用しない2Dゲームでは、通常1 unity unit = 1 pixel がよいでしょう。UI(と2Dゲーム)では、解像度 (HD or 2xHD) を選択して、その解像度でスケールするようにすべてのアセットをデザインします。


2. すべてのシーンを実行できる状態にしておくこと

ゲームを実行するためにシーンを切り替える必要がないようにします。ただし、すべてのシーンで必要とされる永続的なオブジェクトを持っている場合は厄介です。方法をひとつ挙げると、オブジェクト自身を singleton にすることです。singleton については、別の tips で説明します。

旧 tips.10 と同じ内容だと思います。


3. バージョン管理システム(Git など)の効率的な使い方を学んで利用すること

  • アセットをテキストとしてシリアライズする。
    実際は、シーンや Prefab がマージしやすくなるわけではないけど、何が変更されたか確認しやすくなります。
  • シーンと Prefab をまとめる手法を採用する
    一般的に、複数の人が同じシーンや Prefab を作業してはいけません。小さなチームの場合、あらかじめ(作業をはじめる前に)、他に作業していないことを聞いておくだけで十分かもしれません。シーンの所有権を示すための物理的なトークンを用意しておくと便利かもしれません。(机の上にシーンの所有権を示すトークンがあるときだけ、そのシーンの作業することができる)
  • タグをブックマークとして利用する
  • ブランチの手法を決めて採用する
    シーンと Prefab はスムーズにマージできないので、ブランチ化はすこし複雑です。どのようにブランチを使うのか決めるにしても、シーンと Prefab は一緒にするようにします。
  • サブモジュールは注意して利用する
    サブモジュールは、再利用可能なコードを維持するための素晴らしい方法です。しかし、いくつか注意点があります。
    • メタファイルは一般的に複数のプロジェクトで一貫性がありません。一般的には、non-Monobehaviour or non-Scriptable object なコードでは問題ありませんが、MonoBehaviours and Scriptable objects では問題になる恐れがあります。
    • 多くのプロジェクト(サブモジュールを1つ以上含む)で作業をしていると、ときには雪崩を起こすことがあり、すべてのプロジェクトのコード管理を安定させるために、様々のプロジェクトで pull-merge-commit-push をしなければいけないことがあります。(そして、このメンテナンス中に他の誰かが変更を加えてしまうと、さらに雪崩を起こす)この影響を最小化するための方法のひとつは、常にサブモジュール専用のプロジェクトからサブモジュールに変更を加えることです。サブモジュールを使うプロジェクトは、常に pull するだけでよくなります。


4. テストシーンとコードは分離すること

一時的なアセットやスクリプトリポジトリーにコミットして、完了したらプロジェクトから削除します。


5. ツール(もっぱら Unity 本体)をアップデートするときは、他の人も同時にする

Unity は異なるバージョンでプロジェクトを開いたときの対応がとてもうまくなりましたが、複数の作業者が異なるバージョンで作業するとリンクが失われることがあります。

旧 tips.2 とは少し違う内容です。


6. 綺麗な状態のプロジェクトにサードパーティー製アセットをインポートして、そこで新しいパッケージをエクスポートして利用する

アセットは、プロジェクトに直接インポートすると問題を発生することがあります。

  • 特に、Plugins フォルダーの直下にファイルを追加するアセット、または、Standard Assets を利用しているアセットは、衝突(ファイル名など)する可能性があります。
  • 展開されたファイルは、プロジェクト上に整理されていないかもしれません。これは使用しないと決めて削除した場合に問題(面倒)になります。

安全にインポートをする手順は以下に従ってみてください。

  1. 新しいプロジェクトを作成し、アセットをインポート
  2. サンプルを実行して、動作することを確認
  3. アセットをより適切なフォルダー構造に整理
    (通常だと、アセットに自分の好むフォルダー構造を強制しません。しかし、すべてのファイルがひとつのフォルダーに入っていること、そして、プロジェクトを上書きするような重要な場所にファイルが追加されないことを確認します)
  4. サンプルを実行して、動作することを再確認
  5. 不要なものをすべて削除する(サンプルなど)
  6. アセットがコンパイルされ、Prefab がすべてリンクを持っていることを確認し、なにかあれば、テスト
  7. すべてのアセットを選択し、パッケージをエクスポート
  8. プロジェクトにインポート


7. ビルドプロセスを自動化する

小さなプロジェクトでも有効ですが、とくに有効なのは次のとおり:

  • ゲームの異なるバージョンをたくさんつくる場合
  • 技術力の劣るメンバーがビルドをする場合
  • ビルドをする前に、プロジェクトに微調整を加える場合

詳細は「Unity Builds Scripting: Basic and advanced possibilities」(著者の過去記事)を参照してください。


8. コーディング資料 (your setup) をドキュメント化すること

ほとんどのドキュメントはコードの中にあるべきですが、しかし、特定のものはコードの外でドキュメント化されるべきです。

コーディング資料を得るために、デザイナーにコードを調べさせるのは、時間の無駄です。ドキュメント化されたコーディング資料は、開発効率を向上させます。(ドキュメントが最新の状態を保持できれば)

ドキュメントがフォローするもの:

  • タグの使用
  • レイヤーの用途(collision, culling, raycasting など、どのレイヤーに入れるか)
  • レイヤーの GUI の深さ(なにをどの上に表示していくか)
  • 複雑な Prefab の構造
  • イディオムの設定
  • ビルドの方法

わりと Unity の設定(セットアップ)に関する技術資料を指している気がします。

旧 Tips 49 と同じ内容だと思います。



一般的なコーディングについて (General Coding)

9. すべてのコードを名前空間に入れること

これにより、独自のライブラリーとサードパーティのコートとの衝突を避けることができます。しかし、重要なクラスとの衝突を避ける目的で名前空間を頼らないこと。異なる名前空間を使用する場合であっても、クラスの名前を「Object」、「Action」、「Event」のようにしないでください。

後ろはリーダブルコードのような内容の指摘だと思います。


10. Assert を使うこと

アサーションは、コードの実行結果が変わらないことをテストしたり、ロジックのバグを洗い出したりするのに便利な機能です。

Unity では「Unity.Assertions.Assert」を利用できます。これらはなんらかの条件のテストをして、条件を満たさなかった場合、コンソールにエラーメッセージを表示します。

アサーション(というよりも、テスト)がどのように役に立つのかをよく知らない場合は、「The Benefits of programming with assertions(リンク切れ)」を参照してください。


11. 表示されるテキスト以外の文字列は使用しない

特に、オブジェクトや Prefab の識別子としてテキストをそのまま使わないこと。例外もあります。(Unity では名前でしかアクセスできないものが、まだいくつかあります)そのような場合は、 AnimationNames や AudioModuleNames のようにファイルで文字列を定数として定義してください。これらのクラスが管理しきれなくなった場合は、入れ子になったクラスを使用して、AnimationNames.Player.Run のように定義します。

旧 Tips 34 と同じ内容だと思います。


12. Invoke と SendMessage を使わないこと

MonoBehaviour のこれらのメソッドは、名前をつけて他のメソッドを呼び出します。テキストの名前で呼び出されるメソッドはコードで追跡するのが難しいです。(Usages を見つけることができないし、SendMessage は範囲が広いので、さらに追跡できない)

コルーチンや C# のアクションを使って、独自の Invoke を簡単に用意できます:

public static Coroutine Invoke(this MonoBehaviour monoBehaviour, Action action, float time)
{
   return monoBehaviour.StartCoroutine(InvokeImpl(action, time));
}

private static IEnumerator InvokeImpl(Action action, float time)
{
   yield return new WaitForSeconds(time);
   
   action();
}

MonoBehavior から、次のように使うことができます:

this.Invoke(ShootEnemy); //ShootEnemy メソッドの引数はなし (void)

独自に MonoBehaviour を継承する基底クラスを実装する場合は、そこに独自の Invoke メソッドを追加することができます。

旧 Tips 21 と同じ内容です。

より安全な SendMessage の代替を実装するのは、難しいです。その代わりとして、通常は GetComponent を使って、対象のコンポーネントを取得し、直接呼び出すようにします。

補足として、Unity の「ExecuteEvent」についての提案が挙がっています。今のところ、よく調べられていませんが調査する価値がありそうです。


13. ゲーム実行中に生成したオブジェクトがヒエラルキーをごちゃごちゃにしないこと

親オブジェクトをシーンオブジェクトに設定することで、ゲームの実行中にオブジェクトを見つけやすくします。

コードからのアクセスをしやすくするために、空の Game Object や、Behaviour を持たない Singleton クラスを使用することもできます。

こうしたオブジェクトのことを DynamicObjects と呼びます。

旧 Tips 28 と同じような内容です。


14. 正しい値として null を使用するときは具体的にして、可能な限りそれを避けること

null は不正なコードを検出するための役にたちます。しかし、もし、null を暗黙的に渡すことを習慣にしてしまうと、不正なコードは楽々実行されるようになり、また、そのバグに気づくのはかなり後になります。

さらに、それぞれのレイヤーが null の値を渡すことで、コードの深い部分でそれがはっきりすることもあります。私は、null を正しい値として使用することを完全に避けるようにしています。

私の好ましいイディオムは null の値のチェックを一切行わず、問題のあるところではコードを失敗させることです。より深いレベルのインターフェースとして機能するメソッドでは、値が null であるかどうかをチェックし、それが失敗する可能性のある他のメソッドに渡す代わりに例外を投げます。

場合によっては、正しい値が null になることがあり、別のやり方で対処する必要があります。このような場合は、コメントを追加して、いつ、なぜ、値が null になっているのかを説明するコメントを追加してください。

よくあるシナリオ(ケース)では、インスペクターが設定した値を使用することです。ユーザーは値を指定することができますが、ユーザーが指定しない場合はデフォルト値が使用されます。

これより好ましい方法は、T の値をラップする Optional<T> クラスを使用することです。(Nullable<T> とすこし似てる)

特別なプロパティのレンダラーを使って、チェックボックスレンダリングして、チェックが入っている場合にのみ、値を編集するボックスを表示するようにします。(残念ながら、ジェネリッククラスを直接使うことはできないので、T を特定する型にクラスを拡張しないとダメです)

[Serializable]
public class Optional<T>
{
   public bool useCustomValue;
   public T value;
}

コードでは次のように使用することができます:

health = healthMax.useCustomValue ? healthMax.Value : DefaultHealthMax;

補足として、これは多くのコメントで struct を使ったほうがよいという指摘がありました。ただし、これだと、非ジェネリッククラスのベースクラスとしては使えないということなので、実際にインスペクターで使えるフィールドに適します。


15. コルーチンを使うなら効率的な使い方を学ぶ

コルーチンは多くの問題を解決する強力な手段になりえます。しかし、デバッグが難しく、(自分を含めた)誰もが理解できないよくわからないコードを簡単につくることができます。

知っておくべきことは:

  • コルーチンを並列に実行する方法
  • コルーチンを連続して実行する方法
  • 既存のコルーチンから新しいコルーチンを作成する方法
  • CustomYieldInstruction を使ったカスタムコルーチンの作成方法
Enumerator RunInSequence()
{
   yield return StartCoroutine(Coroutine1());
   yield return StartCoroutine(Coroutine2());
}

public void RunInParallel()
{
   StartCoroutine(Coroutine1());
   StartCoroutine(Coroutine1());
}

Coroutine WaitASecond()
{
   return new WaitForSeconds(1);
} 


16. インターフェースを共有するコンポーネントを操作するときは拡張メソッドを利用する

今は、GetComponent はインターフェースも動作するようになったので、この Tips は冗長になっています。

特定のインターフェースを実装したコンポーネントを取得したり、そんなコンポーネントを持つオブジェクトを見つけるときに便利です。

以下の実装では、これの汎用的な実装に typeof を使用しています。ジェネリック版だと、インターフェースは動作しないけど、typeof は動作します。以下のメソッドは、これを通常のメソッドでラップしています。

public static TInterface GetInterfaceComponent<TInterface>(this Component thisComponent)
   where TInterface : class
{
   return thisComponent.GetComponent(typeof(TInterface)) as TInterface;
}


17. 拡張メソッドを使用して構文をより簡便にする

たとえば、つぎのようなもの:

public static class TransformExtensions 
{
   public static void SetX(this Transform transform, float x)
   {
      Vector3 newPosition = 
         new Vector3(x, transform.position.y, transform.position.z);
 
      transform.position = newPosition;
   }
   ...
}

旧 Tips 24 と同じような内容です。


18. 防御的な GetComponent の代替メソッドを使うこと

RequireComponent を使用してコンポーネントの依存関係を強制しても、他のクラスで GetComponent を呼び出す場合は、常に取得可能であるとは限らないし、望ましいことでもないです。RequireComponent を使用する場合であっても、コンポーネントを取得するコードの中で、コンポーネントが存在することを期待しているので、存在しない場合はエラーであることを示すとよいです。

エラーメッセージを表示するか、見つからなかった場合に役立つ例外をスローする拡張メソッドを用意します。

public static T GetRequiredComponent(this GameObject obj) where T : MonoBehaviour
{
   T component = obj.GetComponent();
 
   if(component == null)
   {
      Debug.LogError("Expected to find component of type " 
         + typeof(T) + " but found none", obj);
   }
 
   return component;
}

旧 Tips 25 と同じような内容です。


19. 同じことをするのに異なるイディオムを使うのは避ける

多くの場合、ひとつのものに複数のイディオム(慣用句)があります。そんな場合、プロジェクト全体で使う慣用句をひとつ選びましょう。その理由は:

  • イディオム同時はうまく働かない。ひとつのイディオムを使うと別のイディオムには適していない方向にデザインを強制されます。
  • 全体をとおして同じイディオムを使うことで、チームメンバーは、そこでなにをしているのか理解しやすくなります。構造・コードが理解しやすくなって、ミスをしづらくなります。

同じ意味のイディオムになるケースを挙げると:

  • Coroutine と State Machine
  • Nested Prefab と Lined Prefab と God Prefab
  • データを分離するための手法
  • 2D ゲームのスプライトの使用方法
  • Prefab の構造
  • スポーンするときのやり方
  • オブジェクト見つけるための方法:type, name, tag, layer, reference などいろんなやり方がある
  • オブジェクトをグループ化する方法:type, name, tag, layer, reference などいろんなやり方がある
  • 他のコンポーネントからメソッドを呼び出す方法(Tips 12 みたいなこと)
  • オブジェクトのグループを見つける方法とグループに登録する方法(Tips 13 みたいなこと)
  • 実行順序の制御
  • ゲーム内でのマウスによるオブジェクト・位置・ターゲットの選択
  • シーン変更の間にデータを保持する方法:PlayerPrefab を介して、または、新しいシーンがロードされたときに Destroy されないオブジェクト
  • アニメーションを組み合わせる方法
  • 入力のやり方

これだけ聞くと、半端に UniRx を使うのって微妙な気持ちになる気も。学習不足でコードをあまり追えない状態で利用すると、いくつかの Tips に反してしまうけど、前書きのリファクタリングに関する説明がそれを肯定する関係に思った。


20. ポーズを簡単にするために独自の時間クラスを準備する

ポーズとタイムスケール(遅くしたりする)をするために Time.DeltaTimeTime.TimeSinceLevelLoad をラップしておきます。これは厳格さを必要とするけど、特に異なるタイマーを管理している場合は物事を簡単にします。(インターフェースのアニメーションとゲームのアニメーション速度が別々になるときなど)

補足として、Unity は unscaledTimeunscaledDeltaTime をサポートしており、多くの状況で独自の時間クラスを持つことは冗長になります。Tips 21 のようなケースの場合はまだ便利です。

旧 Tips 20 と同じ内容だと思います。


21. 更新を必要とするカスタムクラスはグローバルな静的時間にアクセスしないこと

更新を必要とするカスタムクラスはグローバルな静的時間にアクセスするべきではないです。

その代わりに、Update メソッドのパラメーターに delta time をとる必要があります。上の Tips 20 で説明したように、ポーズの機能を実装する場合や、カスタムクラスの動作を高速化したり遅くしたりする場合に、このクラスを使用することができる。


22. WWW を使うときは、共通のやり方にする

サーバー通信の多いゲームでは、数十もの WWW のコールがあるものです。

Unity の用意した WWW クラスを使用する場合でも、プラグインを使用する場合でも、その上にひな形 (boiler plate) になるレイヤーを入れることで便利になります。

通常だと、Call メソッド(Get と Post をひとつずつ)、CallImpl コルーチン、MakeHandler を定義します。基本的に、Call メソッドは Make Handler メソッドを使用して、パーサー・成功時と失敗時のハンドラを構築します。また、CallImpl コルーチンを呼び出し、URL を入れて、呼び出し、完了するまで待機してからスーパーハンドラを呼び出します。

大まかにはこんな感じ:

public void Call<T>(string call, Func<string, T> parser, Action<T> onSuccess, Action<string> onFailure)
{
    var handler = MakeHandler(parser, onSuccess, onFailure);
    StartCoroutine(CallImpl(call, handler));
} 

public IEnumerator CallImpl<T>(string call, Action<T> handler)
{
    var www = new WWW(call);
    yield return www;
    handler(www);
}

public Action<WWW> MakeHandler<T>(Func<string, T> parser, Action<T> onSuccess, Action<string> onFailure)
{
   return (WWW www) =>
   {
      if(NoError(www)) 
      {
         var parsedResult = parser(www.text);
         onSuccess(parsedResult);
      }
      else
      {
         onFailure("error text");
      }
   }
}

これには、いくつかのメリットがあります。

  • 定型的なコードを書く必要がなくなります。
  • 特定のこと(読み込み中の UI コンポーネントの表示や、特定の一般的なエラー処理など)をまとめて処理することができます。

23. 文字数の多いテキストは、ファイルにする

インスペクターで編集するフィールドに入れないでください。

Unity エディターを開かなくても、シーンを保存しなくても、簡単に変更できるようにしておきましょう。

旧 Tips 38 と同じ内容だと思います。


24. ローカライズを計画しているなら、すべての文字列をひとつのところに配置します

このやりかたはたくさんあります。ひとつの方法は、各文字列のpublic string のフィールドを持つ Text クラスを定義して、デフォルトを英語に設定しておくことです。他の言語は、これを基底としたサブクラスにして、対応する言語でフィールドを再設定します。

より洗練されたテクニック(テキストの本文が長い or 多くの言語に対応する)は、スプレッドシートを読み込んで、選択した言語に基づいて正しい文字列を選択するロジックを提供することです。

旧 Tips 39 と同じ内容だと思います。


クラスのデザイン (Class Design)

25. インスペクターで操作できるフィールドの実装方法を決めて、標準化する

フィールドは、public にするか private にして「SerializeField」属性を与えるか2つの方法があります。

後者は「より正しい」方法ですが、便利ではありません。(Unity が普及させた方法でないことは確かです)どちらの方法を選ぶにしても、あなたのチームの開発者が public フィールドをどのように扱うのか知っているように、それを標準としてください。

  • インスペクターで操作できるフィールドが public のとき、そのフィールドは「実行時にデザイナーが変更しても安全であり、コードで値を設定しない」ことを意味します。
  • インスペクターで操作できるフィールドが private だけど SerializeFiled 属性を持つとき、public なフィールドは「コード中でこの変数を変更しても安全だ」という意味です。(なので、あまり多くは表示されないはずで、MonoBehaviourScriptable Object には public フィールドは存在しないはずです)


26. コンポーネントはインスペクターで調整するべきではない変数を決して公開しないこと

そうしないと、そのパラメーターがなにをするのか明確でない場合、デザイナーによって調整されてしまいます。レアケースとして、それが避けることができない場合があります。(たとえば、エディタースクリプトが取得する必要がある場合など)その場合は、HideInInspector 属性を使ってインスペクターの中から隠すことができます。

旧 Tips 30 と同じ内容だと思います。


27. インスペクターの独自 (Propery drawers) を利用して、フィールドをより使いやすくすること

インスペクターの独自 (Propery drawers) は、インスペクターのコントロールをカスタマイズするために使用することができます。

これによって、データの性質にあわせたコントロールを作成したり、特定のセーフガードを設置したりできます。(Range のような範囲を設定するなど)Header 属性を利用してフィールドを整理したり、ToolTips 属性を利用して追加のドキュメント(あんちょこ)を提供します。

例に挙がっているとおり、属性でインスペクターに追加するフィールドをわかりやすくすることだと思います。ただ、単純に属性を追加するカスタムエディターと違う点だけフォローする。


28. カスタムエディターよりも独自の (Propery drawers) を優先すること

独自の (Propery drawers) はフィールドタイプごとに実装されているため、実装の手間が大幅に軽減できます。また、再利用性も高いので、ある型のために一度実装すれば、どのクラスでも再適用できます。

カスタムエディターは MonoBehaviour ごとに実装されるため、再利用性が低く、実装の手間がかかります。


29. デフォルトでは MonoBehaviour を seal すること

一般的には、Unity の MonoBehaviour は継承にフレンドリーではありません:

  • Unity が Start() や Update() などのメソッドを呼び出す方法は、サブクラスからこれらのメソッドを扱うのは面倒です。注意しないと、間違ったものが呼ばれたり、ベースになるメソッドの呼び出しを忘れてしまったりします。
  • カスタムエディターを使う場合、通常はエディターの継承構造を複製している必要があります。クラスのひとつを拡張したいなら、独自のエディターを提供するか、提供されているものを利用します。

継承が必要な場合は、避けられる場合は Unity のメソッド(Start や Update)を提供しないようにしてください。もしも、それらを提供するなら仮想化してはいけません。必要であれば、メソッドから呼び出される空の virtual 関数を定義して、子クラスが override して追加の作業をすること。

public class MyBaseClass
{
   public sealed void Update()
   {
      CustomUpdate();
   }

   virtual public void CustomUpdate(){};
}

public class Child : MyBaseClass
{
   override public void CustomUpdate()
   {

   }
}

これによって、誤って override することを防ぎますが、それでも Unity のメッセージにフックされることがある。このパターンがよくと思わない理由のひとつは、物事の順序が問題になることです。上の例では、クラスが自身の更新をした後に、子クラスで更新があるかもしれません。

最後は、整合性がとれないという指摘だと思います。


30. インターフェースをゲームロジックから分離すること

インターフェースのコンポーネントは、通常だと使用されるゲームについて何も知らないはずです。 認識するために必要なデータを与え、イベントを購読して、ユーザーがそれらと相互に作用したときにわかるようにしておきます。

インターフェースのコンポーネントは、ゲームロジックを行うべきではないです。入力をフィルタリングして有効であることを確認することはできるけど、主なルールチェックは別のところですべきです。

多くのパズルゲームでは、コマはインターフェースの延長線上にあって、ルールを含むべきではありません。(例えば、チェスの駒は駒自体が次の手を計算してはいけない)

同様に、入力はその入力に基づいて動作するロジックから切り離されるべきです。入力コントローラーを使用して、手を動かす意図をだけをアクターに通知します。(コントローラーの操作自体ではない)

ユーザーがリストの中から武器を選択する UI コンポーネントを例にします。これらのクラスがゲームについて知っていることは Weapon クラスだけです。(Weapon クラスは、このコンテナに表示するデータ自身です)逆にゲームはコンテナーについて、何も知りません。

public WeaponSelector : MonoBehaviour
{
   public event Action OnWeaponSelect {add; remove; } 

   public void OnInit(List  weapons)
   {
      foreach(var weapon in weapons)
      {

          var button = ... //Instantiates a child button and add it to the hierarchy
 
          buttonOnInit(weapon, () => OnSelect(weapon)); 
          // child button displays the option, 
          // and sends a click-back to this component
      }
   }
   public void OnSelect(Weapon weapon)
  {
      if(OnWepaonSelect != null) OnWeponSelect(weapon);
   }
}

public class WeaponButton : MonoBehaviour
{
    private Action<> onClick;

    public void OnInit(Weapon weapon, Action onClick)
    {
        ... //set the sprite and text from weapon

        this.onClick = onClick;
    }

    public void OnClick() //Link this method in as the OnClick of the UI Button component
    {
       Assert.IsTrue(onClick != null);  //Should not happen

       onClick();
    }    
}

旧 Tips 31 と同じ内容だと思います。個人的には、GUI クラスをすべてクリアしたとしても、そのゲームはコンパイルできるべきだ、という文言が好きでした。疎結合感が伝わりやすい。


31. コンフィグ・ステート・記録を分離すること

  • コンフィグに関する変数
    これは、インスペクターで微調整される変数で、プロパティを通してオブジェクトを定義します。たとえば、maxHealth のような値です。
  • ステート(状態)に関する変数
    これは、オブジェクトの現在の状態を決定する変数で、ゲームがセーブをサポートしているならセーブする必要がある変数です。たとえば、currentHealth のような値です。
  • 記録 (Bookkeeping) に関数変数
    これは、スピード・利便性・移り変わる状態のために使用する変数です。それらはステートに関する変数から決定することができます。たとえば、previousHealth のような変数です。

これらのタイプの変数を分離することで、何を変更できるのか、何を保存する必要があるのか、何をネットワーク経由で送受信する必要があるのかを容易に知ることができ、(変数の管理に対して)ある程度の強制力を持つことができるようになります。単純な例を示します。

public class Player
{
   [Serializable]
   public class PlayerConfigurationData
   {
      public float maxHealth;
   }

   [Serializable]
   public class PlayerStateData
   {
      public float health;
   }

   public PlayerConfigurationData configuration;
   private PlayerState stateData;

   //book keeping
   private float previousHealth;

   public float Health
   {
      public get { return stateData.health; }
      private set { stateData.health = value; }
   }
}

旧 Tips 32 と同じ内容かもしれません。サンプルを見る限りだと Bookkeeping は Memento に近いようにも思いました。


32. インデックスで関連づけた public 配列の使用を避けること

たとえば、武器の配列、弾丸の配列、パーティクルの配列を定義してはいけません。

public void SelectWeapon(int index)
{ 
   currentWeaponIndex = index;
   Player.SwitchWeapon(weapons[currentWeapon]);
}
 
public void Shoot()
{
   Fire(bullets[currentWeapon]);
   FireParticles(particles[currentWeapon]);
}

この場合、コード中でやるというより、インスペクターで間違わないように設定するべきです。いっそのこと、3つの変数をカプセル化したクラスを定義して、それを配列にします。

[Serializable]
public class Weapon
{
   public GameObject prefab;
   public ParticleSystem particles;
   public Bullet bullet;
}

コードがすっきりしているように見えるけど、(大切なのは)インスペクターでデータを設定する際にミスをしづらくなります。

旧 Tips 35 と同じ内容だと思います。


33. シーケンス以外のデータ(連続性のないデータ)に配列を使用することを避けるべき

たとえば、プレイヤーは、3つの攻撃タイプを持っているとします。それぞれ、今に装備している武器を使用するけど、異なる弾を生成して、異なる動作をします。

3つの弾丸を配列に入れて、このようなロジックを使いたくなるかもしれませんが、避けるべきです。

public void FireAttack()
{
   /// behaviour
   Fire(bullets[0]);
}
 
public void IceAttack()
{
   /// behaviour
   Fire(bullets[1]);
}
 
public void WindAttack()
{
   /// behaviour
   Fire(bullets[2]);
}

列挙型はコードの見栄えをよくしますが、インスペクターではできません。

public void WindAttack()
{
   /// behaviour
   Fire(bullets[WeaponType.Wind]);
}

別々の変数を使ったほうが、どの内容を入れるのかを示すのに役立つように、名前は別の変数を使ったほうがよいでしょう。クラスを使ってすっきりさせましょう。

[Serializable]
public class Bullets
{
   public Bullet fireBullet;
   public Bullet iceBullet;
   public Bullet windBullet;
}

補足すると、これは3つの属性(火・氷・風)以外のデータがないことを前提にしています。(拡張性に留意)

旧 Tips 36 の内容と同じだと思います。


34. インスペクターの中をすっきりさせるために、Serializable なクラスにしてデータをグループ化すること

いくつかのエンティティに、微調節可能な変数がたくさんあるとき、インスペクターで適切な変数を調節するのはひどい悪夢みたいな作業になるかもしれません。これを簡単にするためには、次の手順に従ってみてください:

  • 変数のグループに対して個別のクラスを定義する
  • それらを public なクラスにして、Serializable 属性を付与する
  • プライマリークラス(項1,2のクラスを含むクラス)では、定義されたそれぞれのクラスを public 変数で定義する
  • これらの変数はシリアライズ可能なので Awake() や Start() で初期化しない
  • (コード中の)定義に値を代入することで、これまでと同じようにデフォルト値を指定することができる

これによってインスペクター内で折りたたみができるようになるので、変数のグループを扱いやすくなる。

[Serializable]
public class MovementProperties //Not a MonoBehaviour!
{
   public float movementSpeed;
   public float turnSpeed = 1; //default provided
}
 
public class HealthProperties //Not a MonoBehaviour!
{
   public float maxHealth;
   public float regenerationRate;
}
 
public class Player : MonoBehaviour
{
   public MovementProperties movementProeprties;
   public HealthPorperties healthProeprties;
}

旧 Tips 37 と同じ内容だと思います。


35. public フィールドを使用しておらず MonoBehaviour ではないクラスはシリアライズ可能なクラスにすること

インスペクターがデバッグモードになっているときに、インスペクターでクラスのフィールドを確認することができます。これは入れ子になっているクラス (private or public) でも動作します。


36. インスペクターの微調整できる変数は、コード内で変更を加えないようにすること

インスペクターで調整可能な変数は設定変数であるため、実行時定数として扱われ、状態に関する変数として二重に扱うべきではないです。

このルールに従うことで、コンポーネントの状態を初期状態にリセットするメソッドが書きやすくなり、変数はなにをするのか明確になります。

public class Actor : MonoBehaviour
{
   public float initialHealth = 100;
   
   private float currentHealth;

   public void Start()
   {
      ResetState();
   }   

   private void Respawn()
   {
      ResetState();
   } 

   private void ResetState()
   {
      currentHealth = initialHealth;
   }
}



パターン (Patterns)

パターンとは、頻繁に発生する問題を標準的な方法で解決する方法のことです。

Bob Nystrom氏の著書「Game Programming Patterns」(オンライン上で無料で読める)は、ゲームプログラミングで発生する問題にパータンがどのように適用されるのかを知るために便利な資料です。

Unity もこれらのパターンの多くを使っています。Instantiate() は prototype パターンの一例だし、MonoBehaviour は template パターンに従っているし、UI とアニメーションは observer パターンを使っているし、新しいアニメーションのエンジンは state machine パターンを使用しています。

これら(ここで)の Tips は、特に Unity で使用するパターンを紹介します。


37. 便利に singleton を使うこと

つぎのクラスは、継承しているクラスを自動的に singleton にします。

public class Singleton<T> : MonoBehaviour where T : MonoBehaviour
{
   protected static T instance;
 
   //Returns the instance of this singleton.
   public static T Instance
   {
      get
      {
         if(instance == null)
         {
            instance = (T) FindObjectOfType(typeof(T));
 
            if (instance == null)
            {
               Debug.LogError("An instance of " + typeof(T) + 
                  " is needed in the scene, but there is none.");
            }
         }
 
         return instance;
      }
   }
}

singleton は ParticleManager、AudioManager、GUIManager などのようなマネージャー(クラス)のために役にたちます。

多くのプログラマーは、クラスの名前を XManager のように曖昧なものにすることに対して警告していますが、それは名前のつけ方が悪かったか、関連性のないタスクが多すぎるクラスになっていることを示しています。一般的には、私もこれに同意します。しかし、どんなゲームにもちょっとはマネージャークラスが存在していて、どんなゲームでも同じことをしていて、これらのクラスは実際のところイディオムです。

  • マネージャーではない Prefab のユニークなインスタンスに singleton を利用することは避けてください。(Player など)
    この原則に従わないと、継承が複雑になって、変更が難しくなります。マネージャーで管理したいなら、GameManager のようなもの(適切なマネージャークラス)にこれらの参照を追加してください。
  • クラスの外から頻繁に使用されるように(しやすいように)、static プロパティ、メソッドを定義します。こうすることで、GameMAnager.Instance.Player とする代わりに GameManager.Player と書くことができる。

他の Tips で説明したように、singleton は、オブジェクトのデフォルト生成位置を作成したり、シーンやロードの間を超えて永続しているオブジェクトを持つのにも便利です。

旧 Tips 29 と同じ内容だと思います。


38. state machine(パターン)を使用して、複数の振る舞い (state) をつくり、状態遷移をしてコードを実行すること

簡易な state machine はいくつかの state を持ち、それぞれの state に対して、その state に入る・存在しているときに実行・更新するアクションが決まります。

これによって、コードを綺麗な状態にして、エラーを発生しづらくすることができます。state machine の恩恵を受けることができる兆候は、Update() メソッドのコードに if 文や switch 文があってそれらがなにかを変更する場合や、hasShownGameOverMessage のような変数がある場合です。

public void Update()
{
   if(health <= 0)
   {
      if(!hasShownGameOverMessage) 
      {
         ShowGameOverMessage();
         hasShownGameOverMessage = true; //Respawning resets this to false
      }
   }
   else
   {
      HandleInput();
   }
}

state の数が増えてくると、この問題は非常にやっかいになります。state machine パターンを適用することで、よりすっきりしたコードにすることができます。


39. UnityEvent 型のフィールドを使用して、インスペクターに observer パターンを使えるようにしておくこと

UnityEvent クラスを使用すると、インスペクターで、ボタンのイベントと同じ UI インターフェースを使用して、(最大4つの引数を使える)メソッドをリンクすることができます。この機能は、特に入力を扱う場合に便利です。


40. observer パターンを使って、フィールドの値が変化したタイミングを検出する

変数の値が変化したときだけ、コードを実行したいというケースは、ゲームの中で頻繁に発生します。この問題の一般的な解決策として、値が変わるたびにイベント登録できる汎用クラスを準備します。

ここでは health を例に示します。以下のようになります:

/*ObservedValue*/ health = new ObservedValue(100);
health.OnValueChanged += () => { if(health.Value <= 0) Die(); };

これで、例えばこんな感じで、チェックする場所ごとにやらなくても、どこでもチェックできるように(通知してくれるように)なりました。

if(hit) health.Value -= 10;

health の値が0以下になれば Die() メソッドが呼び出されます。これについて、さらなる議論・実装等については、こちらの「記事」を参照してください。

訳をした「記事」をあげました。


41. Prefab に actor パターンを使用する

このパターンは標準的なパターンではありません。基本的な考え方は、Kieran Load の「プレゼンテーション」からのものになります。

actor とは、Prefab のメインコンポーネントであり、通常は Prefab の「アイデンティティ」を決めるコンポーネントであり、上位レベルのコードが最も頻繁に相互作用するものになります。actor は、同じオブジェクト(子オブジェクト上)から他のコンポーネントに「ヘルパー」を使って作用します。

Unity でメニューからボタンオブジェクトを作成すると、Sprite と Button コンポーネントを持つ Game Object が生成されます。(Text コンポーネントを持つ子要素も生成されます)この場合、ボタンは actor コンポーネントです。

同様に、メインカメラも通常は Camera コンポーネントが付属しているだけではなくて、いくつかのコンポーネントGUI and Flare layer、Audio Listener)が付属しています。Camera も actor です。

actor が正しく動作するためには、他のコンポーネントが必要になる場合があります。actor コンポーネントに以下の属性を与えることで、Prefab をより堅牢で有用なものにすることができます。

  • RequiredComponent を使用して、actor が同じゲームオブジェクト上で必要とするすべてのコンポーネントを指定します。(これによって、actor は常に安全に GetComponent を呼び出すことができ、返された値が null かどうかをチェックしなくてもよくなります)
  • 複数の同じコンポーネントがアタッチされるのを防ぐためには、DisallowMultipleComponent を使用します。これによって、同じコンポーネントが複数存在しないことになるので、actor は常に GetComponent を呼び出すことができます。
  • actor オブジェクトに子要素がある場合は、SelectionBase を使用します。これによって、シーンビューでの選択がやりやすくなります。
[RequiredComponent(typeof(HelperComponent))]
[DisallowMultipleComponent]
[SelectionBase]
public class Actor : MonoBehaviour
{
   ...//
}


42. ランダムなものとパターンのあるデータストリームには generator を使用すること

これは標準的なパターンではないけれど、非常に有用であることがわかりました。

generator は random generator に似ています。これは、Next() メソッドを持つオブジェクトで、特定の方の新しいアイテムを取得するために呼び出すことができます。

generator はそのアイテムを構築する間に操作して、多様なパターン、異なるタイプのアイテムとして生成することができます。

generator は、新しいアイテムを生成するロジックをアイテムが必要な場所と別のところに(generator 自身のロジックとして)配置することができるので、コードをよりすっきりさせることができるので、便利です。

いくつかの例を挙げます:

var generator = Generator
   .RamdomUniformInt(500)
   .Select(x => 2*x); //Generates random even numbers between 0 and 998
 
var generator = Generator
   .RandomUniformInt(1000)
   .Where(n => n % 2 == 0); //Same as above
 
var generator = Generator
    .Iterate(0, 0, (m, n) => m + n); //Fibonacci numbers
 
var generator = Generator
   .RandomUniformInt(2)
   .Select(n => 2*n - 1)
   .Aggregate((m, n) => m + n); //Random walk using steps of 1 or -1 one randomly
 
var generator = Generator
   .Iterate(0, Generator.RandomUniformInt(4), (m, n) => m + n - 1)
   .Where(n >= 0); //A random sequence that increases on average

私たちは、障害物の生成、背景色の変更、手続き的な音楽、単語ゲームの単語の文字列生成などに generator を使用してきました。また、generator の機能は、一定ではない間隔で繰り返すコルーチンを制御することもうまく機能します。

while (true)
{
   //Do stuff
   
   yield return new WaitForSeconds(timeIntervalGenerator.Next());
}

generator について詳しく知りたいなら、これちらの「記事」を確認ください。



Prefab と Scriptable object

43. すべてのものに Prefab を使うこと

シーンの中で Prefab(または Prefab の一部)であってはならないゲームオブジェクトはフォルダーだけです。

一度しか使用されないユニークなオブジェクトであっても、Prefab にするべきです。これによって、シーンを変更せずに(オブジェクトの)変更を簡単にすることができます。

旧 Tips 16 と同じ内容だと思います。


44. Prefab を Prefab にリンクし、インスタンスインスタンスにリンクしないこと

Prefab へのリンクは、Prefab をシーンにドロップしたときに維持されます。インスタンスへのリンクは維持されません。シーンへの設定を減らして、Prefab にリンクさせることで、シーンを変更する必要を減らすことができます。

可能な限り、インスタンス間のリンクは自動的に確立します。もし、インスタンス間にリンクの必要がないなら、プログラム(コーディング)からリンクを確立します。たとえば、Player の Prefab は GameManager が起動したときに、自身を登録したり、インスタンスを見つけて登録すること。

旧 Tips 18 と同じ内容だと思います。


45. なにかスクリプトを追加したいとき、Prefab のルートにメッシュを配置しないこと

メッシュから Prefab を作るときは、最初にメッシュを空の game object の親として、それをルートにします。

スクリプトはメッシュのノードではなく、ルートに配置します。そうすれば、インスペクターで設定を見失うことなく、メッシュを別のメッシュに置換することもやりやすくなります。

旧 Tips 19 と同じ内容だと思います。


46. 共有するコンフィグ(設定)データには、Prefab の代わりに Scriptable object を使用すること

そうするとこうなります:

  • シーンが小さくなる
  • (Prefab のインスタンス上の)ひとつのシーンに対して誤って変更を加えることができない


47. レベルを表すデータには、 ScriptableObject を使用すること

レベルを表すデータには、XMLJSON で保存されることが多いですが、代わりに ScriptableObject を利用すると、いくつかの利点があります:

  • エディターで編集できる。これによって、データの検証が楽になって、技術的な知識のないデザイナーにもフレンドリーになる。さらに、カスタムエディターを使っておくことで、編集がもっと楽になる。
  • データの read/write や解析を気にする必要がなくなる。
  • 分割したり、ネストしたり、結果として得られる Assets の管理が楽になるので、大きなコンフィグからレベルを構成するのではなく、積み木 (building block) からレベルを構成します。


48. ScriptableObject を利用して、インスペクターの中で振る舞いを設定すること

Scriptable Object は通常、データを設定することに使いますが、「メソッド」をデータのようにして設定することもできます。

Enemy という型があって、それぞれの Enemy がいくつも SuperPower というデータを持っているシナリオを検討してみます。

これらの基礎になるクラスを作成して、Enemy クラスの中にそれらのデータをリストに持たせることができます……が、カスタムエディターでなければ、異なる SuperPower(それぞれ独自のプロパティを持つ)リストをインスペクターの中で設定することができません。

しかし、SuperPower をアセット(ScriptableObject として実装すれば)、(設定を)できるようになります。

こんな感じになります:

public class Enemy : MonoBehaviour
{
   public SuperPower superPowers;

   public UseRandomPower()
   {
       superPowers.RandomItem().UsePower(this);
   }
}

public class BasePower : ScriptableObject
{
   virtual void UsePower(Enemy self)
   {
   }
}

[CreateAssetMenu("BlowFire", "Blow Fire")
public class BlowFire : SuperPower
{
   public strength;
   override public void UsePower(Enemy self)
   {
      ///program blowing fire here
   }
}

このパターンに従う際は、いくつか注意するべきことがあります:

  • ScriptableObject を抽象化することはできません。代わりに、具体的な base クラスを使って、抽象化するべきメソッドは NotImplementedException の例外を投げるようにします。また、abstract を定義して、抽象化するべきクラスやメソッドにマークをつけることもできます。
  • ScriptableObject はシリアライズできません。しかし、ジェネリックな base クラスを使用して、ジェネリックを指定したサブクラスのみをすべてシリアライズすることができます。


49. 特殊化した Prefab に ScriptableObject を使用すること

もしも、2つのオブジェクトの構成が一部のプロパティにだけ違いがあるなら、シーンの中に2つのインスタンスを配置して、インスタンスの上でそれらのプロパティを調整するのが普通です。

大抵は異なる2つのプロパティを持つオブジェクト同士を、別々の ScriptableObject として分けたほうがよいです。

(そうすると)、柔軟さを得ます:

  • 特殊化したクラスの継承を使用して、型の異なるオブジェクトに特定のプロパティを与えることができます。
  • シーンの設計はより安全になります。(オブジェクトを目的の型にするために、すべてのプロパティを調整しないし、適切な ScriptableObject を選択するだけになります)
  • コードを通じて実行時にこれらのオブジェクトを操作するのが、もっと楽になります。
  • 2つの型のインスタンスを複数持っているなら、変更を加えたときにそれらのプロパティが常に等価(一貫してるもの)であることがわかります。
  • コンフィグの変数のセットを、混ぜたり・合わせたりできるセットに分けることができます。

設定の簡単な例を紹介します:

[CreateAssetMenu("HealthProperties.asset", "Health Properties")]
public class HealthProperties : ScriptableObject
{
   public float maxHealth;
   public float resotrationRate;
}

public class Actor : MonoBehaviour
{
   public HealthProperties healthProperties;
}

特殊化したものの数が多いときは、特殊化したものを通常のクラスとして定義して、そのリストを ScriptableObject の中で用いて、それを取得できる適切な場所(GameManager クラスなど)にリンクをしておくとよいです。

安全に、速く、便利にするためにはもうすこし貼り付ける(馴染ませる)必要があります。以下に小さな例を示します:

public enum ActorType
{
   Vampire, Wherewolf
}

[Serializable]
public class HealthProperties
{
   public ActorType type;
   public float maxHealth;
   public float resotrationRate;
}

[CreateAssetMenu("ActorSpecialization.asset", "Actor Specialization")]
public class ActorSpecialization : ScriptableObject
{
   public List healthProperties;

   public this[ActorType]
   {
       get { return healthProperties.First(p => p.type == type); } //Unsafe version!
   }
}

public class GameManager : Singleton
{
   public ActorSpecialization actorSpecialization;

   ...
}

public class Actor : MonoBehaviour
{
   public ActorType type;
   public float health;

   //Example usage
   public Regenerate()
   {
      health 
         += GameManager.Instance.actorSpecialization[type].resotrationRate;
   }
}

この内容は最近 Unity Blog に載っていたものと近い気がしました。まとめたのが「これ」。インスペクターでの設定機能を強化することを好む Tips が多い印象です。


50. CreateAssetMenu の属性を利用して、自動的に ScriptableObject の作成をするメニューを追加すること

本文はありません。タイトルだけで全部です。Project ビューを右クリックして ScriptableObject を追加できるようにすることだと思います。



デバッグ (Debugging)

51. Unity のデバッグ機能を有効に使う方法を学ぶこと

  • Debug.Log() ステートメントに context object を追加しておけば、どこから(ログが)生成されたのかを確認することができます。
  • Debug.Break() を使用すればエディターからゲームを一時停止することができます。(たとえば、エラーになる状態が発生したとき、エラーが発生したフレームにおけるコンポーネントのプロパティを調べたりする際に便利です)
  • ビジュアルのよいデバッグには、Debug.DrawRay()Debug.DrawLine() 関数を使用します。(たとえば、Debug.DrawRay() は、レイキャストがヒットしない原因をデバッグする際にとても便利です)
  • ビジュアルのよいデバッグのために、Gizmos を利用します。DrawGizmo の属性を付与すると、MonoBerhaviour の他に Gizmo の機能による(デバッグ画面への)描画を提供することもできます。
  • インスペクターの画面にあるデバッグ機能を使用します(インスペクターを使用すると、Unity 実行時に private なフィールドの値を確認することができます)


52. IDE にあるデバッガー機能を有効に使う方法を学ぶこと

Debugging Unity games in Visual Studio」のサンプルを確認してください。


53. 時間の経過で変わる値のグラフを描画する知覚的なデバッガーを使用すること

これは、物理・アニメーション・その他の動的なプロセス・特に散発的な不具合のデバッグにとても役立ちます。グラフの中に不具合を見て取ることができるし、他の変数がどのように変化しているのか確認することができます。

また、目視によるチェックでは、よく変化する値や、原因がわからないけど値が狂う値など、いくつかのタイプの奇妙な挙動をするバグも明らかになります。

私たちは、Monitor Component を利用していますが、他にもいろいろあります。


54. 改善されたコンソールロギングを使用すること

カテゴリーに応じて出力するログを色分けしておけば、このカテゴリーに応じて出力するログをフィルタリングできるエディター拡張機能を使用します。

私たちは、Editor Console Pro を使用していますが、これも他にもいろいろあります。


55. Unity のテストツールを使用して、特にアルゴリズムや数学を使うコードをテストすること

これの詳細は、「Unity Test Tools(リンク切れ)」のチュートリアルや「Unit testing at the speed of light with Unity Test Tools」を参照してください。


56. Unity のテストツールを使用して、「スクラッチパッド」のテストをすること

Unity のテストツールは、フォーマルなテストに適当なだけではありません。

シーンを実行していなくても、エディターで走らせることができる便利なスクラッチパッドのテストにも利用することができます。

正直なところ、私はこれが何を指摘しているのかよくわかりませんでした。「scratchpad」という名詞が強調されていますが、よくわかりません。unrealengine にも同じ名前のパネルがあるみたいです。意味的にはメモ用紙のつづり紙のような、ちょっとしたものくらいことかとも思いました。


57. スクリーンショットを撮影するためのショートカットを実装すること

多くのバグは目に見えるものなので、画像を撮影できるようにすれば報告しやすくなります。理想的なシステムは、連続したスクリーンショットが上書きされないように、Prefab の中でカウンターを維持しておくことです。

また、誤ってリポジトリーに(画像を)コミットしてしまわないように、画像はプロジェクトフォルダーの外に保存しておくこと。

旧 TIps 43 と同じ内容だと思います。


58. 重要な変数のスナップショットを記録するためのショートカットを実装すること

ゲーム中になにか予期しないことが起こったとき、チェックできる(に使える)情報を簡単に記録することができる。もちろんだけど、どの変数もゲームに依存しています。(なので)あなたは、あなたのゲームで発生するバグに引き回されます。たとえば、プレイヤーと敵の位置、AI のロジックなどが挙げられます。

これは、画像を記録するという意味ではないと思います。


59. テストを簡単にするためのデバッグオプションを実装すること

いくつか例を挙げると:

  • すべてのアイテムをアンロックする
  • 敵の(出現など)を無効化する
  • GUI を無効化する
  • プレイヤーを無敵にする
  • すべてのゲーム動作を無効化する

これらのデバッグオプションを誤ってコミットしないように注意してください。デバッグオプションの変更によって、他チームの開発者を混乱させる恐れがあります。

旧 Tips 45, 46 と同じ内容だと思います。RPGデバッグルームのような話だと思います。


60. デバッグ用のショートカットキーの定数を定義して、ひとつのところにまとめておくこと

デバッグに使うキーは、ゲーム入力のあまったところのように、普通だと一か所にありません。

ショートカットはキーとキーの衝突を避けるために、中央的な(一元的な)ところに定数を定義します。別の方法としては、デバッグの関数であるかどうかは関係なく、すべてのキーを一か所で処理することになります。(欠点は、このクラスのためにオブジェクトへの余分な参照が必要になるかもしれません)


61. Procredural Mesh を生成するときに、小さな sphere を頂点して描画・スポーンすること

これは triangle や UV をいじってメッシュを表示するまえに、メッシュが正しいサイズであること、正しいと想定したところにあることを確認するための役に立ちます。

これもよくわかりませんでした。なので、変な訳になっている恐れがあります。



パフォーマンス (Performance)

62. パフォーマンス上の理由からの設計・デザインに関する一般的なアドバイスには注意すること

  • このようなアドバイスには、神話であったりテストに裏打ちされていません。
  • 時にはテストに裏打ちされているかもしれないけれど、そのテストには欠陥があると思っておきましょう。
  • 時にはアドバイスは正しいテストに裏打ちされているかもしれないけれど、それは非現実的なものであったり、異なるコンテキストかもしれないです。(たとえば、list より array を使うほうが高速であることを示すのは簡単です。しかし、実際のゲームの中では、ほとんどの場合この違いはごく僅かなことです。同じように、テストをするときターゲットのデバイスと異なるものでやっているなら、その結果はあなたにとって意味のあるものではないかもしれないです)
  • アドバイスは正しい(かった)が、時代遅れかもしれないです。
  • 時にはそのアドバイスが適用することもあります。しかし、トレードオフがあります。完成した低速な(パフォーマンスが十分ではない)ゲームは、完成しなかったパフォーマンスのよいゲームよりもよいものでしょう。また、いきすぎた最適化は、完成を遅らせる可能性が高いトリッキーなコードを含んでいるかもしれません。

パフォーマンスのアドバイスは、以下にアウトラインしたプロセスを使って、実際の問題の発生源をより速く見つけるための手助けになります。


63. 開発の初期段階からターゲットとなるデバイスで定期的にテストすること

バイスによってパフォーマンスの特徴は大きく異なるので、驚かないように。問題は早くに知れば知るほど、より効率的に問題に対処することができます。


64. プロファイラーを効果的に使用することで、パフォーマンスの問題の原因を調べる方法を学ぶこと

  • もしプロファイリングに初めてさわるなら、「Introduction to the Profiler」を参照してください。
  • 独自にフレームを定義する方法(Profiler.BeginFrameProfiler.EndFrame を使う)を習得して、詳細な分析をする方法を説明します。
  • iOS 用のビルドイン プロファイラーなど、プラットフォーム固有のプロファイリングの使用方法を学びます。
  • ビルトインプレーヤーの「profile to file(リンク切れ)」とプロファイラーの「display the data(リンク切れ)」について学びます。


65. より正確なプロファイリングをするために、必要に応じてカスタムプロファイラを利用すること

時には Unity のプロファイラはなにが起こっているのかをわかりやすく表示することができないことがあります。プロファイルのフレームが不足していたり、詳細プロファイル (Deep Profile) によってゲームの速度が低下してテストが意味をなくすことがあります。

私たちはこのことに対して、独自のプロファイラーを使用しますが、Asset Store で他のプロファイラを見つけることができるはずです。


66. パフォーマンスを強化する影響を測定すること

パフォーマンスを上げるための変更をおこなったときは、本当に改善されたかどうかを確認するために測定します。もしも、測定できなかったり、測定に漏れがあった場合は元に戻します。


67. パフォーマンスのために読みにくいコードを書かないこと

もしそうしたコードを書くときは:

  • プログラムに問題があるなら、プロファイラでソースを特定し、変更のあとに改善点を測定して、その改善のメリットが保守性の損失と比較して十分だと判断した場合
  • 自分で自分がなにをしているのか分かっている場合



命名規則とフォルダーの構造

68. 文書化した命名規則とフォルダー構造に従うこと

一貫性のある命名規則とフォルダー構造は、データを見つけやすく、わかりやすくします。

独自の命名規則とフォルダー構造を作りたいと思っているだろうけど、一例は以下のようになります。


一般的な規則

  1. 命名する)モノが「なに」であるかを呼んでみる。単に鳥であったなら「bird」とするべきです。
  2. 発音できて、覚えることができる名前を選ぶ。もし、あなたがマヤ族のゲームを作るなら、あなたは「Quetzalcoatl(ケツァルコアトル) is Return」みたいな名前はやめよう。(発音できない、覚えれない)
  3. 一貫性があること。(なんらかの)名前を選択したとき、その名前を一貫してつかう。どこかで ButtonHolder としたら、別のところで ButtonContainer とするのはやめましょう。
  4. Pascal ケースの命名規則を適用すること。たとえば、ComplicatedVerySpecificObject のようにします。 (space), _, - は使用しないこと。(例外:最後に書いてある「オブジェクトに様子(アスペクト)の名前を付け加える」を参照しておく)
  5. バージョン番号や進行状況を表すような単語を使わないこと。(WIP, Final とか)
  6. 略語にしないこと。(DVamp@W は、DarkVampire@Walk とするべきです)
  7. デザインのドキュメントにある用語を使用すること。もしも、ドキュメントに死ぬアニメーションを「Die」としているなら、DarkVampire@Death ではなくて DarkVampire@Die とすること。
  8. 名前の語句は(左並びを)ふつうに保つこと。VampireDark ではなく DarkVampire がいい。ButtonPaused ではなく PauseButton がいい。たとえば、すべてのボタンが Button で始まらないほうが、インスペクターで一時停止を見つけやすくなります。逆の並びを好むのは視覚的にわかりやすいからだけど、名前はグループ化するためのものではないです。(フォルダーはそのためのものだけど)名前は、同じ種類のオブジェクトを区別して、それらを確実かつ迅速に見つけることができるようにするためのものです。
  9. 名前の中には、連続性を構成するものがあります。これらの名前には、PathNode0, PathNode1 のように数字を使用すること。必ずこれは1からではなく、0から開始すること。
  10. 連続性のないものには数字を使用しないこと。たとえば、Bird0, Bird1, Bird2 は、 Flamingo, Eagle, Swallow のようにします。
  11. 一時的なオブジェクトの名前の前に二重のアンダースコア __ をつけること。(例:__Player_Backup)


オブジェクトに様子(アスペクト)の名前を付け加える (Naming Different Aspects of the Same Thing)

オブジェクトの主要な「なに」を表す(コアな名前)と様子(アスペクト)を記述もるものの間には、アンダースコア _ を使うこと。たとえば、このような感じです:

  • GUI ボタンの状態:EnterButton_Active, EnterButton_InActive
  • テクスチャー:DarkVampire_Diffuse, DarkVampire_Normalmap
  • スカイボックス: JungleSky_Top, JungleSky_North
  • LOD グループ: DarkVampire_LOD0, DarkVampire_LOD1

この規則はアイテムの種類が異なるものを区別するために使用しないこと。たとえば、Rock_Small と Rock_Large は SmallRock と LargeRock にすべきです。


構造 (Structure)

あなたのシーン・プロジェクトフォルダー・スクリプトフォルダーの構成は、似たような構成にするべきです。ここでは、あなたが(こうした構成を)はじめるためのいくつかの簡単な例を挙げます。


フォルダーの構成

MyGame
   Helper
      Design 
      Scratchpad
   Materials
   Meshes
      Actors
         DarkVampire
         LightVampire
         ...
      Structures
         Buildings
         ...
      Props
         Plants
         ...
      ...
   Resources
      Actors
      Items
      ...
   Prefabs
      Actors
      Items
      ...
   Scenes
      Menus
      Levels
   Scripts
   Tests
   Textures
      UI
      Effects
      ...
   UI 
MyLibray
   ...
Plugins
SomeOtherAsset1
SomeOtherAsset2
...


シーンの構成

Main
Debug
Managers 
Cameras
Lights
UI
   Canvas
      HUD
      PauseMenu
      ...
World
   Ground
   Props
   Structures
   ...
Gameplay
   Actors
   Items
   ...
Dynamic Objects
Scripts Folder Structure
Debug
Gameplay
   Actors
   Items
   ...
Framework
Graphics
UI
...

旧 Tips 50 と同じ内容だと思います。


参考

50 Tips and Best Practices for Unity 2016 Edition

作って学べる Unity本格入門

作って学べる Unity本格入門

Unity アプリケーションのエントリーポイントを作る

f:id:shikaku_sh:20200910152454p:plain:w400

Unity のプログラムを作成していて、ゲーム全体を通して一番最初に実行される(通常の条件で最初に実行させる)メソッドを使いたいときがあると思います。

他のものでたとえると、C++C# の Main() メソッドのようなものです。WPF でも Application.Run() の OnStartup() があります。

なんらかの初期化をするときに使いたくなる、というのがわかりやすい。「Unity Mobile Notifications Package」を利用しているなら、一番最初にチャンネルを登録(生成)するなど、アプリケーションを開始したタイミングで1回だけ実行し、Loading 画面を必要としない(読み込みに時間がかからない)ようなものが該当するのだと思います。


RuntimeInitializeOnLoadMethodAttribute

今のところの推奨はこれみたいです。

RuntimeInitializeOnLoadMethod 属性は、引数のパラメーターを RuntimeInitializeLoadType.BeforeSceneLoad にすると MonoBehaviour クラスの継承クラスの持つ Awake メソッドより早く実行されます。

これは、MonoBehaviour を継承しないクラスでも実行されます。ポイントは、static メソッドに使う属性になります。

using UnityEngine;

public class App
{
    [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
    public static void EntryPoint()
    {
        Debug.Log("EntryPoint Called.");
    }
}

個人的に、(エントリーポイントを含んでいる)クラスの名前は App か Application が好きです。ファイルの並びでも上のほうにくるので、エントリーポイントが迷子になりません。


注意

RuntimeInitializeLoadType.BeforeSceneLoad という名前なので、シーンが切り替わるたびに呼び出されるようにも見えますが、そうではないようです。

f:id:shikaku_sh:20200911091436g:plain:h500
実行時だけ Log が出力される例

RuntimeInitializeOnLoadMethod 属性を付与したメソッドを1つだけにしておくことです。2つ以上になった場合、呼び出される順序を明示的に制御することができません。

Note: The execution order of methods marked [RuntimeInitializeOnLoadMethod] is not guaranteed.
Unity DOCUMENTATION - RuntimeInitializeOnLoadMethodAttribute

よって、通常は1つだけしか利用しないほうがよさそうです。(あとで、面倒がないの意味)


過去のログ

過去の情報を探すと、いろいろあって 2015 年ごろまでは、ちょうどよい回答がなかったようです。RuntimeInitializeOnLoadMethodAttribute メソッド自体は Unity 5 から追加されたようなので、それ以前の情報かどうかは区切りになっています。


参考

楽しく学ぶ Unity2D超入門講座

楽しく学ぶ Unity2D超入門講座

Unity ゲームエフェクト マスターガイド

Unity ゲームエフェクト マスターガイド

  • 作者:秋山 高廣
  • 発売日: 2019/07/19
  • メディア: 単行本(ソフトカバー)

Unity ダイアログをポップアップするときのエフェクト(拡大・透過)

Unity キャンバス内に Scroll View を使ったダイアログをポップアップ」の記事の続きです。

前回の記事で、ポップアップするダイアログをシンプルに作成しました。なので、応用としてエフェクトを加えてみます。エフェクトがなくても、おもしろいゲームはたくさんあるかと思います。エフェクトの要/不要の調整は難しいと思いますが、とりあえずやってみよう。

f:id:shikaku_sh:20200909112924g:plain
エフェクトを追加

こんな感じになりました。0.n 秒で終わるエフェクトです。パッと表示して、操作の邪魔をしないこと、背景つきのポップアップでしたが、画面遷移も伝わりやすくなるかと思います。

レトロなゲームであるほど、エフェクトはピンポイントに利用されました。また、繰り返し操作する Windows のフォルダー表示の UI 演出なんかを見ると、とてもさりげないエフェクトですが、小気味いい効果音のように機能していると思います。


拡大(スケール)エフェクトの追加

Unity のエフェクトは、よく UniRx が利用されていると思いましたが、とりあえず利用しませんでした。コードが UniRx 独自のものになってしまうので、これくらい基本的な内容なら普通に書くほうが(間口が広くて)いいかな、と思いました。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;

public class ScaleEffect : MonoBehaviour
{
    private GameObject _Target = null;
    private float _Current = 0.0F;
    private float _Progress = 0.0F;

    [SerializeField]
    public bool _IsEnabled = true;

    [Header("スケールの線形")]
    [SerializeField]
    private Vector3 _From = Vector3.one;

    [SerializeField]
    private Vector3 _To = Vector3.one;

    [Header("実行時間")]
    [SerializeField]
    private float _Duration = 1.0F;

    [SerializeField]
    private AnimationCurve _Curve = new AnimationCurve(new Keyframe(0, 0), new Keyframe(1, 1));

    [Header("イベント")]
    public UnityEvent Begin;

    public UnityEvent End;

    void Start()
    {
        // _IsEnabled = true;

        _Target = gameObject;
        _Current = 0.0F;
        _Progress = 0.0F;

        Begin?.Invoke();
    }

    void Update()
    {
        if (!_IsEnabled) return; // ガード節

        if (_Progress <= 1.00F) // 進行度の割合
        {
            _Current += Time.deltaTime;
            _Progress = _Current / _Duration;

            if (_Progress >= 1.00F)
            {
                _Progress = 1.00F;
                _IsEnabled = false;

                End?.Invoke();
            }
        }

        var curvePoint = _Curve.Evaluate(_Progress);

        _Target.transform.localScale = Vector3.Lerp(_From, _To, curvePoint);
    }
}

こんな感じになったので、これをポップアップさせる Prefab にコンポーネントとして追加します。

f:id:shikaku_sh:20200909113030p:plain:w600
詳細


透過度エフェクトの追加

コンポーネント単位で透過度を操作するには Canvas Group を作って、要素をまとめる必要があります。Canvas Group は、子要素の再描画の基本単位にもなるので、ポップアップの要素をまとめるのは、理にかなっているかと。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;

public class FadeEffect : MonoBehaviour
{
    private GameObject _Target = null;
    private CanvasGroup _CanvasGroup = null;

    private float _Current = 0.0F;
    private float _Progress = 0.0F;

    [SerializeField]
    public bool _IsEnabled = true;

    [Header("透過度の線形")]
    [SerializeField, Range(0, 1)]
    private float _From = 0;

    [SerializeField, Range(0, 1)]
    private float _To = 1;

    [Header("実行時間")]
    [SerializeField]
    private float _Duration = 1.0F;

    [SerializeField]
    private AnimationCurve _Curve = new AnimationCurve(new Keyframe(0, 0), new Keyframe(1, 1));

    [Header("イベント")]
    public UnityEvent Begin;

    public UnityEvent End;


    void Start()
    {
        // _IsEnabled = true;

        _Target = gameObject;
        _CanvasGroup = _Target.GetComponent<CanvasGroup>();

        _Current = 0.0F;
        _Progress = 0.0F;

        Begin?.Invoke();
    }

    void Update()
    {
        if (!_IsEnabled) return; // ガード節

        if (_Progress <= 1.00F) // 進行度の割合
        {
            _Current += Time.deltaTime;
            _Progress = _Current / _Duration;

            if (_Progress >= 1.00F)
            {
                _Progress = 1.00F;
                _IsEnabled = false;

                End?.Invoke();
            }
        }

        var curvePoint = _Curve.Evaluate(_Progress);

        _CanvasGroup.alpha = curvePoint;
    }
}

こんな感じで、Canvas Group のアルファ値を操作するのがポイントになります。

f:id:shikaku_sh:20200909113055p:plain:w600
詳細

実は、スケールのコードと(今のところ)ほぼ一緒です。2つのエフェクトを1つのコードにまとめると整理できるかもしれませんが、オブジェクト指向疎結合の観点からはマイナスかも。

ただ、重複するコードが余計になっていて気になるのも事実。なんらかで重複を防ぐこともできるでしょうが、リファクタリングにおける3度目の法則の範囲かと思いますのでパス。時間は有限。

「美しいコード」、「すばらしいエンジニアリングのプラクティス」といった道徳的理由だけでリファクタリングするのも考え物です。経済的に不利益となる恐れがあります。(詳細は、「既存のコードを安全に改善するリファクタリング(第二版)」)

ちなみに、今回のポップアップは最終的にこんな構成要素に。

f:id:shikaku_sh:20200909113603p:plain
コンポーネントの構成例


サンプル

GitHub に「unity-popup-practice」を公開しています。

f:id:shikaku_sh:20200908114936p:plain

参考

Effective C# 6.0/7.0

Effective C# 6.0/7.0

Unity キャンバス内に Scroll View を使ったダイアログをポップアップ

f:id:shikaku_sh:20200908114309g:plain:w500
ダイアログを表示するサンプルです

こんな感じで、Canvas 上のボタンを押下すると、画面全体に背景つきのダイアログ(ポップアップ)を表示するサンプルです。(エフェクトも省略しています)

ダイアログを表示するサンプルはちょこちょこ見つかるのですが、Scroll View を中に入れている例が思ったほど見つからなかったので作成してみました。

Padding の設定に思ったよりも時間がかかりました。コツ・癖のようなポイントがあったので一応記録しておきます。また、Prefab 化と、ボタン押下のイベントに関するやり方を省略しています。


ダイアログの構成

Hierarchy の Canvas 中に追加するコンポーネントの構成は次のようになります。

f:id:shikaku_sh:20200908114421p:plain:w600

Rect Transform の多くはアンカープリセットを親コンポーネントにあわせた設定にしています。ポップアップダイアログの基底コンポーネントは、追加する Canvas の大きさにあわせる必要があるので、当然 Stretch。あとのコンポーネントも親コンポーネントのサイズを基準にして相対的に配置したほうが楽だと思います。

f:id:shikaku_sh:20200908114455p:plain:w600 f:id:shikaku_sh:20200908114509p:plain:w600 f:id:shikaku_sh:20200908114540p:plain:w600 f:id:shikaku_sh:20200908114556p:plain:w600 f:id:shikaku_sh:20200908114640p:plain:w600

スクロールビューはパラメーターが多いけど、把握しておいたほうがよさそうです。マニュアルを参照します。

viewport を2つ用意してあるのは、Mask のマージン対策です。「How padding a scrollable text inside a Scroll View ?」を参考にしました。

f:id:shikaku_sh:20200908114705p:plain:w600

画像のデザインを別にすると、こんな感じがわりと基本的な設定になるのだと思います。


ダイアログの構成を考える

UI 演出と同じように、ポップアップするダイアログは次の2つに気をつけるほうがよさそうです。

  • ユーザーの知りたい情報を適切に表示できる機能を有すること
  • ゲームの世界観に一致するようなデザインを有すること

このあたりは以下を参考にするとよさそうです。

そんなわけでサンプルではダイアログの背景を画像で設定できるようにしました。世界観にあわせて透明度を与えたグレーやホワイト以外のデザインにも対応できたほうが具合よさそうです。

f:id:shikaku_sh:20200908114823p:plainf:id:shikaku_sh:20200908114826p:plainf:id:shikaku_sh:20200908114828p:plainf:id:shikaku_sh:20200908114831p:plain
使用した素材


スクリプト

表示するためのスクリプトはこんな感じにしました。

インスペクター上で設定した Canvas の上に、Resources に追加したポップアップ用のプレハブをロードして、インスタンスするパターンです。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class DialogCaller : MonoBehaviour
{
    private GameObject _DialogPrefab;

    [SerializeField]
    private GameObject _Canvas = null;

    // Start is called before the first frame update
    void Start()
    {
        _DialogPrefab = Resources.Load("PopupDialog") as GameObject;
    }

    public void PopupTest()
    {
        var instance = Instantiate<GameObject>(_DialogPrefab);

        instance.transform.SetParent(_Canvas.transform, false);
    }
}

逆にインスタンス化したコンポーネントを削除するスクリプトは次。

OK ボタンを押下すると、CloseTest を実行します。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PopupCloser : MonoBehaviour
{
    [SerializeField]
    private GameObject _PopupDialog;

    public void CloseTest(float delaySecond = 0.0F)
    {
        Destroy(_PopupDialog, delaySecond);
    }
}


サンプル

GitHub に「unity-popup-practice」を公開しています。

f:id:shikaku_sh:20200908114936p:plain
ポップアップ(ダイアログ)のサンプル

このあとは、ポップアップ時のエフェクトのテストを追加するかもしれません。繰り返し操作する UI は、パッと表示されるほうが好きですが、アニメーションでイージングさせてみるなど、抱く悪感情と相談しつつテストしようかと。
追記:2020年9月9日「Unity ダイアログをポップアップするときのエフェクト(拡大・透過)」の記事を書きました。


参考

Unity Ads を利用して広告表示するためのサンプル

Unity の広告表示

Unity のなかで手早く広告表示をするなら、Unity 公式の広告プラットフォームの「Unity Ads」が一番ラクみたいです。

そんなわけで、調べたことをメモ。

Unity Ads を紹介する記事はもうたくさんあるんですが、(基本部分はそれにお任せしつつ)実装をテストして気づいたポイントなんかを(図をいれたりして)書きました。


Unity DashBoard

Unity Ads を利用するためには、Unity DashBoard が使える必要があるので、まず使える状態にします。

で、Unity で Ads を利用する(テストする)プロジェクトを開きます。Ads を利用する上で重要なのは「Monetization」の「Placements」です。次の2つをメモします。

  • GameID
  • Placement ID

f:id:shikaku_sh:20200831103108p:plain:w600
メモが必要なテキスト
f:id:shikaku_sh:20200831103332p:plain:w600
Placement ID を選択すると詳細設定

GameID はみたままです。Placement ID は「video」や「rewardedVideo」のことみたいです。Placement ID は最初から「video」と「rewardedVideo」の2つが登録されていました。それぞれ、どういった広告にするのかパラメーターを設定できるのがこの画面ということみたいです。

「Ad Filters」は、広告に表示しないカテゴリーや、年齢制限を設定できるみたいです。具体的にこの広告は非表示、みたいなことはできないみたいですね。


Unity で Ads を利用する手続き

ツールバーから「Build Settings」から広告表示するプラットフォームに切り替えます。Unity で(今のところ)広告表示できるのは以下のプラットフォームのどちらかです。

f:id:shikaku_sh:20200831104135p:plain:w600
この設定も必要です

ツールバーから次の「Services」を選択して、インスペクターに「Services」を表示します。

Windows > General > Services

f:id:shikaku_sh:20200831102317p:plain
サービスを起動します

Settings

Settings を開いて、次の2つが設定されているか確認しておきます。

f:id:shikaku_sh:20200831102642p:plain:w600
ブラウザ上でも確認できます

必要があれば AGE DESIGNATION のチェックを有効にします。


有効化

Services に戻って、ADS を ON(有効化)します。ADS の設定に入るので、次を有効にします。

  • Enable built-in Ads extension
  • Enabled test mode
  • Advanced

Advanced の「Enabled built-in Ads extension」はチェックを入れておきます。


Ads のスクリプト

Android のプラットフォーム下で Unity Ads を表示するサンプルです。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Advertisements;

public class AdsController : MonoBehaviour
{
    [SerializeField]
    private bool IsTestMode = false;

    [SerializeField]
    private string PlacementID = "rewardedVideo";

    [Header("GameID")]
    [SerializeField]
    private int Android = 0;

    void Awake()
    {
        Advertisement.Initialize(Android.ToString(), IsTestMode);
    }

    public void ShowAds()
    {
        // 初期化に失敗していたら、再生しない
        if (!Advertisement.isInitialized) return;

        if (Advertisement.IsReady(PlacementID))
        {
            // 広告表示後のオプション設定
            var options = new ShowOptions
            {
                resultCallback = (result) =>
                {
                    switch (result)
                    {
                        case ShowResult.Finished: // 最後まで正常に再生
                            Debug.Log("The Ads was successfully shown.");
                            break;
                        case ShowResult.Skipped: // 途中でスキップされた
                            Debug.Log("The Ads was skipped.");
                            break;
                        case ShowResult.Failed: // 再生に失敗した
                            Debug.Log("The Ads failed to be shown.");
                            break;
                    }
                }
            };

            Advertisement.Show(PlacementID, options);
        }
    }
}

SerializeField の属性を設定しておいたので、Ads を利用するための各種パラメーターを設定できるようにしてあります。設定しておきましょう。

f:id:shikaku_sh:20200831103533p:plain:w600
こうしておくと便利かも?

補足として、AndroidiOS プラットフォームに両対応する場合は、次のような感じになると思います。

#if UNITY_ANDROID
            Advertisement.Initialize(AndroidID);
#elif UNITY_IOS
            Advertisement.Initialize(IosID);
#endif

広告をサポートするクラス Advertisement の初期化は Unity 5.2 以降は不要になったみたいですが、マニュアルで設定するとこんな感じかと。

ボタンを押下したら広告を流し、広告を見た報酬を与えるタイプだと ShowOptions にコールバックするメソッドを設定しておくとよい感じになります。


動かしてみる

こんな感じで表示されました。

f:id:shikaku_sh:20200831103608g:plain:w600
デバッグで動きを確認します

Android でも一応やってみました。APK ファイルを出力して、APK ファイルを直接(手動で)インストールしています。

f:id:shikaku_sh:20200831103719j:plain:h400f:id:shikaku_sh:20200831103723j:plain:h400
できた

おお。普通に広告が再生されてる。思ったよりも迷うことなくここまでこれた。


広告のよい使い方ノウハウ

どういうタイミングで表示するか、どういうようにすれば効果的かのようなものは、効率を最大化しようとすると、とても難しいのだと思いますが、基本的には次のようなことかと思います。

プライベートでゲーム開発するなら、収益化はあまり大切なことじゃないように思っていましたが、某所で今はわりと設定が簡単なんで導入しておいたほうがいい、というか、導入するコストがすごく小さいから、とりあえず設定しておくだけでも~なんだとか。

昔のゲーム開発と違ってブラウザ上の設定も結構大切みたいにも思いました。統合開発環境である Unity 内で全部のことを記述できないのですね。


参考

作って学べる Unity本格入門

作って学べる Unity本格入門