Unity開発で便利だったアセット・サービス紹介 & Unityでのプログラミングテクニック

  • 139
    Like
  • 0
    Comment

自己紹介

  • 名前:とりすーぷ(@toRisouP
  • 趣味でUnityでゲーム開発してます

今作ってるゲーム

  • タイトル「ハクレイフリーマーケット
  • 東方二次創作ゲーム
  • 3D対戦アクション
  • ネットワーク対戦対応(最大6人)
  • ユーザ管理機能もある

アイテムを自陣に入れたら点数になるゲームです
3.jpg


今回の内容

  • ハクレイフリーマーケットの開発を経て得た 知見やノウハウを雑にまとめます
  • 1人開発で得た知見なので、チーム開発にそのまま適用できないかも
    • かなりプログラマ視点よりの内容です
  • 書き殴りなので内容に脈略はありません
    • 気になるところだけかいつまんで読んでもらえれば

お品書き

大きく2つあります

  • おすすめアセット・サービス編
  • プログラミング編

おすすめアセット・サービス編


おすすめのアセット・サービス

  • 何か機能が必要になった時に利用した、アセットやサービスを紹介

1.ネトゲが作りたい


つかったもの:Photon Cloud


Photon

  • 日本ではGMOインターネットさんが提供しているネットワークエンジン
  • サーバ・クライアント型のネトゲを作成できるようになる
  • 中でも「Photon Cloud」はサーバをクラウドで提供してくれる
    • Unity向けSDKも公開されている(PUN)
    • 20同時接続までは無料

公式サイト


Unity + Photon Cloudでできること

  • 部屋を作って参加者を募集する
  • Transform、Animator、その他任意の情報をネットワーク越しに同期する
  • ネットワーク越しにメソッドを呼び出して実行する

ネトゲ制作に必要なことはだいたいできる


ただしネトゲが簡単に作れるとは言っていない


ネトゲ開発の難しさ

  • 考えなくてはいけない状態が増える
    • 同じオブジェクトが「ローカル」と「ネットワーク越しの相手の世界(リモート)」の両方に同時に存在する
    • ローカルとリモートの両方の世界での状態を考慮しながらオブジェクトを操作しないといけなくなる
  • 単純なことでも非同期処理だらけになる
  • デバッグが非常にしにくい
    • 通信時の微妙なタイミングで発生するバグとか再現がまず困難
  • 通信相手がいないとデバッグ・レベルデザインできない
    • 動作確認のために複数人のプレイヤが必要
    • ぼっち開発だとこれが一番ツライ
    • 生放送で人を集めるとかやるしかない

ゲーム開発初心者にネトゲ開発はオススメできない

  • Photonがサポートしてくれるのはあくまで通信の低レイヤな部分のみで、アプリケーションは結局自分で実装しなくてはいけない
  • オフラインゲームと比べてネトゲ開発考えなくてはいけないこと・実装しなくてはいけないものが数倍はある
  • ある程度開発経験を積んで、プログラミングに自信がついてきたくらいじゃないと手を出すのは危険だと思う

2.ユーザ管理がしたい


つかったもの:ニフティクラウド mobile backend(NCMB)


NCMB

  • ニフティさんが提供するmBaaSサービス
    • (mBaaS: Mobile Backend as a Service)
  • 会員登録機能、データの保存、プッシュ通知などのサービスを利用できる
  • 200万APIリクエスト/月までなら無料で利用できる

公式サイト


NCMBでユーザ管理

  • NCMBを使えば簡単にゲームに会員登録・ログイン機能を実装できる
  • メールアドレスによる認証にも対応
  • 自前でアカウント管理サーバ立てなくても良いのは本当にありがたい

3.シリアルライセンス認証機能が欲しい


つかったもの:ない


どういうこと?

  • シリアルコードを持っている人だけ遊べる機能が欲しい
    • コミケで買ってくれた人だけ遊べるようにしたい
  • シリアルライセンス認証を提供してくれる良い感じのサービスは存在しない

結局どうしたか

  • 自前で認証サーバを作るしか無かった
    • Ruby on RailsはサクッとWebサービス作れていいね
  • 自前サーバとNCMBを連携させて購入者・未購入者の管理をやる

詳しくはUnityとNCMBでユーザ管理を実装してみた話を参照


4. NPCの対戦相手を作りたい


つかったもの

Behavior Designer
Behavior.png


Behavior Designer

  • BehaviorTreeをGUIで作成できるアセット
  • NPCのアクションを最小単位に分割し、それらを組み合わせることで人っぽく動くようにする
  • ステートマシンを組むのと比べて非常にわかりやすくNPCが作れるのでオススメ

完成した実際のTreeがこちら

Cx8_2o9UkAA-Cc1.jpg

  • 作るのにかかった時間は15時間くらい
  • 7割くらいは自作のTask

実際に作ったNPCの動画

(開発中)ハクレイフリーマーケット AI同士で対戦させてみた 3


5.ゲームパッドの管理をしたい


つかったもの

Rewired
rewired.png


Rewired

  • ゲームパッドの管理に特化したアセット
  • ゲームパッドの自動検出・自動割当を動的に行ってくれる
  • ゲームパッドの型番に応じて適切にキーアサインをしてくれる
  • 膨大な種類のゲームパッドが最初から登録されている
    • とりあえず手元にあったゲームパッドは大体対応していた

6. 物理演算ベースのCharacterControllerが欲しい


つかったもの

Easy Character Movement

ECM.png


Easy Character Movement

  • 物理演算と相互干渉しながら動作するキャラクタが簡単に作れるアセット
  • CharacterControllerと同じインターフェイスでRigidBodyを扱える
    • Move()メソッドで移動しつつApplyForce()で吹っ飛ばす、みたいなことができる
    • 動く床の影響を受けるようにしたりもできる

7. オブジェクトを目立たせたい


つかったもの

Highlighting System
HS.png


Highlighting System

  • スクリプトをアタッチするだけでGameObjectにアウトラインをつけることができる
    • 特別なシェーダを使わずスクリプトオンリー
  • 点滅機能や、壁を透けて描画する機能などもある
  • プレイヤにアウトラインをつけておくと視認性が向上するのでオススメ

おすすめアセット・サービス編 完


プログラミング編


プログラミング編

  • Unityでプログラムを書いてきて発生した問題とその解決策を紹介

1.型安全じゃないところで死ぬ問題
2.マネージャシングルトンの配置問題
3.神クラスができる問題
4.コンポーネント同士の連携方法
5. ViewとModelの連携


1.型安全じゃないところで死ぬ問題


「起動はするけどなんか挙動がおかしい。動かない。」
「コンパイルエラーは出てないのに、動かした時にエラーが出るんだけど。」


型安全とは?

  • プログラムの書き方を間違えた時にちゃんとコンパイルエラーになる状態
    • 「型」によってプログラムの正当性が担保されている状態のこと
    • 型安全性が無い場合、どこか記述を間違えても人間がそのミスに気づくすべが無い
    • バグったときに怪しいところを総当りでコードチェックすることになる

Unityで型安全じゃない機能

  • SendMessage
  • Tag
  • Scene遷移
  • Invoke
  • Animatorのフラグ指定

ほとんどが文字列ベースで処理をしているところ


例:タグ

public void OnCollisionEnter(Collision collision)
{
    //Enemyをスペルミスしてるけどコンパイルエラーにならない!
    if (collision.gameObject.tag == "Eneny")
    {
        //なんかの処理
    }
}

例:Animatorのアニメーションフラグ

//IsRunningをスペルミスしてるけどコンパイルエラーにならない
animator.SetBool("IsRuning", true);

対策


文字列ベースの処理を消す


型安全に書く方法

  • 文字列を使って動作を制御してるのが諸悪の根源
  • 文字列を使う場所を制限し、できるだけ「型」の恩恵を受ける形にすれば良い
    • enumを使うなり、インターフェイスを使うなり、プロパティでラップするなり

例1

  • Tagの比較にenumを使う
enum EntityType
{
    Player,
    Enemy,
    Boss
}

public void OnCollisionEnter(Collision collision)
{
    //enumのToStringは遅いけどネ...
    if (collision.gameObject.tag == EntityType.Enemy.ToString())
    {

    }
}

例2

  • そもそもTagを使わずに型で判定する
public void OnCollisionEnter(Collision collision)
{
    //IEnemyインターフェイスを実装したコンポーネントを持っているか調べる
    var enemyComponent = collision.gameObject.GetComponent<IEnemy>();
    if (enemyComponent != null)
    {
        //処理
    }
}

例3

  • アニメーションフラグをプロパティで包む
Animator animator;

/// <summary>
/// 移動アニメーションフラグ
/// </summary>
private bool IsRunning
{
    //"IsRunning"という文字列が登場するのはここ1箇所のみ
    //(ここだけスペルミスしないように注意しておけばよい)
    set { animator.SetBool("IsRunning", value); }
}

void Start()
{
    animator = GetComponent<Animator>();

    // boolを代入するだけで利用できる
    IsRunning = true;
}

「型安全じゃないところで死ぬ問題」のまとめ

  • int型やstring型を使って条件判定を行っているところはミスが発生しやすい
  • 自前で型を作ったり、enumを使ったり、プロパティで包むなど、とにかくプリミティブな型が露出しないように心がけると良い

2. マネージャシングルトンの配置問題


「Editor上でシーンを指定して実行したらなんかエラー出るんだけど」
「なんか同じGameObjectが2個あるんだけど?」


マネージャシングルトン

  • シーンを横断してリソース等を管理し続けるシングルトン
    • シーン遷移時のトランジションアニメーション管理
    • サウンド管理
    • ゲーム中の設定項目の管理
  • 常に1個ゲームの実行中に存在しないとゲームが正しく動作しない

マネージャシングルトンをどうやって初期化する?

  • シーンにPrefab化したシングルトンを配置しておくだけでいいんじゃないの?
    • DontDestroyOnLoad指定して一度配置したら消えないようにしておく

 だめです


シーンに最初からシングルトンを置いておくのがダメな理由

  • シングルトンを配置してないシーンからゲームを実行するとシングルトンが存在しなくて死ぬ
    • Editor上でデバッグ中に発生する
    • 全シーンにシングルトンを配置しておくみたいなことはやりたくない
  • シングルトンが存在するシーンを訪れるたびにシングルトンが生成されて増殖する
    • 多重生成されたら消すというのも根本的な解決になってない

対策


「必要になったタイミンで初めてシングルトンを動的に生成すればいい」

必要になった時に、

  • 既にシングルトンが存在したらそれを使う
  • 無かったら新しく生成する

ってやるだけで解決できる(要するにただの遅延初期化)


実装例

シングルトン生成側
using UnityEngine;

/// <summary>
/// シーン遷移を行うクラス
/// </summary>
public static class SceneLoader
{
    /// <summary>
    /// シングルトンなマネージャのPrefabへのパス
    /// </summary>
    private static readonly string managerPrefabPath = "Managers/TransitionManager";

    /// <summary>
    /// シングルトントランジションアニメーションの制御コンポーネント
    /// </summary>
    private static TransitionManager _transitionManager;

    /// <summary>
    /// 既にシングルトンが存在するならそれを返し、無いなら作る
    /// </summary>
    private static TransitionManager TransitionManager
    {
        get
        {
            if (_transitionManager != null) return _transitionManager;
            if (TransitionManager.Instance == null)
            {
                var resource = Resources.Load(managerPrefabPath);
                Object.Instantiate(resource);
            }
            _transitionManager = TransitionManager.Instance;
            return _transitionManager;
        }
    }

    /// <summary>
    /// シーン遷移を開始する
    /// </summary>
    /// <param name="scene">次のシーン</param>
    public static void LoadScene(GameScenes scene)
    {
        TransitionManager.StartTransaction(scene);
    }
}

staticクラスにマネージャシングルトンの存在チェックを挟み、無ければ生成させる。


実装例

呼び出し側
SceneLoader.LoadScene(GameScenes.Title);

呼び出し側はシングルトンを意識せず、staticクラスに実装したメソッドを実行する


マネージャシングルトンの配置問題のまとめ

  • 遅延初期化を利用すれば解決する
  • ただし初回アクセス時にシングルトンの生成コストが発生するデメリットがある
    • スタンドアロン時には起動直後のシーンでまとめて初期化を実行
    • エディタ実行時のみこの遅延初期化を使う
    • ってやるといいかも

3.神クラスができる問題


「このクラス1000行近くあるぞ…」
「フィールド変数とメソッドが大量にあってどう依存してるかわからない!」
「Update()の中身がやばい!!!!!!!!!」


神クラス?

  • いろんな機能が詰め込まれ肥大化したクラス
  • 避けるべきアンチパターン
  • 雑に作るとこれになる
    • Unityの公式チュートリアルのプロジェクトをそのまま拡張していくと神クラスに成長しやすい

神・PlayerController

  • Input管理、移動、攻撃、ダメージ、アニメーション、効果音、パーティクルエフェクト、UI管理…
  • これらの処理が 全て1つのクラスに定義されてる
    • 状態を表すフラグが乱立し、Update()の中もグッチャグチャになってる

これをやらかした人は多いはず


対策


コンポーネントを分割する

reimu.png


コンポーネントを分割する

  • 単一責任原則を意識してコンポーネントを分割

    • 1コンポーネントが請け負う仕事を1つに限定してしまう
    • 移動処理はPlayerMoverの中、アニメーション管理はPlayerAnimation、みたいに
  • 1コンポーネントが扱う領域が狭くなるため必要最低限のフィールドとメソッドだけ実装されてればよくなる


コンポーネントを分割する時のコツ

  • そのコンポーネントが実現する「機能」に関係ある処理のみを実装する
    • 「移動」に特化したコンポーネントは移動処理に必要なことしか考えない
    • 自分の責務外の処理は別コンポーネントに委譲させる

コンポーネント分割のメリット

  • 1コンポーネントが受け持つ責務が明確化される
    • 余計なことを考えず、今このコンポーネントがやるべきことのみを考えて記述すれば良い
  • どの処理がどのコンポーネントに書かれているかわかりやすくなる
    • クラス名から処理の場所を正引きできで探せるようになる

ただしデメリットもある


コンポーネント分割のデメリット

  • 膨大な数のComponentをアタッチする必要がありその管理が大変になる
    • 鬼のようなRequireComponentを書くのもアリだけど…
    • MonoBehaviorを継承しないPureなClassを生成してそっちに処理を委譲するという方法もあり
  • Updateの呼び出しコストが増える
    • (UniRxのObservableUpdateTriggerを使えばUpdateコストは一応削減できる)
  • 後述する「依存関係」や「クラス間の連携」の問題が出てくる

神クラスは絶対悪か?

  • 規模が小さくて手に負える範囲なら全処理を1個のクラスに詰め込んでてもよい
  • とりあえずベタ書きしてみて不穏な空気がしたら分割する、で最初はよいかと

3.依存・参照関係が複雑化する問題


「どのコンポーネントがどれに依存してんだ?」
「コンポーネントを使いまわしたいんだけど、なんかいろいろ依存してて使いまわせない!」
「あれ?なんか相互参照してて初期化に失敗するぞ」


依存関係が複雑化する問題

  • 分割したコンポーネントを行き当たりばったりに繋いでいくと複雑化する
  • どこをいじればどこに影響が出るのか予測不可能になる

対策


実装する前にクラス図を書いて設計する


設計をちゃんとやろう

  • 複雑化する原因はほとんどが 行き当たりばったりに実装する から
    • 仕様がもともと複雑な場合はしょうがない
  • 設計してクラス図に起しておけば実装作業を分担できるようになる
    • 誰が書いても設計の通りに作られるはずである
  • 雑なクラス図でも書いておけば後から全体像の把握が可能になる

クラス図を書くのにオススメツール

  • PlantUMLを使うと簡単に図が書けてオススメ
    • テキストを書くだけで自動的にクラス図が生成される

1.gif


実際に書いたクラス図の例

Players.png
(ハクレイフリーマーケットのプレイヤ周りのクラス図)


クラス設計時のコツ

  • 相互参照、循環参照はできるだけ避ける
    • 相互参照・循環参照はメリットよりもデメリットの方が目立つので安易に利用しない
    • 閉じた概念の中で局所的に利用する程度に抑えておくとよい
  • 抽象化」や「依存関係の逆転」を利用して依存関係を整理する

抽象化

1..png


依存関係の逆転

2.png


設計はノウハウのかたまり

  • SOLID原則はしっかり意識するとよい
  • 「きな臭さ」を感じ取れるようになれるとよい
    • 慣れてくるとどこを抽象化しておくべきか感覚でわかるようになる
  • とりあえず設計をガン無視していろいろ作ってドツボにはまってみて、そこから設計の重要性を実感するとよいかと

以前こういう反論もありました

「仕様がコロコロ変わるから設計なんてやってられない!」


「仕様がすぐ変わる、だから設計しない」は正しいか?

  • ぶっちゃけケース・バイ・ケースとしか言えない
  • 変更に柔軟に作るのもありだし、YAGNIで作るのもあり
    • (YAGNI: 仕様変更で機能が無駄になることを見越してあえてシンプルに作っておこうという開発スタイル)
  • プロジェクトによって最適な開発スタイルは変わるので、設計を投げ捨てた方が効率がいいならそうするべき
    • 設計したほうが後々楽になる場合の方が多いが、「どうしても設計したくない」という人に無理強いはできない

ちなみに


「依存・参照関係が複雑化する問題」のまとめ

  • 設計をちゃんとやろう、が答え
  • クラス図を作っておけばとりあえず実装で迷子になることはない

4.コンポーネント同士の連携方法


バラバラにしたコンポーネントをどう協調させて動かす?

  • 「プレイヤが気絶したら、移動できなくして、気絶アニメーションを再生して、効果音を再生したい」

  • 状態管理を一元化して、そこの変化を検知して勝手に処理が動く形にしたい


コンポーネントの連携

  • 何かある度に イベントを発行するのが一番楽
    • (Observerパターン を適用して連携させる)
  • 各コンポーネントは「○○が起きたら××をする」ということだけを意識させる形にする

Observerパターン?


Observarパターン

  • 何かあった時に、監視対象側からイベントを飛ばしてもらって購読側で処理をするデザインパターン
  • 依存関係を逆転できるメリットがある
    • (毎フレーム相手のフラグを監視しにいく、みたいな実装が無くせる)

わからないならとりあえず「UniRx」を使えばOK


UniRx

  • Reactive Extensions for Unity
  • リアクティブプログラミングをUnityでできる
  • できることが多すぎて一言で説明できない!
    • 過去にまとめ記事をいくつか書いてるのでそちらを参照してください
    • まとめリンク

UniRxのReactiveProperty<T>

  • イベント発行機能を持った変数
  • 値が書き換わると通知が飛ぶ
  • これを親となるクラスに持たせ、子がイベントを待ち受ければOK

実装例

  • 「プレイヤが気絶したら、無敵にして、移動できなくして、気絶アニメーションを再生させたい」
  • UniRxを用いた実装例を紹介

体力管理(イベント発行側)
using System.Collections;
using UniRx;
using UnityEngine;

/// <summary>
/// 体力管理を行うコンポーネント
/// </summary>
public class PlayerHealth : MonoBehaviour
{
    /// <summary>
    /// 気絶フラグ
    /// </summary>
    public BoolReactiveProperty IsStunned = new BoolReactiveProperty();
    /// <summary>
    /// ダメージ処理
    /// </summary>
    public void ApplyDamage()
    {
        //既に気絶していないなら気絶させる
        if (!IsStunned.Value) StartCoroutine(StunCoroutine());
    }
    /// <summary>
    /// 気絶中に実行されるコルーチン
    /// </summary>
    private IEnumerator StunCoroutine()
    {
        //気絶フラグON
        IsStunned.Value = true;
        //適当に待機
        yield return new WaitForSeconds(5);
        //気絶フラグOFF
        IsStunned.Value = false;
    }
}

/// <summary>
/// 移動管理コンポーネント
/// </summary>
public class PlayerMove : MonoBehaviour
{
    /// <summary>
    /// 移動許可フラグ
    /// </summary>
    private bool _canMove;

    private void Start()
    {
        //参照の取得はお好みの方法で
        var playerHealth = GetComponent<PlayerHealth>();

        //気絶フラグが変化したら移動許可フラグに反映する
        playerHealth.IsStunned.Subscribe(x => _canMove = !x);

        //以下に_canMoveフラグを使った移動処理を書く
        //以下略
    }

アニメーション管理
using UnityEngine;
using UniRx;
public class PlayerAnimation : MonoBehaviour
{
    private Animator animator;

    private bool IsStunned
    {
        set { animator.SetBool("IsStunned", value); }
    }

    void Start()
    {
        animator = GetComponent<Animator>();

        var playerHealth = GetComponent<PlayerHealth>();

        //気絶フラグが書き換わったらAnimatorの気絶フラグに反映
        playerHealth.IsStunned.Subscribe(x => IsStunned = x);
    }
}

サウンド管理
using UniRx;
using UnityEngine;

/// <summary>
/// サウンド管理
/// </summary>
public class PlayerSound : MonoBehaviour
{
    void Start()
    {
        var playerHealth = GetComponent<PlayerHealth>();
        //気絶状態にあわせて効果音を再生・停止する
        playerHealth.IsStunned.Subscribe(x =>
        {
            if (x)
            {
                Play();
            }
            else
            {
                Stop();
            }
        });
    }

    private void Play()
    {
        //省略
    }
    private void Stop()
    {
        //省略
    }
}

「コンポーネント同士の連携方法」のまとめ

  • イベント駆動な実装にしてしまうと楽
    • Observerパターンを使うとよい
  • UniRxがすごくよくマッチする
    • まさにリアクティブプログラミング!

5. ViewとModelの連携


「なんかModel(データ実体)とView(UI)が相互に依存してんだけど…」

3.png


ViewとModelが相互参照するといろいろヤバイ

  • データの実体を持つコンポーネント(Model)がUIの制御までしないといけなくなる
    • Modelが神クラス化していく
  • UIの差し替えが困難になる
    • ModelがViewにべったりくっついてるので、簡単に切り離し・差し替えができない
  • UIの使い回しができなくなる
    • Viewに必要なロジックがModelに書いてあるため、別Modelに同じViewを適用しようとするとModelの実装ごと移植しないといけなくなる

ViewとModelの上手い取り扱い方

  • 昔からずっと議論されてきている問題
  • 今までにMVC、MVP、MVVMなどのアーキテクチャパターンが考案されてきた
  • Unityで良い感じに使えるパターンは無いのか?

あります


MV(R)Pパターン


MV(R)Pパターン

  • Model-View-(Reactive)Presenterパターン
  • UniRxの作者、neueccさんが提唱するUniRxを用いたUnityにおけるUIアーキテクチャパターン
  • 「Presenter」を用意しModelとViewの仲介をさせる
  • UniRxのReactivePropertyを使うのがミソ

参考: UniRx 4.8 - 軽量イベントフックとuGUI連携によるデータバインディング


MV(R)Pパターン適応後

4.png


MV(R)Pの実装例

  • uGUIのInputFieldの入力をModelに保存
  • Modelの保持するデータに変更があったらInputFieldも更新する
  • InputField = Viewそのもの という扱い

Model

using UniRx;
using UnityEngine;

public class Model : MonoBehaviour
{
    //外に公開するデータ
    public ReactiveProperty<string> Name = new ReactiveProperty<string>();
}

Presenter

using UniRx;
using UnityEngine;
using UnityEngine.UI;

/// <summary>
/// ModelとViewをつなぐPresenter
/// </summary>
public class Presenter : MonoBehaviour
{
    //View
    [SerializeField]
    private InputField nameInputField;

    //Model
    [SerializeField]
    private Model model;

    void Start()
    {
        //Viewが更新されたらModelを書き換える
        nameInputField
            .OnValueChangedAsObservable()
            .Subscribe(x => model.Name.Value = x);

        //Modelが更新されたらViewを書き換える
        model.Name
            .Subscribe(x => nameInputField.text = x);
    }
}

MV(R)Pパターンはオススメ

  • uGUI使うならこのパターンにするとめっちゃ楽
  • Viewのためにちょっとしたロジックが必要ならそれはPresenterに書いても良い
    • データフォーマットの変換とか
  • Presenterの役割は「ModelとViewをつなぐこと」なので、そこから逸脱した処理は書かない
    • Presenterの実装は数行~数十行程度で終わるくらいでちょうどいい

プログラミング編・完


最後に

  • 思いついたものを片っ端から書いたらすごい分量になっちゃった
  • 少しでも参考になった部分があれば嬉しいです
  • もう2度とネトゲは作りたくないです

ありがとうございました