いろいろガンガンいこうぜ。体も大事に

普段はJavaでAndroidアプリ開発しているプログラマーのブログです。

Google Play Game ServiceとBaseGameUtilsの簡易メモ

Google Play Game ServiceをBaseGameUtilsというライブラリプロジェクトを利用して使う際の簡易メモです。

プロジェクトの構成のメモ

 ゲームのプロジェクトには2つのライブラリプロジェクトが必要です。Eclipseのプロジェクト名で言うと,

  • google-play-service_lib
  • BaseGameUtils

という2つです。

 google-play-service_libプロジェクトのクラス群をBaseGameUtilsは使用しています。そのためEclipseで使う場合,BaseGameUtilsの設定でgoogle-play-service_libにライブラリプロジェクトを登録してあげる必要があります。

 単純なことをするだけでいいのであれば,自作のゲームプロジェクトのActivityはBaseGameUtilsプロジェクト内のBaseGameActivityを継承して,そのクラスで定義されたメソッド呼んだり,そのメソッドで取得できるクラス・インスタンスを利用すれば良さそうです。

BaseGameUtilsのメモ

 BaseGameUtilsというプロジェクトには2つのクラスがあります。

  • BaseGameActivity
  • GameHelper

の2です。

 GameHelperクラスは,GamesClientなどのクライアントクラスの管理をしています。また,デバックログの出力やダイアログを出力したり,google-play-service_lib内のクラスを用いてサインインやコネクションの処理もしているようです。

 BaseGameActivityはActivityのサブクラスで抽象クラスになっています。抽象メソッドは,

  • onSignInFailed
  • onSignInSucceeded

の二つです。beginUserInitiatedSignIn()というメソッドを呼び出して,成功もしくは失敗した際に呼ばれるメソッドが抽象メソッドとして定義されています。

 先ほどのGameHelperを内部で保持していて,GameHelperのメソッド群をラップしていて,いくつかpublicメソッドやprotectedメソッドとして定義されています。

 このBaseGameActivityを継承して,抽象メソッドを実装したり,クラスで定義されたメソッドを呼び出したりすれば,ある程度簡単にGoogle Play Game Serviceが利用できるのでは無いかと思います。

BaseGameActivityのサブクラスで呼び出せるメソッド

サインインボタンをクリックしたときに呼び出す

beginUserInitiatedSignIn()

サインアウトボタンをクリックしたときに呼び出す

signOut()

サインインしているかどうかを調べる

isSignedIn()

アチーブメント表示アクティビティに遷移するためのIntent作成

getGamesClient().getAchievementsIntent()

リーダーボード表示アクティビティに遷移するためのIntent作成

getGamesClient().getAllLeaderboardsIntent()

プレイヤー名の取得

getGamesClient().getCurrentPlayer().getDisplayName()

アチーブメントを解放する

getGamesClient().unlockAchievement("#アチーブメントのID#")

増分アチーブメントのポイントを増分する

