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というクラスを作ります。
上の画像のように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で試しています。)
この記事はUnity4.3.2f1での記事です。ドキュメント化されていない仕様・挙動について他のバージョン・最新のバージョンでのUnityの動作を保証するものではありません。
3個のオーバーロード
Componentクラスのインスタンスメソッド,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コンポーネントをつけます。
これを実行すると,Derived0
とデバックログが出力されます。
サブクラスのコンポーネントが付与されているGameObjectに対して,スーパークラスの型を指定して,GetCompoenentでコンポーネントを取得することができました。
次のように,スーパークラスを抽象クラスにすることも可能なようです。
using UnityEngine; public abstract class Base : MonoBehaviour { }
また,次のようにDerive0とは違うサブクラスを作って,
public class Derived1 : Base { }
それを次のようにGetComponentSampleと一緒にGameObjectに付与して,
実行した場合,こちらは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個付与します。
これを実行すると,
デバックログにはvalue0
と表示されました。
インスペクターのMoveUpなどを用いて,コンポーネントの順番を変えます。
上の画像のような状態で実行するとデバックログにはvalue1
と表示されました。
また順番を変えて以下のようにします。
この状態で実行するとデバックログにはvalue2
と表示されました。
同じクラスのコンポーネントが複数付与されている場合,インスペクターにおける一番上のインスタンス(という言い方でいいのでしょうか?)が取得できるようです。
前の節で作ったBaseクラス,Derived0クラスそしてDerived1クラスを使ってみます。ふたたびBaseクラスでGetComponentして,クラス名を表示します。
using UnityEngine; public class GetComponentSample : MonoBehaviour { void Start () { Base component = GetComponent<Base> (); Debug.Log (component.GetType ().Name); } }
上の画像のように, GetComponentSampleクラス, Derived0クラス, Derived1クラス の順番でコンポーネントを付与します。
これを実行するとデバックログに,Derived0
と表示されました。
順番を変えます。
上の画像の用に 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); } } }
上の画像ようにGetComponentSampleを1個とStringContainerクラスのコンポーネントを複数付けて文字列valueを画像の用に設定し,実行すると
value0 value1 value2 value3
という順番で表示されました。
順番を変えます。
上の画像のように,順番を変えると実行結果は
value3 value2 value1 value0
となりました。
GetComponentsで取得した配列の要素の順番は,インスペクターで表示される順番と同じ順番のようです。
インタフェースでGetComponent
2019/03/17(日) 追記ここから
Unity 5.0からインターフェース型Tでも T GetComponent<T>()
が利用できるようになりました。
Unity 4系では、T GetComponent<T>()
のT
はComponent型の型引数制約が設定されていたため、インターフェースでのT GetComponent<T>()
はできませんでした。
もとの記事では、「インターフェースではできない」という旨の記述がありましたがUnity5からはできるので、その部分を削除しました。
追記ここまで
まとめとか
よく使う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
関連
「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から利用しています。一見問題がなさそうに見えます。 これらのクラスを普通に使う分には問題ないと思います。
次のような手順で問題が発生します。
- ConstansクラスとMainクラスをコンパイルする。
- 実行する。ここでは期待通り「Hello World!」と表示される。
- Constantsクラスの STRING_VALUEを「HELLO WORLD!!!!!」に書き換える。
- Constantsクラスのみコンパイルする。
- 実行する。「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つ目のルールは変数・クラス・メソッドの名前は大切だからその通りだなと,目次だけ読んでもなるほどと思えました。
他のルールでは,目次だけ読んだ段階では「えっ?」なんでと思ったり,読んでもまだ腑に落ちないものもありました。 さて,オブジェクト指向,正しく奇麗で理想的なオブジェクト指向とは何でしょうか?
じっくりもう一度勉強してみようと思います。