getGamesClient().incrementAchievement("#アチーブメントのID#", #増分ポイント#)

リーダーボードにスコアを登録する

getGamesClient().submitScore("#リーダーボードのID#", #スコア#)

GameObjectについている全てのコンポーネントを取得する

GameObjectについている全てのコンポーネントを取得するGetComponentsSampleというクラスを作ります。

f:id:MRStar:20140102140651p:plain

上の画像のようにGameObjectに,

  • Transform
  • BoxCollider
  • RigidBody
  • Sample0
  • Sample1
  • Sample2
  • GetComponentsSample

という並び順でひとつのGameObjectにコンポーネントを付与しています。

まず,MonoBehaviourクラスを継承した自作コンポーネントを取得します。 コンポーネントの型をGetTypeで取得して,型名をデバックログで表示します。

using UnityEngine;

public class GetComponentsSample : MonoBehaviour
{
    void Start ()
    {
        MonoBehaviour[] monoBehaviours = GetComponents<MonoBehaviour> ();
        foreach (var monoBehaviour in monoBehaviours){
            Debug.Log (monoBehaviour.GetType ().Name);
        }
    }
}

デバックログの出力結果は,

Sample0
Sample1
Sample2
GetComponentsSample

となりました。

次にビルドインコンポーネントも含めて表示して,型を取得,型名を表示します。

using UnityEngine;

public class GetComponentsSample : MonoBehaviour
{
    void Start ()
    {
        Component[] components = GetComponents<Component> ();
        foreach (var component in components){
            Debug.Log (component.GetType ().Name);
        }
    }
}

実行結果は

Transform
BoxCollider
RigidBody
Sample0
Sample1
Sample2
GetComponentsSample

となりました。

自作のクラス(MonoBehaviourのサブクラス)だけでいいならば

var behaviours = GetComponents<MonoBehaviour> ();

ビルドインコンポーネントも含めて取得するならば,

var components = GetComponents<Component> ();

で取得できるようです。

GetComponentメソッドについて

GetComponentメソッドについて調べたり,まとめてみたりしました。 (Unity4.3.2f1で試しています。)

3個のオーバーロード

Componentクラスのインスタンスメソッド,GetComponent。

GetComponentのスクリプトリファレンス

このGetComponentメソッドは3個のオーバーロードを持っています。

Component GetComponent(Type type);
T GetComponent<T>();
Component GetComponent(string type);

スクリプトリファレンスによるとパフォーマンスの観点から見て,文字列型を引数にとるオーバーロードより他の物を使った方がいいようです。

またGeneric Functionsにはジェネリクスを用いると,キャストをする手間とタイプする文字数が減るよねという記述があります。

基本的には,ジェネリクス版を使えばいいと思います。が,しかしジェネリクスのオーバーロードではできず,Type型を引数にとるオーバーロードではできるものがありました。

型パラメータにスーパークラスを与えてGetComponent

MonoBehaviourクラスを継承したBaseクラス。そのBaseクラスを継承したDerived0クラスを用意します。

using UnityEngine;

public class Base : MonoBehaviour { }
public class Derived0 : Base { }

そして,Baseクラスを型パラメータに指定してGetComponentします。 そのGetComponentしたコンポーネントのクラス名を表示するクラスを作ります。

using UnityEngine;

public class GetComponentSample : MonoBehaviour
{
    void Start ()
    {
        Base component = GetComponent<Base> ();
        Debug.Log (component.GetType ().Name); 
    }
}

次のようにシーン上のGameObjectにDerived0とGetComponentSampleコンポーネントをつけます。

画像1

これを実行すると,Derived0とデバックログが出力されます。 サブクラスのコンポーネントが付与されているGameObjectに対して,スーパークラスの型を指定して,GetCompoenentでコンポーネントを取得することができました。

次のように,スーパークラスを抽象クラスにすることも可能なようです。

using UnityEngine;

public abstract class Base : MonoBehaviour { }

また,次のようにDerive0とは違うサブクラスを作って,

public class Derived1 : Base { }

それを次のようにGetComponentSampleと一緒にGameObjectに付与して,

画像2

実行した場合,こちらはDerived1と出力されました。

型パラメータにスーパークラスを指定したGetComponentメソッドの呼び出しで,サブクラスのコンポーネントを取得することができることがわかりました。

複数のコンポーネントが付与されてる場合のGetComponent

using UnityEngine;

public class StringContainer : MonoBehaviour
{
    public string value;
}
using UnityEngine;

public class GetComponentSample : MonoBehaviour
{
    void Start ()
    {
        StringContainer component = GetComponent<StringContainer> ();
        Debug.Log (component.value);
    }
}

次の画像のように一つのGameObjectにStringContainerクラスを3個,GetComponentSampleを1個付与します。

画像3

これを実行すると, デバックログにはvalue0と表示されました。 インスペクターのMoveUpなどを用いて,コンポーネントの順番を変えます。

画像4

上の画像のような状態で実行するとデバックログにはvalue1と表示されました。 また順番を変えて以下のようにします。

画像5

この状態で実行するとデバックログにはvalue2と表示されました。

同じクラスのコンポーネントが複数付与されている場合,インスペクターにおける一番上のインスタンス(という言い方でいいのでしょうか?)が取得できるようです。

前の節で作ったBaseクラス,Derived0クラスそしてDerived1クラスを使ってみます。ふたたびBaseクラスでGetComponentして,クラス名を表示します。

using UnityEngine;

public class GetComponentSample : MonoBehaviour
{
    void Start ()
    {
        Base component = GetComponent<Base> ();
        Debug.Log (component.GetType ().Name); 
    }
}

画像6

上の画像のように, GetComponentSampleクラス, Derived0クラス, Derived1クラス の順番でコンポーネントを付与します。

これを実行するとデバックログに,Derived0と表示されました。

順番を変えます。

画像7

上の画像の用に GetComponentSampleクラス, Derived1クラス, Derived0クラス の順番でコンポーネントを付与したら

これを実行するとデバックログに,Derived1と表示されました。

GetComponentでコンポーネントを取得する際,もし指定した型パラメータのクラスで取得出来るコンポーネントが複数ある場合は,インスペクター上で一番上の物が取得できるようです。

GetComponentsについて

複数のコンポーネントを取得するGetComponentsというメソッドがあります。

取得しようとするコンポーネントがGameObjectについていない場合,GetComponentではnullが帰ってきますが,GetComponentsでは空の配列が帰ってくるようです。

前節のStringContainerクラスとGetComponentsメソッドを用いたサンプルを作りました。

using UnityEngine;

public class GetComponentSample : MonoBehaviour
{
    void Start ()
    {
        StringContainer[] stringContainers = GetComponents<StringContainer> ();
        foreach (var stringContainer in stringContainers) {
            Debug.Log (stringContainer.value);
        }
    }
}

画像8

上の画像ようにGetComponentSampleを1個とStringContainerクラスのコンポーネントを複数付けて文字列valueを画像の用に設定し,実行すると

value0
value1
value2
value3

という順番で表示されました。

順番を変えます。

画像9

上の画像のように,順番を変えると実行結果は

value3
value2
value1
value0

となりました。

GetComponentsで取得した配列の要素の順番は,インスペクターで表示される順番と同じ順番のようです。

インタフェースでGetComponent

抽象クラスを指定してGetComponentをすることができたので,インタフェース型を指定してGetComponentをすることもできるのではないかと思いました。

結論から言うと,インタフェース型を指定してGetcomponentは

  • Component GetComponent(Type type)はできる
  • T GetComponent()はできない

ようです。

public interface ISample { }
using UnityEngine;
public class Implemented0 : MonoBehaviour, ISample { }
using UnityEngine;

public class GetComponentSample : MonoBehaviour
{
    void Start ()
    {
        // ISample component = GetComponent<ISample> (); これはコンパイルエラー
        ISample component = GetComponent(typeof(ISample)) as ISample;
        Debug.Log (component.GetType ().Name);
    }
}

このGetCompoentSampleクラスとImplemented0クラスを下の画像のようにGemeObjectに付与します。

画像10

実行するとデバックログにImplemented0と出力されます。

Type型を引数にとるGetComponentのオーバーロードでは,インタフェース型を指定してコンポーネントインスタンスを取得することができました。

ジェネリクスの方のオーバーロードのGetComponentではインタフェース型を指定すると,コンパイルエラーとなってしまいました。

T GetComponent<T>()の制約について

インタフェースで T GetComponent<T>() しようとするとコンパイルエラーになります。この時,次のようなメッセージが表示されます。

The type `ISample' cannot be used as type parameter `T' in the generic type or method `UnityEngine.Component.GetComponent<T>()'. There is no implicit reference conversion from `ISample' to `UnityEngine.Component' (CS0311) (Assembly-CSharp)

Component型への暗黙の参照変換ができないから,型パラメータTとしてISampleはダメだよ。ってことでしょうか。

System.Reflection名前空間のクラスを使って, T GetComponent<T>() の,型パラメータの制約を見てみました。

先に結論を言うと,T GetComponent<T> ()

  public T GetComponent<T> () where T : Component

となっているのではないかと思います。

void Start ()
{
    MethodInfo methodInfo = typeof(GameObject).GetMethod ("GetComponent", new Type[]{});
    Debug.Log ("Method Name : " + methodInfo.Name);
    // Method Name GetComponent

    Debug.Log ("Return Type : " + methodInfo.ReturnType.Name);
    // Return Type : T

    ParameterInfo[] parameterInfos = methodInfo.GetParameters ();
    Debug.Log ("parameter count : " + parameterInfos.Length);
    // parameter count : 0


    Type[] genericArgumentTypes = methodInfo.GetGenericArguments ();
    Debug.Log ("generic argument type count : " + genericArgumentTypes.Length);
    // generic argument type count : 1

    Type genericArgumentType = genericArgumentTypes [0];
    Debug.Log ("generic argument type : " + genericArgumentType.Name);
    // generic argument type : T


    Type[] genericParameterConstraints = genericArgumentType.GetGenericParameterConstraints ();
    Debug.Log ("generic parameter constraints count : " + genericParameterConstraints.Length);
    // generic parameter constraints count : 1

    Type genericParameterConstraint = genericParameterConstraints [0];
    Debug.Log ("type constraint : " + genericParameterConstraint.Name);
    // type constraint : Component
}

Debug.Logの下のコメント行は,表示されたデバックログです。

デバックログの出力内容から見るに,T GetComponent <T> () のT 型パラメータとして指定出来るのは,Component 型もしくはそのサブクラスである必要があるようです。

そのため,T GetComponent <T> ()のTにインターフェースを指定した場合,Component型でもComponent型のサブクラスでもないため,コンパイルエラーになるようです。

まとめとか

よく使うGetComponentメソッド。(あとGetComponentsメソッド) 型パラメータにスーパークラスや抽象クラスを指定するのは,いくつか使い方がありそうですね。インターフェースはどうでしょう。もしかした。

本当は全コンポーネントや,一つのGameObjectについている,全ての自作コンポーネントを取得する方法を調べていたら,細かいところが気になって書いたポスト。 次は,GetComponents<Component>GetComponents<MonoBehavoiour>コンポーネント全取得します。

関連リンク

  • GetComponentのスクリプトリファレンス

http://docs.unity3d.com/Documentation/ScriptReference/Component.GetComponent.html

http://docs.unity3d.com/Documentation/Manual/GenericFunctions.html

  • 暗黙の参照変換

http://msdn.microsoft.com/ja-jp/library/aa691284(v=vs.71).aspx

  • 型パラメータ制約

http://msdn.microsoft.com/ja-jp/library/d5x73970(v=vs.90).aspx

StopCoroutineみたいに引数にメソッド名を文字列で渡すのが嫌。

前置き

静的型付け言語の良さの一つは,実行する前にコンパイルエラーで型の矛盾点やおかしい点を見つけられることだと思います。 例えば,定義していないメソッドを呼び出そうとしたり,クラス名が間違っていた場合,実行前におかしいということに気づけます。 EclipseやInteliJ,MonoDevelopなどのIDEもコンパイルが通らないコードを書いたら,「おい,そこおかしいぞ」って教えてくれます。

StartCoroutineメソッドとStopCoroutineメソッド

話は変わって,UnityのMonoBehaviorクラスのStartCoroutineメソッドとStopCoroutineメソッド。 1秒をおきに"logging"ってデバックログを表示したいとします。

private IEnumerator RoopLoggingRoutine ()
{
    while (true){
        yield return new WaitForSeconds (1.0F);
        Debug.Log ("logging");
    }
}

というメソッドを定義して, これを,

StartCoroutine (RoopLoggingRoutine ());

もしくは,

StartCoroutine ("RoopLoggingRoutine");

って呼び出してあげれば良いです。

ロギングをやめたい場合は,StopCoroutineメソッドを用います。

StopCoroutine ("RoopLoggingRoutine");

StopCoroutineは文字列を引数にとるものしかありません。 また,StopCoroutineメソッドで止めることができるのは,StartCoroutineメソッドの文字列を引数にとるオーバーロードで始めたコルーチンだけです。

IEnumeratorを引数にとるコルーチンを能動的に止めるには,StopAllCoroutinesメソッドを用いる必要があります。

メソッド名の文字列の引数が嫌だ。

やっと,本題。

メソッド名を変更する,そんな状況を想像してください。 普通だったらIDEのリファクタリング機能を使って,変更したメソッドを呼び出している箇所も一気に変更すると思います。 仮にそうしなくても古いメソッド名のままになっている箇所は,コンパイルエラーになるので,それをIDEが教えてくれます。

しかし,StartCoroutineやStopCoroutineのようにメソッド名を文字列で引数で渡している場合そうはなりません。

前の例のRoopLoggingRoutineをLoggingRoutineというメソッド名に変更するとします。

StopCoroutine ("RoopLoggingRoutine")StartCoroutine ("RoopLoggingRoutine")という箇所も変更しないといけません。 この箇所はリファクタリング機能では変更されません。自分で変更しなくてはいけません。もし,この箇所の変更を忘れた場合,IDEは何も警告をしてくれませんし,コンパイルも通ります。

このまま変更を忘れてまま,実行すると次のようなエラーが出てしまいます。

Coroutine 'RoopLoggingRoutine' couldn't be started!
UnityEngine.MonoBehaviour:StartCoroutine(String)

実行時にエラーに気づくのでなく,最悪でもコンパイル時・ビルド時にエラーに気づけるようにしたいです。

対処法

System.Funcクラスを使います。

StopCoroutine ("RoopLoggingRoutine");

string methodName = ((Func<IEnumerator>) RoopLoggingRoutine).Method.Name;
StartCoroutine (methodName);

と変更します。

こうすれば,リファクタリング機能を用いてRoopLoggingRoutineというメソッド名をLoggingRoutineに変えた際にコルーチン処理の箇所も変更されます。もしリファクタリング機能を用いずに,変えた場合もコンパイル時にエラーになったり,MonoDevelopが警告してくれるので,実行時に「なんかエラーでた!なんだこれ!?」と焦ることもないです。

まとめ

System.Funcを介して,メソッド名を取得する方法でコルーチン処理を管理すれば,実行時エラーをさけることができると思います。 ブログを書いておいてあれなのですが,Func<IEnumerator>でキャストしてるところで何が起きているのか,自分はしっかりと分かっていないです。勉強しておきます。

最後にもう一つ。 Unityのスクリプトリファレンスによると,そもそも文字列を引数にとるStartCoroutineメソッドの呼び出しはオーバーヘッドが大きいそうです。またパラメータも一つしかとれません。その点にご注意ください。

以上です。

参考

Unityスクリプトリファレンス StartCoroutine

Unityスクリプトリファレンス StopCoroutine

関連

サンプルプロジェクト (github)

「Java魂」の2章,finalストーリーを4での感想

論理エラーをコンパイルエラーに置き換える

コンパイルエラーはある程度簡単に修正することがほとんどです。 それに対して論理エラーは発見するのが難しく,致命的な不具合を起こすことも多いです。

そのような内容で始まる,「Java魂」の2章,finalストーリーを4での感想です。

2..1 final 定数

2.1.2 public 基本型と置き換え

バージョニング問題。

自分もこの節で紹介されている問題で痛い目を見たことがあります。

package com.mrstar.sample.final_story.mock_library;

public class Constants {
    public static final String STRING_VALUE = "Hello World!";

    private Constants() {
    }
}
package com.mrstar.sample.final_story;

import com.mrstar.sample.final_story.mock_library.Constants;

public class Main {
    public static void main(String[] args) {
        System.out.println(Constants.STRING_VALUE);
    }
}

Constantsクラスで定義したpublic static fianalな文字列定数を,他のクラスMainから利用しています。一見問題がなさそうに見えます。 これらのクラスを普通に使う分には問題ないと思います。

次のような手順で問題が発生します。

  1. ConstansクラスとMainクラスをコンパイルする。
  2. 実行する。ここでは期待通り「Hello World!」と表示される。
  3. Constantsクラスの STRING_VALUEを「HELLO WORLD!!!!!」に書き換える。
  4. Constantsクラスのみコンパイルする。
  5. 実行する。「HELLO WORLD!!!!!」表示されることを期待するが,「Hello World!」と表示されてしまう。

この問題は,Constantsクラス内の定数の値を書き換えたビルドした時に,その利用側のMainクラスも再ビルドすることで発生しなくなります。 そもそも,public static finalな定数でなくて,private static finalであれば問題が発生しません。 これは,static finalなプリミティブ型の値や文字列の変数はリテラルで置き換えられ,コンパイルされた利用側のクラスは定数の値・リテラルが埋め込まれ,定数クラスの変数という情報がなくなるため発生するようです。

2.1.2 多すぎる定数

private static finalの定数は一つのメソッド内でしか使わないならば,メソッド内の定数として定義しましょう。とのこと。 確かにクラス内で定数が多数あると,読みづらいですよね。 クラス内のスコープということは,そのクラス内からであればどこからでも参照できるので,publicな定数ほどではないですが広いスコープを持っていますよね。

本では,複数のメソッドが定数にアクセスしている場合はprivate static finalの定数にすればいい。と書いてあります。

ScalaやKotlinなどでは,メソッド内にメソッドを定義できます。 複数のメソッド内メソッドで共通な定数もがあると思います。 その場合は外側のメソッドで定数を定義するのが有効なのかと思いました。

2.2 final変数,fialパラメーター

scalaなどのval。 初めて存在を知った時にはそれにどれくらい価値があるのだろうと思ったのですが,便利ですね。

この章の冒頭の論理エラーをコンパイルエラーに変える,という言葉。 valな変数,Javaではfinalな変数は、再代入するとコンパイルエラーになります。

このような変数は,参照しているオブジェクトは変わりません。値型ならば値は変わりません。 本来は再代入しないはずなのに,誤って再代入してしまった論理エラー。 これはvalな変数やfinalな変数をもちいることでコンパイルエラーにすることができます。

少し話は変わって初期化。 finalな変数は宣言時に初期化しなくても良いですね。 ただし,フィールドの場合はコンストラクタにおいては初期かしないといけません。クラス定数は,static{}で初期化しなくてはいけません。

2.4 finalコレクション

finalなコレクション。これも気をつけないといけないことを再確認しました。 以下のクラスを使ってみます。

package com.mrstar.sample.final_story;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

public class IntListWrapper {

    private final List<Integer> list;

    public IntListWrapper(Integer... values) {
        list = new ArrayList<Integer>(Arrays.asList(values));
    }

    public List<Integer> getList() {
        return list;
    }
}

IntListWrapperのフィールドlistにはfinalですね。 これを次のような感じで使います。

package com.mrstar.sample.final_story;

public class Main {
    public static void main(String[] args) {
        IntListWrapper intListWrapper = new IntListWrapper(1, 2, 3, 4, 5);
        System.out.println(intListWrapper.getList().toString());
    }
}

これを実行すると,[1, 2, 3, 4, 5]と表示されます。 List型のフィールドにはfinalがついているので,listが保持している中身は変わりませんね。 めでたしめでたし。

というのは間違いです。

list変数はfinal修飾子がついています。 ですが,不変なのはあくまでlist変数が参照するインスタンスです。 listの中身は不変ではありません。 次のようなことをしてみます。

package com.mrstar.sample.final_story;

public class Main {
    public static void main(String[] args) {
        IntListWrapper intListWrapper = new IntListWrapper(1, 2, 3, 4, 5);

        intListWrapper.getList().clear();
        intListWrapper.getList().add(1);

        System.out.println(intListWrapper.getList().toString());
    }
}

実行結果は,[1]です。 listの中身が変わってしまっています。

当然ですね。 list変数の参照先のインスタンスは,clearもremoveもaddも呼べるのですから。中身も変わります。

また,addやremoveを呼び出すとUnnsupportedOperationExceptionがなげられるListやSet,Mapを実装したインスタンスを返すCollectinsのメソッドが紹介されました。これは,addを呼ぶことに関してはコンパイルエラーにはなりませんが,UnnsupportedOperationExceptionが投げられることで,実行時エラーにはなりますが,分かりずらい論理エラーではないので,早く解決・検知できることも多いと思います。

Scalaでのコレクションは,不変なものと可変なものがあります。一般的に不変なものを使うことが多いらしいです。 不変なコレクションであれば,うっかりリストの中身が変わってしまうということも無いので,安心ですね。

2.5 インスタンススコープ変数

finalがついているフィールドは,

という性質があります。便利ですよね。

2.6 finalクラス,2.7 finalメソッド

Kotlinというプログラミング言語では,デフォルトでfinalクラスで,デフォルトでfinalメソッドらしいです。 もっというと,openという修飾子をクラスにつけないと継承できないし,openをつけないとメソッドはオーバーライドできないようですね。 一見面倒に思えますね。 Effective Javaという本には,「継承の為に設計及び文書化する。でなければ継承を禁止する。」という項目があります。 継承をする際にはしっかりと考え,設計をした後にコーディングを行うと思いますので,それに加えたらopen修飾子をつけるのは対したコストではない,ということでしょうか。

さてさて,final修飾子をつけるとクラスは継承ができなくなります。 また,privateなコンストラクタを作った場合,サブクラスはスーパークラスコンストラクタを呼べないので,継承禁止のクラスとなる,とあります。

2.8 条件付きコンパイル

public static final boolな変数のインスタンスを用いて,本番環境に残したくない処理を除くテクニックが紹介されてました。

finalというキーワードのみで構成されていた章。 苦い思い出が蘇り,大切さを再確認し,なぜそれがいいかをもう一度考えるきっかけとなりました。

オブジェクト指向って何だ?

オブジェクト指向って何だ?

オブジェクト指向

クラス,インスタンス,継承,アクセス修飾子,メソッド,フィールド,インタフェース,抽象クラス,ポリモーフィズム

一つ一つの用語の意味は理解しているつもりです。

ThoughtWorksアンソロジー アジャイルとオブジェクト指向によるソフトウェアイノベーションの5章「オブジェクト指向エクササイズ」を読みました。

さてさて,オブジェクト指向とは何でしょう? もっと言うと正しく奇麗で理想的なオブジェクト指向とは何でしょうか?

この章の2節では,筆者の経験に基づく9つのルールが述べられています。

1. 1つのメソッドにつきインデントは1段階までにすること
2. else句を使用しないこと
3. 全てのプリミティブ型と文字列型をラップすること
4. 1行につきドットは1つまでにすること
5. 名前を省略しないこと
6. すべてのエンティティを小さくすること
7. 1つのクラスにつきインスタンス変数は2つまでにすること
8. ファーストクラスコレクションを使用すること
9. Getter、Setter、プロパティを使用しないこと

これらのルールは,,5章2節の各小節においてその理由・背景が説明されています。 (こちらのリンク先の目次でそれぞれのルール名だけは確認できます。)

1つ目のルールはインデントの段数が深くなったらメソッド化すればいいのだな, 9つ目のルールは変数・クラス・メソッドの名前は大切だからその通りだなと,目次だけ読んでもなるほどと思えました。

他のルールでは,目次だけ読んだ段階では「えっ?」なんでと思ったり,読んでもまだ腑に落ちないものもありました。 さて,オブジェクト指向,正しく奇麗で理想的なオブジェクト指向とは何でしょうか?

じっくりもう一度勉強してみようと思います。

Unityでインスペクターから値や参照を設定したいけど,publicなフィールドは嫌。[SerializeFieldAttribute]

 結論。SerializeFieldAttribute属性をフィールドに設定することで,フィールドをpublicにしなくても,privateなフィールドでもインスペクターで値を設定できるようです。

現状の問題点

 Unityでは,Monobehaviorのサブクラスのpublicなフィールドの値・参照を,GUI(Inspector View)から設定できます。 (HierarchyView中やProjectViewのプレファブのGameObjectのコンポーネントになっている必要はあります。)

これは,下記のようなメリットがあります。

  • プログラマーでなくてもコーディングなしに,GUIから値を設定・操作できる
  • 結果を見ながら、GUIで簡単に値を調節できる

一方で,下記のようなデメリットもあります。

  • 値・参照が変更された時に任意の処理を行うことがプロパティではできるが,フィールドではできない。
  • Inspectorでの初期化はしたいが,他のクラス・オブジェクトからフィールドにアクセスしてほしくない場合など,そのような制限ができない。
  • アクセッサー(getter)とミューテーター(setter)に違うアクセス修飾子を設定できない。

 またC#では,publicなフィールドではなく,プロパティを使うべきとも言われています。

命名規約での解決法

これを解決するために,以下のような方法,というより命名規約を考えました。 (もっといい方法を後から述べます。)

  • 値・参照をInspector Viewから設定したいオブジェクトのpublicフィールドを定義する
  • フィールド名は,"_"をプレフィックスに持つこととする
  • 自動実装プロパティではなくて,_をプレフィックスに持つフィールドを用いたプロパティを作る
  • 他のクラスからは,_をプレフィックスに持つフィールドにはアクセスしない。

 このルールにより,Inspector Viewから値・参照を設定でき、アクセスはプロパティ経由からのみに制限ができます。(ただしルールを守れば。)また,適切なアクセス修飾子も,アクセスと同時に任意の処理を行うことも可能です。これを実現したサンプルは下記のようになります。

using UnityEngine;
using System.Collections;

namespace MRstar.Sample.UsingSerializeField
{
        public class FieldInt : MonoBehaviour
        {
                // This field is used for only initializing with inspector.
                public int _intField;

                public int Field {
                        get { return _intField; }
                        private  set { _intField = value; }
                }
        }
}

 とはいえ,「_」をプレフィックスに持つフィールドはpublicなフィールドなので,ルールで縛ったとしても,そのルールを下記のように破れば他のクラスから対象フィールドにアクセスすることは可能です。

// 想定したプロパティでのアクセス
Debug.Log (string.Format ("{0}", _fieldInt.Field));

// 想定していないpublicフィールドへのアクセス
Debug.Log (string.Format ("{0}", _fieldInt._intField));
_fieldInt._intField = 1;

困りました。

SerializeFieldAttribute

一応ルールとして縛りはしたけれど,実質アクセスできているので意味のないルールのようにも思えてきました。

困っていたら,というより悩んでいたところ,SerializeFieldAttributeという属性で解決できるということを教えてもらいました。

このSerializeFieldAttributeを使えば冒頭でも述べた通り,SerializeFieldAttributeを付与したprivateなフィールドをpublicなフィールドのようにInspector Viewから値・参照を設定できるようになります。

using UnityEngine;
using System.Collections;

namespace MRstar.Sample.UsingSerializeField
{
        public class SerializeFieldInt : MonoBehaviour
        {
                [SerializeField]    
                private int _intField;

                public int Field {
                        get { return _intField; }
                        private  set { _intField = value; }
                }
        }
}

使う側では,

// フィールドはprivateなので,プロパティでしかアクセスできない
Debug.Log (string.Format ("{0}", _serialzeFieldInt.Field));

です。 privateなフィールドですので,他のクラスからはアクセスはできません。 しかし,SeralizeFieldAttributeがついているのでInspector Viewから値・参照を設定できます。

便利な属性を教えてもらいました! 自分としては,publicなフィールドをつくることが若干,気持ち悪かったので,すっきりしました。 属性をつけるのと,プロパティを用意するというのは少し面倒ですが。

 サンプルプロジェクト: https://github.com/RyotaMurohoshi/unity_sample_serialize_field