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

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

ListAdapterについて調べてみた(1) ArrayAdapter編

ArrayAdapterを継承して,getViewをオーバーライドしたり,Adapter内でViewHolderをインナークラスで定義したり,セルのビューのタグにホルダーをセットしたりなどは今回しません。ArrayAdapterを継承せずに,そして非常に少ないコード記述量で使ってみます。


ArrayAdapterクラスのコンストラクタは,TextView単体のレイアウトXMLファイルのidを引数にとったり,レイアウトXMLファイルのidとその中のTextViewのidを引数に取ったりするものがあります。リスト内のセルの構成要素がTextViewだけ,もしくは単一のTextViewの表示内容だけがセルごとに異なる場合,非常に少ないコード記述量でリストを表示できるようです。(もちろんArrayAdapterは,getViewメソッドをオーバーライドすることで,画像や複数のTextViewが一つのセル内にあるようなリストを作ることも可能です。)


シンプルな使い方

(多分一度は見たことがある)ArrayAdapterの基本的な使い方。
下記のようなTextViewのみで構成されているレイアウトXMLファイルを用意します。

res/layout/list_item_text_view.xml

<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/label"
    android:layout_width="match_parent"
    android:layout_height="40dp"
    android:gravity="center"
    android:text="Label" >

</TextView>


このレイアウトXMLファイルのidと,文字列の配列からArrayAdapterのインスタンスを作ります。(ActivityのonCreate内。thisはActivityのインスタンス)

String[] strings = new String[] {
	"日曜日", "月曜日", "火曜日", "水曜日", "木曜日", "金曜日", "土曜日"
};

int itemLayoutId = R.layout.list_item_text_view;
ArrayAdapter<String> adapter = new ArrayAdapter<String>(this, itemLayoutId, strings);

このAdapterを用いてテキストのみの簡単なリストを表示することが可能です。
また,配列でなくリストを用いるコンストラクタも用意されています。

ちなみに,試しにやってみたいけれどTextViewのみで構成されたレイアウトXMLファイルを作るのが面倒な場合,android.R.layout.textを使うといいと思います。



オブジェクトの文字情報をリストに表示

次のようなクラスを定義し,このクラスの文字情報をリスト内のセルに表示しします。

public class Person {
	private final String name;
	private final int age;

	Person(String name, int age) {
		this.name = name;
		this.age = age;
	}

	@Override
	public String toString() {
		return String.format("名前:%s  年齢:%02d歳", name, age);
	}
}

アダプターの作成は以下の通りです。
(ActivityのonCreate内。thisはActivityのインスタンス)

Person[] persons = new Person[] {
	new Person("太郎", 25),
	new Person("次郎", 22),
	new Person("三郎", 19),
	new Person("四郎", 16),
	new Person("五郎", 13),
};

int itemLayoutId = R.layout.list_item_text_view;
ArrayAdapter<Person> adapter = new ArrayAdapter<Person>(this, itemLayoutId, persons);

これをListViewにセットすると先頭のセルには下記のように表示されます。
名前:太郎 年齢:25歳

二番目以降のセルも,それぞれのPerson型のインスタンスのtoStringの結果がセルに表示されています。

実は「オブジェクトの文字情報をリストに表示」のコンストラクタと,最初に紹介した「シンプルな使い方」のコンストラクタは同一のものです。
使っているコンストラクタの定義は,

ArrayAdapter(Context context, int resource, T[] objects)

となっています。
前者はString型を,後者はPerson型の配列をコンストラクタの引数として渡しています。
String[]型の場合も他の型の配列の場合も,セルの表示の際に表示するデータ要素のオジェクトのtoStringメソッドを呼び出しています。
そのため,任意の型の文字情報を表示したい場合,ToStringメソッドを実装すればその情報をセルに表示することが可能です。


リソースで定義した文字列配列を表示する

リソースファイル内に定義した下記のような文字列配列

res/values/arrays.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>

    <string-array name="day_array">
        <item>日曜日</item>
        <item>月曜日</item>
        <item>火曜日</item>
        <item>水曜日</item>
        <item>木曜日</item>
        <item>金曜日</item>
        <item>土曜日</item>
    </string-array>

</resources>

これをリストで表示するためのAdapterを作成します。
(ActivityのonCreate内。thisはActivityのインスタンス)

int itemLayoutId = R.layout.list_item_text_view;
int textArrayResId = R.array.day_array;
ArrayAdapter<CharSequence> adapter = ArrayAdapter.createFromResource(this, textArrayResId, itemLayoutId);

ArrayAdapterのクラスメソッドを用いてインスタンスを生成出来ました。
上記は3行ですが,実質1行で書けますね。


カスタマイズしたセル中にテキスト情報を表示する

下記のようなレイアウトXMLファイルを定義します。
res/layout/list_item_text_in_layout.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="40dp" >

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerVertical="true"
        android:layout_toLeftOf="@+id/label"
        android:text="曜日:" />

    <TextView
        android:id="@+id/label"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerInParent="true"
        android:text="Label" >
    </TextView>

    <ImageView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentLeft="true"
        android:layout_centerVertical="true"
        android:layout_marginLeft="10dp"
        android:src="@drawable/ic_launcher" />

</RelativeLayout>

このレイアウトXMLファイルと,

ArrayAdapter(Context context, int resource, int textViewResourceId, T[] objects)

というコンストラクタを使います。(ActivityのonCreate内。thisはActivityのインスタンス)

String[] strings = new String[] {
	"日曜日", "月曜日", "火曜日", "水曜日", "木曜日", "金曜日", "土曜日"
};

int itemLayoutId = R.layout.list_item_text_in_layout;
int textViewId = R.id.label;
ArrayAdapter<String> adapter = new ArrayAdapter<String>(this, itemLayoutId, textViewId, strings);

ここで使っているコンストラクタは,第2引数にセルのレイアウトXMLファイルのidを,第3要素にセル内のTextViewのidを取ります。コンストラクタの第3引数で渡した配列の各要素の文字列は,第2要素で渡したidが示すTextView(ここではlabelというidのTextView)に表示されます。

これを使えば,セルに背景を設定したり,画像を複数個表示したり,他のTextViewを持つセルの表示も容易です。



まとめ

ArrayAdapter(もしくはBaseAdapter)のgetViewメソッドをオーバーライドしたり,内部にViewHolderを定義したり,セルのビューのタグにViewHolderをセットしたりして,リストを表示するのは,(定型パターンではありますが)やはり多少面倒です。
試しにささっとデータをリストで表示してみたい場合や,文字列のみが変わるリストを表示したい場合,ArrayAdapterを適切に使い少ないコード量でリストを作成すりことも可能です。


ただし,TextViewではなくImageViewの要素をセルごとに設定したい,セルごとに複数のTextViewの値を設定したい,画像と文字列両方セルごとに設定したい場合は,
getViewメソッドをオーバーライドするか,SimpleAdapterを使う必要があります。



参考:
http://developer.android.com/reference/android/widget/ArrayAdapter.html

Unityのシーンファイルの中身を見てみた (2)

Unityのシーンファイルの中身を見てみた(1)の続きです。


Unity 4.2から,無料版でもテキストベースでシーン・マテリアルなどを保存出来るようになり,バージョン管理システムなどで差分を確認しやすくなりました。Unityのシーンを編集した際に,シーンはテキストベースでその表現がどのように変化するかを確認してみました。



シーン中に空のゲームオブジェクトを置いた時,.unityファイルには,以下のようなテキストが追加されました。

--- !u!1 &1696434231
GameObject:
  m_ObjectHideFlags: 0
  m_PrefabParentObject: {fileID: 0}
  m_PrefabInternal: {fileID: 0}
  serializedVersion: 4
  m_Component:
  - 4: {fileID: 1696434232}
  m_Layer: 0
  m_Name: EmptyGameObject
  m_TagString: Untagged
  m_Icon: {fileID: 0}
  m_NavMeshLayer: 0
  m_StaticEditorFlags: 0
  m_IsActive: 1
--- !u!4 &1696434232
Transform:
  m_ObjectHideFlags: 0
  m_PrefabParentObject: {fileID: 0}
  m_PrefabInternal: {fileID: 0}
  m_GameObject: {fileID: 1696434231}
 m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
  m_LocalPosition: {x: 0, y: 0, z: 0}
  m_LocalScale: {x: 1, y: 1, z: 1}
  m_Children: []
  m_Father: {fileID: 0}+--- !u!1 &1696434231
GameObject:
  m_ObjectHideFlags: 0
  m_PrefabParentObject: {fileID: 0}
  m_PrefabInternal: {fileID: 0}
  serializedVersion: 4
  m_Component:
  - 4: {fileID: 1696434232}
  m_Layer: 0
  m_Name: MyGameObject
  m_TagString: Untagged
  m_Icon: {fileID: 0}
  m_NavMeshLayer: 0
  m_StaticEditorFlags: 0
  m_IsActive: 1
--- !u!4 &1696434232
Transform:
  m_ObjectHideFlags: 0
  m_PrefabParentObject: {fileID: 0}
  m_PrefabInternal: {fileID: 0}
  m_GameObject: {fileID: 1696434231}
  m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
  m_LocalPosition: {x: 0, y: 0, z: 0}
  m_LocalScale: {x: 1, y: 1, z: 1}
  m_Children: []
  m_Father: {fileID: 0}


さて,前回のおさらいになりますが,

--- !u!1 &1696434231

これは,!u!1でこのYAMLドキュメントがGameObject型の情報を表し,&の後でこのidが1696434231であることを表しているようです。

--- !u!4 &1696434232

同様に,!u!4でこのYAMLドキュメントがTransform型の情報を表し,&の後でこのidが1696434232であることを表しているようです。
(型と!u!の後のクラスidの対応はこちら。)




ではこのGameObject(以後,MyGameObject)のデフォルト状態のTransformを

  • Positionはx = 2, y = 3, z = 4
  • Rotationはx = 30, y = 10, z = 20
  • Scaleはx = 0.5, y = 0.1, z = 1.5

のように変更しました。

gitで差分を確認すると以下のようになりました。

-  m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
-  m_LocalPosition: {x: 0, y: 0, z: 0}
-  m_LocalScale: {x: 1, y: 1, z: 1}
+  m_LocalRotation: {x: .268535882, y: .0381345674, z: .144878149, w: .951548576}
+  m_LocalPosition: {x: 2, y: 3, z: 4}
+  m_LocalScale: {x: .5, y: .100000001, z: 1.5}

Position,Rotation,Scaleそれぞれの要素がYAMLのフロー形式で表現されているようです。

RotationはEditor上では,x,y,zの3つの入力項目あります。TransformクラスのlocalRotationはQuertanion型で定義されています。YAMLではm_LocatlRotationがクォータニオン型として定義されているためからか,x,y,z,wの4つの項目で表現されています。
設定されているコンポーネントの値を変えるくらいだったら,差分は非常に確認しやすいですね。



次にMyGameObjectにRigidbodyコンポーネントを付与してみましす。

以下のようなRigidbodyコンポーネントの情報・設定を表現する部分が.unityファイルに追加されました。54はRigidbodyクラスのクラスidです。

+--- !u!54 &1696434233
+Rigidbody:
+  m_ObjectHideFlags: 0
+  m_PrefabParentObject: {fileID: 0}
+  m_PrefabInternal: {fileID: 0}
+  m_GameObject: {fileID: 1696434231}
+  serializedVersion: 2
+  m_Mass: 1
+  m_Drag: 0
+  m_AngularDrag: .0500000007
+  m_UseGravity: 1
+  m_IsKinematic: 0
+  m_Interpolate: 0
+  m_Constraints: 0
+  m_CollisionDetection: 0

これに加えて,MyGameObjectのYAMLドキュメント中に以下のような追加がありました。

   (略)
   serializedVersion: 4
   m_Component:
   - 4: {fileID: 1696434232}
+  - 54: {fileID: 1696434233}
   m_Layer: 0
   m_Name: EmptyGameObject
   (略)

m_Componentは,配列になっているようです。
その配列の要素はマップになっていて,キーがコンポーネントのクラスidで,バリューがさらにマップになっているようです。
そのマップは,fileIDという文字列をキーに持ち,YAMLドキュメントのidをバリューに持つようです。

MyGameObjectはTransform型(id : 4)とRigidbody型( id : 54)が付与されていて,それぞれのコンポーネントの値・情報はidが1696434232,1696434233のYAMLドキュメントにあるということが表現されているようです。

実際に,さきほどidが1696434233のRigidbodyの情報を表現するYAMLドキュメントが追加されています。




次に,空のゲームオブジェクトをMyGameObjectに子オブジェクトとして追加します。

+--- !u!1 &141034563
+GameObject:
  (略)
+--- !u!4 &141034564
+Transform:
  (略)
+  m_Father: {fileID: 1696434232}

.unityファイルに空のゲームオブジェクトとそのTransformを表すYAMLドキュメントが追記されました。

また,MyGameObjectに

--- !u!1 &1696434231
GameObject:
  (略)
   m_LocalScale: {x: .5, y: .600000024, z: 1.5}
-  m_Children: []
+  m_Children:
+  - {fileID: 141034564}
   m_Father: {fileID: 0}

以下のような追記がされました。

MyGameObjectのTransformのYAMLドキュメントのidは1696434232。新たに追記した空のゲームオブジェクトのTransformのYAMLドキュメントのidは141034564です。
MyGameObjectのm_Childrenでは,新たに追加した空のゲームオブジェクトのTransfromを参照しています。
新たに追加した空のゲームオブジェクトのm_Fatherでは,MyObjectのTransfromを参照しています。

親子関係は,Transformで関係を作っていることが.unityでもわかります。




コンポーネントのプロパティの値の変更,ゲームオブジェクトへのコンポーネントの追加,子オブジェクトの追加でどのように差分があるのかを確認してみました。
コンポーネントのプロパティの値の変更くらいだったら,差分は追いやすいのですが,ゲームオブジェクトを新規追加すると,一つにつきGameObjectとTransformのYAMLドキュメントが必ず作成されるので差分が非常に多くなりますね。




次は,自作コンポーネントでの差分とプレハブなどの差分を確認したいと思います。


それではまた会えることを祈りつつ

ListViewが一番下までスクロールした時点で追加要素を読み込むようにしたら,スクロールしていないのに要素が1回追加されていた

結論から言うと原因は,AbsListView.OnScrollListenerのonScrollメソッドの仕様を勘違いしていて,このメソッドがスクロールしていなくても呼ばれていたためのようです。


Twitterクライアントのようなものを想定しています。
ListViewが一番下までスクロールした時点で追加要素を取得し,読み込み,リストビューに加えようとしていました。

こちらのブログを参考に,実装させていただきました。
http://visible-true.blogspot.jp/2010/12/listview.html



さて,

ActivityのonCreateメソッド内で次のように書いていました。

listView.addFooterView(LayoutInflater.from(footerViwe);
listView.setOnScrollListener(new OnScrollListener() {
	@Override
	public void onScroll(
		AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
			boolean isLastItemVisible = totalItemCount == firstVisibleItem + visibleItemCount;
			if (isLastItemVisible && !isLoading()) {
				loadNewItems();
			}
		}
	}

	@Override
	public void onScrollStateChanged(AbsListView view, int scrollState) {
	}
});
setListAdapter(adapter);

loadNewItems();

footerViewにはプログレスバーなどが含まれています。loadItemsメソッド内で,追加要素をロードして,adapterのnotifyDatasetChangedメソッドを読んでいます。isLoading()メソッドは,名前通り追加要素を読み込み中かどうか判断します。onScrollメソッドが複数回呼ばれるため,複数回ロードされるのを防ぐための処理です。



このように,フッターでリストの末尾に常にプログレスバーを表示して,末尾までスクロールしたら追加要素のロードを開始するようにしていました。



さてこのようなコードで初期状態を20個,スクロールするごとに要素を20個追加しようとしました。リストの最後までスクロールするごとに,正しく20個要素が追加されます。


しかし,なぜ初期状態要素が40個になっていました。


どうやらloadNewItemsメソッドがスクロールしていなくても2回呼ばれいるようです。
調べてみると,

public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) 

が,スクロールしていないのに複数回呼ばれていました。
そのうちの最初の1回は引数のfirstVisibleItem, visibleItemCount, totalItemCountは全て0になっており,そのタイミングでloadItemsメソッドが呼ばれてしまっていました。

そのため,onCreate内で1回目とOnScrollListenerのonScroll内で2回目,計2回loadItemsがよばれて初期の状態が40個の要素が入ってしまっていたようです。


また,どうやらユーザーが画面をスクロールしていなくても,ListViewに要素が追加されたタイミングでもonScrollが呼ばれているようです。




このように,onScrollメソッドの呼ばれるタイミングの仕様を勘違いしていたことが原因のようです。onCreateの中のloadItemsメソッドの呼び出しを消して解消しました。

Unityのシーンファイルの中身を見てみた (1)

Unity 4.2から今まで有料のPro版でしか使えなかった,マテリアル、プレハブ、シーンなどのテキストベースでのシリアルライズが無料版でも利用出来るようになりました。
(http://japan.unity3d.com/unity/whats-new/unity-4.2を参照)
無料版において,gitなどを用いたバージョン管理がしやすくなるかと思います。


今回は下記のページの内容を確認したり,シーン(.unityファイル)がどのようにシリアルライズされているかを確認してみました。(リンク先では,Pro専用機能とありますが(2013年7月31日時点),4.2ではしっかりと使えました。)
http://docs-jp.unity3d.com/Documentation/Manual/TextualSceneFormat.html


まず,シリアルライズ形式のYAML形式を非常に簡単に。

YAMLで配列は

- 1
- 2
- 3

という形式で表され,マップは,

a: 1
b: 2
c: 3

となります。

配列の配列は

- 
  - a
  - b
- 
  - c
  - d
  - e

で,マップのマップは

U:
  a: 1
  b: 2
V:
  c: 3
  d: 4
  e: 5

となるようです。

また,配列のマップは

U: 
  - a
  - b
V: 
  - c
  - d
  - e

そして,マップの配列は次の用になります。

-
  a: 1
  b: 2
-
  c: 3
  d: 4
  e: 5

フロー形式という記述方法もあるらしく

配列

[1, 2, 3]

マップ

{a: 1 , b: 2, c: 3}

配列の配列

- [a, b]
- [c, d, e]
[[a, b], [c, d, e]]

マップのマップ

U: {a: 1, b: 2}
V: {c: 3, d: 4, e: 5}
{U: {a: 1, b: 2}, V: {c: 3, d: 4, e: 5}}

配列のマップ

U: [a, b]
V: [c, d, e]
{U: [a, b], V: [c, d, e]}

マップの配列

- {a: 1, b: 2}
- {c: 3, d: 4, e: 5}
[{a: 1, b: 2}, {c: 3, d: 4, e: 5}]

だそうです。


また,---がYAMLドキュメントの区切りになっていて,これにより下記のように一つのテキストファイルに複数のYAMLドキュメントを含むことができるようです。

---
name: Taro
age : 25
---
name: Jiro
age : 22
---
name: Saburo
age : 19


さて,やっと本題。
Unityのシーンファイル(.unityファイル)がどのようにYAMLで表されているかを見てみました。

先頭は以下の2行のようになっていました。

%YAML 1.1
%TAG !u! tag:unity3d.com,2011:

http://docs-jp.unity3d.com/Documentation/Manual/YAMLSceneExample.html
によるとシーンファイルはこの2行で始まらないといけないようです。

1行目は,YALMLディレクティブ,2行目は,タグディレクティブだそうです。
http://yaml.org/spec/1.2/spec.html#id2781553
http://yaml.org/spec/1.2/spec.html#id2782090



さて,シーン中のゲームオブジェクトそしてコンポーネントは一つのYAMLオブジェクトとして定義されるようです。

---の後に

!u!1 &6

だとか

!u!4 &8

等の文字列が含まれます。
この!u!のあとの最初の数字は下記のYAMLドキュメントのクラスを表し,そのあとの&のはシーンファイル中の一意のID番号だそうです。

下記は空のゲームオブジェクトをEmptyGameObjectという名前で,原点(0,0,0)に置いた際のシーンファイルの増分です。

--- !u!1 &1090478263
GameObject:
  m_ObjectHideFlags: 0
  m_PrefabParentObject: {fileID: 0}
  m_PrefabInternal: {fileID: 0}
  serializedVersion: 4
  m_Component:
  - 4: {fileID: 1090478264}
  m_Layer: 0
  m_Name: EmptyGameObject
  m_TagString: Untagged
  m_Icon: {fileID: 0}
  m_NavMeshLayer: 0
  m_StaticEditorFlags: 0
  m_IsActive: 1
--- !u!4 &1090478264
Transform:
  m_ObjectHideFlags: 0
  m_PrefabParentObject: {fileID: 0}
  m_PrefabInternal: {fileID: 0}
  m_GameObject: {fileID: 1090478263}
  m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
  m_LocalPosition: {x: 0, y: 0, z: 0}
  m_LocalScale: {x: 1, y: 1, z: 1}
  m_Children: []
  m_Father: {fileID: 0}

GameObjectを表すYAMLドキュメントと,Transfrom表すYAMLドキュメントが確認できます。
それぞれのドキュメントはマップになっていてます。Transformを示すYAMLドキュメントでは,キーがTransformひとつ。そのバリューもマップになっていてTransformクラスのプロパティ名にm_をつけた物が並んでいます。
GameObjectも同じような感じです。


今回はこの辺で。

また次回があることを祈りつつ。


[参考リンク]
http://magazine.rubyist.net/?0009-YAML
http://yaml.org/spec/1.2/spec.html
http://yamllint.com/
http://d.hatena.ne.jp/tociyuki/20121130/1354278408

Photonを用いてオブジェクトの座標を同期してみる

Unity上でPhotonを使って,ネットゲームでのオブジェクトの座標の同期に挑戦してみました。



Photon Cloud公式のチュートリアルを見たところ(http://doc.exitgames.com/photon-cloud-jp/TutorialMarcoPolo/#cat-tutorialsなど)ネットゲームの大切な要素がたくさん詰まって(いそうだということが分かり)ました。(あまりしっかり理解できませんでした。)
大事な要素を少しずつに分けて確認したいと思います。



今回は必要最低限の構成で,プレイヤー間のオブジェクトの座標の同期を行うプロジェクトを作ってみました。



やれることとしては,

  • 名称固定な部屋を作れる(詳しくは,こちらを)
  • (存在している場合),名称固定な部屋に入れる
  • 部屋から抜ける
  • スペースキー(Androidの場合は画面タップで)で,Playerオブジェクト(Cupsule)が真上にジャンプ
  • 同じ部屋にいる他のユーザーのオブジェクトの状態が見える

です。



次に作成手順です。
Photon Cloud AppIdとプロジェクトの設定は
プロジェクトの設定はこちらのページがとても参考になりました。


作成手順


  1. プロジェクトを新規作成,AppIdを設定
  2. 空のsceneを作成
  3. PlaneとDirectinal Lightをおく
  4. 空のGameObjectを作成し,名前をGameManagerにする
  5. 下記のMainMenuとGameManagerをGameManagerに追加
  6. Capsuleをおき,名前をPlayerに
  7. PlayerにJumperスクリプトを追加
  8. PlayerにPhoton Unity NetworkingのPhotonViewスクリプトを追加
  9. PlayerのPhotoViewのObserverインスペクター中に,Player自身をドラッグアンドドロップ
  10. Assets以下にResourcesディレクトリを作成。その中にPlayerをプレハブ化
  11. シーン中のPlayerを削除

    こここまでである程度同期するけれど,カクカクしてしまいます。

  12. NetworkPlayer.csを作成し,Playerにコンポーネント追加

    OnPhotonSerializeViewが呼ばれていません。プルプルしてまいす。(オブジェクトが本来あるべき点と原点とを行ったり来たりしているように見える)

  13. PlayerのインスペクターのObserver変更
    Player中のObserにPlayer中のNetworkコンポーネントをドラッグアンドドラッグして,Player中のObserveを変更します。

PlayerやFloorにMaterialをつけた方が見やすいかと。


ソースコードを張っておきます。
おかしい点があったら,教えていただけると嬉しいです。


以下,ソースコード

GameManager.cs
using UnityEngine;
using System.Collections;

public class GameManager : Photon.MonoBehaviour
{
	public string playerPrefabName = "Player";

	void OnJoinedRoom ()
	{
		StartGame ();
	}
    
	IEnumerator OnLeftRoom ()
	{
		while (PhotonNetwork.room!=null || PhotonNetwork.connected==false) {
			yield return 0;
		}
		
		Application.LoadLevel (Application.loadedLevel);
	}
	
	void OnGUI ()
	{
		if (PhotonNetwork.room == null) {
			return;
		}

		if (GUILayout.Button ("Leave & QUIT")) {
			PhotonNetwork.LeaveRoom ();
		}
	}

	void StartGame ()
	{
		PhotonNetwork.Instantiate (playerPrefabName, getPositionRamdomly (), Quaternion.identity, 0);
	}
	
	Vector3 getPositionRamdomly ()
	{
		return new Vector3 (Random.Range (-3.0f, +3.0f), 1, Random.Range (-3.0f, +3.0f));
	}
}
Jumper.cs
using UnityEngine;
using System.Collections;

[RequireComponent(typeof(PhotonView))]
[RequireComponent(typeof(Rigidbody))]
public class Jumper : Photon.MonoBehaviour
{
	void Awake ()
	{
		if (!photonView.isMine) {
			rigidbody.isKinematic = true;
		}
	}
	
	void Update ()
	{		
		if (Input.GetKeyDown (KeyCode.Space) || IsTapScreen ()) {
			rigidbody.AddForce (transform.up * 10.0f, ForceMode.Impulse);
		}
	}
	
	bool IsTapScreen ()
	{
		if (Input.touchCount <= 0) {
			return false;	
		}
		
		foreach (Touch touch in Input.touches) { 
			if (touch.phase == TouchPhase.Began) {
				return true;
			}
		}
		return false;
	}
}
MainMenu.cs
using UnityEngine;
using System.Collections;

public class MainMenu : MonoBehaviour
{
	private const string ROOM_NAME = "JumpingSampleRoom";

	void Awake ()
	{
		if (!PhotonNetwork.connected) {
			PhotonNetwork.ConnectUsingSettings ("v1.0");
		}
		PhotonNetwork.playerName = PlayerPrefs.GetString ("playerName", "Guest" + Random.Range (1, 9999));
	}

	void OnGUI ()
	{
		if (!PhotonNetwork.connected) {
			return;
		}

		if (PhotonNetwork.room != null) {
			return;
		}

		GUILayout.BeginArea (new Rect ((Screen.width - 400) / 2, (Screen.height - 300) / 2, 400, 300));

		GUILayout.Label ("Main Menu");

		{
			GUILayout.BeginHorizontal ();
			GUILayout.Label ("Player name:", GUILayout.Width (150));
			PhotonNetwork.playerName = GUILayout.TextField (PhotonNetwork.playerName);
			if (GUI.changed) {
				PlayerPrefs.SetString ("playerName", PhotonNetwork.playerName);
			}
			GUILayout.EndHorizontal ();
		}	
		
		GUILayout.Space (15);

		{	
			GUILayout.BeginHorizontal ();
			GUILayout.Label ("JOIN ROOM:", GUILayout.Width (150));
			GUILayout.Label (ROOM_NAME, GUILayout.Width (150));
			if (GUILayout.Button ("GO")) {
				PhotonNetwork.JoinRoom (ROOM_NAME);
			}
			GUILayout.EndHorizontal ();
		}
		
		GUILayout.Space (15);

		{
			GUILayout.BeginHorizontal ();
			GUILayout.Label ("CREATE ROOM:", GUILayout.Width (150));
			GUILayout.Label (ROOM_NAME, GUILayout.Width (150));
			if (GUILayout.Button ("GO")) {
				PhotonNetwork.CreateRoom (ROOM_NAME, true, true, 10);
			}
			GUILayout.EndHorizontal ();
		}
        
		GUILayout.EndArea ();
	}
}
NetworkCharacter.cs
using UnityEngine;
using System.Collections;

[RequireComponent(typeof(PhotonView))]
public class NetworkCharacter : Photon.MonoBehaviour
{
	private Vector3 correctPlayerPosision = Vector3.zero;

	void Update ()
	{
		if (!photonView.isMine) {
			transform.position = Vector3.Lerp (transform.position, correctPlayerPosision, Time.deltaTime * 5);
		}
	}
	
	void OnPhotonSerializeView (PhotonStream stream, PhotonMessageInfo info)
	{
		if (stream.isWriting) {
			stream.SendNext (transform.position);
		} else {
			correctPlayerPosision = (Vector3)stream.ReceiveNext();
		}
	}
}


それでは,また会えることを祈りつつ。

ArrayListの中身を見てみた(1)

ArrayList

完全修飾名で言うと,

java.util.ArrayList

いつもお世話になっているこのクラスの実装がどうなっているのか?

気になったので中身を見てみました。



参照したのは,Android SDKの以下の
リファレンスは
docs/reference/java/util/ArrayList.html
ソースコードは,
sources/android-16/java/util/ArrayList.java



ArrayListクラスは,
AbstractListを継承していて,

  • Cloneable
  • Serializable
  • RandomAccess

インターフェースを実装しています。

また,スーパークラスが実装しているインターフェースには,

  • Collection
  • List

インターフェースなどがあります。





コンストラクタ

とりあえずコンストラクタから。

  1. public ArrayList(int capacity)

  引数capacityの値を初期容量とするコンストラクタ

  1. public ArrayList()

  0を初期容量とするコンストラクタ

  1. public ArrayList(Coolection collection)

  コレクションを引数に取り,そのコレクションの要素を含むインスタンスを作るコンストラクタ

の3種類があります。


2.と3.のコンストラクタについてですが,
Collectionインターフェースのドキュメントによると,
Collectionインターフェースを実装するクラスは少なくとも二つのコンストラクタを持つべきだそうです。
一つは引数なしで空のコレクションを作るコンストラクタ(2番)。
もう一つは,引数のコレクションと同じ要素を初期構成要素とするコレクションを作るコンストラクタ(3番)

3番のコンストラクタは,以下のように使うことができます.

public static void main(String[] args) {
        ArrayList<String> list;

        Map<String, String> map = new HashMap<String, String>();
        map.put("a", "Taro");
        map.put("b", "Jiro");
        map.put("c", "Saburo");

        list = new ArrayList<String>(map.values());
        System.out.println(list);

        list = new ArrayList<String>(map.keySet());
        System.out.println(list);

        Set<String> set = new HashSet<String>();
        set.add("Tokyo");
        set.add("Aichi");
        set.add("Osaka");
        set.add("Fukuoka");
        list = new ArrayList<String>(set);
        System.out.println(set);
}

容量を表すsize変数とオブジェクトを格納する配列

ArrayListクラスは次のようなフィールドを持ちます.

  • 今いくつの要素を保持しているかを表すint型のsize変数
  • オブジェクトを保持するObject[]型のarray

他にもありますが...
それはまた別の機会に。



sizeフィールドは,

例えば
ArrayListが何も保持していなければtrueを返す
public boolean isEmpty()
メソッドの中で,

@Override public boolean isEmpty() {
    return size == 0;
}

のようにs使っていたり,

リスト中の特定の位置の要素を取り出すgetメソッドでは,

@SuppressWarnings("unchecked") @Override public E get(int index) {
    if (index >= size) {
        throwIndexOutOfBoundsException(index, size);
    }
    return (E) array[index];
}

の用に,
不正な引数のチェックに使ったりしています。


arrayフィールドは,
オブジェクトの保持に使っていて,

@Override public boolean add(E object) {
    Object[] a = array;
    int s = size;
    if (s == a.length) 
          Object[] newArray = new Object[s +
                (s < (MIN_CAPACITY_INCREMENT / 2) ?
                 MIN_CAPACITY_INCREMENT : s >> 1)];
        System.arraycopy(a, 0, newArray, 0, s);
        array = a = newArray;
    }
    a[s] = object;
    size = s + 1;
    modCount++;
    return true;
}

addメソッドは,arrayに追加したい要素を加えます。
保持している要素の最後尾に新しい要素を加えます。
必要であればその途中でより大きな配列を生成して,
今まで保持していた要素をコピーすることで,
arrayのサイズをより大きくします。


ArrayList固有のメソッド

addメソッド・addAllメソッドを使うとフィールドarrayはどんどん大きくなっていきます。
ArrayListインスタンスの要素数・内部のarray型の配列長が10000になったとします。
その後removeメソッドで9000個の要素が削除され,要素数が1000になったとします。
この時,要素数1000に対してarrayの配列長は10000のままです。


このように大きくなってしまった内部の配列arrayのサイズを今の要素数のサイズまで小さくするメソッドがあります。
public void trimToSize()メソッドです。

    public void trimToSize() {
        int s = size;
        if (s == array.length) {
            return;
        }
        if (s == 0) {
            array = EmptyArray.OBJECT;
        } else {
            Object[] newArray = new Object[s];
            System.arraycopy(array, 0, newArray, 0, s);
            array = newArray;
        }
        modCount++;
    }





内部のarrayを小さくするメソッドもあれば,大きくするメソッドもあります。
addメソッドでちらっと触れたのですが,
要素を追加するときにArrayList型のインスタンスは必要があれば,
内部の配列arrayを大きくします。

非常に大きい配列を以下のようにArrayListに格納する時に,

// ids Integer型の配列 非常に要素数が多い
// list Integer型を格納するArrayList
for (Integer id : ids) {
    list.add(id);
}

バックグラウンドでは,内部の配列arrayの拡張が複数回実行されてしまいます。
内部の配列の拡張は,より大きな配列の作成と今まで使っていた配列から新たな配列へのコピーが行われます。
内部の配列の拡張は少ないにこしたことはありません。
そこで,この後必要になる要素数が分かっていて,内部の配列がその大きさを確保したいときは,public void ensureCapacity(int minimumCapacity)メソッドが使えます。

public void ensureCapacity(int minimumCapacity) {
    Object[] a = array;
    if (a.length < minimumCapacity) {
        Object[] newArray = new Object[minimumCapacity];
        System.arraycopy(a, 0, newArray, 0, size);
        array = newArray;
        modCount++;
     }
}


以下のようにすれば,複数回内部の配列の拡張が行われることはありません。

// ids Integer型の配列 非常に要素数が多い
// list Integer型を格納するArrayList
list.ensureCapacity(ids.length);
for (Integer id : ids) {
    list.add(id);
}

また,前述したコンストラクタを以下のように使えば,配列の拡張の回数を減らすことができます.

ArrayList<Integer> list = new ArrayList<Integer>(ids.length);
for (Integer id : ids) {
    list.add(id);
}



まだまだ気になることがたくさんありましたので,
何回かArrayListを続けていこうと思います。


では,また会えることを祈りつつ.

Gsonのユーザーガイドを見てみた (3)

前回,前々回に引き続き,Gsonのユーザーガイドを見ていきます。



Person.java

package com.rmstar.gson.userguide;

public class Person {
    private String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public String toString() {
        return String.format("%s %d才", name, age);
    }
}

サンプルとして,このクラスを使います。





JSONの表示フォーマット.単純な形式と分かりやすい形式
package com.rmstar.gson.userguide;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;

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

public class PrettyPrinting {
    public static void main(String[] args) {
        Gson gson;

        Person person = new Person("Alice", 25);

        List<Person> personList = new ArrayList<Person>();
        personList.add(new Person("Taro", 20));
        personList.add(new Person("Jiro", 17));
        personList.add(new Person("Saburo", 14));

        System.out.println("===Compact===");
        gson = new Gson();
        System.out.println(gson.toJson(person));
        System.out.println(gson.toJson(personList));

        System.out.println();

        System.out.println("===Pretty Printing===");
        gson = new GsonBuilder().setPrettyPrinting().create();
        System.out.println(gson.toJson(person));
        System.out.println(gson.toJson(personList));
    }
}

実行結果は,

===Compact===
{"name":"Alice","age":25}
[{"name":"Taro","age":20},{"name":"Jiro","age":17},{"name":"Saburo","age":14}]

===Pretty Printing===
{
  "name": "Alice",
  "age": 25
}
[
  {
    "name": "Taro",
    "age": 20
  },
  {
    "name": "Jiro",
    "age": 17
  },
  {
    "name": "Saburo",
    "age": 14
  }
]


となります。

new Gson().toJson(object)

として生成したGsonでの出力文字列は,空白も改行もありません。

new GsonBuilder().setPrettyPrinting().create().toJson(object)

一方で,GsonBuilderを用いてGsonクラスのインスタンスを生成し,
setPrettyPrinting()を行った後にGson型のインスタンスを生成すれば,
上記のように空白が挿入されたり改行付きで表示されたりします。



nullオブジェクトのサポート
package com.rmstar.gson.userguide;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;

public class NullObjectSupport {
    public static void main(String[] args) {

        Person person = new Person(null, 20);
        Gson gson;

        gson = new Gson();
        System.out.println(gson.toJson(person));

        gson = new GsonBuilder().serializeNulls().create();
        System.out.println(gson.toJson(person));
    }
}

実行結果

{"age":20}
{"name":null,"age":20}

普通にGsonを用いた場合はnullのフィールドは無視されます。
nullを出力したい場合は,
Gsonクラスをインスタンス化する際,

new GsonBuilder().serializeNulls().create()

とすれば,nullは無視されなくなります。



バージョニング
package com.rmstar.gson.userguide;

import com.google.gson.annotations.Since;

public class VersionedObject {
    private final String v1;

    @Since(2)
    private final String v2;

    @Since(3)
    private final String v3;

    public VersionedObject() {
        v1 = "version1";
        v2 = "version2";
        v3 = "version3";
    }
}

各フィールドに@Sinceアノテーションを上記のように記述して,

package com.rmstar.gson.userguide;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;

public class VersioningSupport {
    public static void main(String[] args) {
        Gson gson;
        VersionedObject object = new VersionedObject();

        gson = new Gson();
        System.out.println(gson.toJson(object));

        gson = new GsonBuilder().setVersion(1).create();
        System.out.println(gson.toJson(object));

        gson = new GsonBuilder().setVersion(2).create();
        System.out.println(gson.toJson(object));

        gson = new GsonBuilder().setVersion(3).create();
        System.out.println(gson.toJson(object));
    }
}

上記のように,gsonをインスタンス化すれば,

{"v1":"version1","v2":"version2","v3":"version3"}
{"v1":"version1"}
{"v1":"version1","v2":"version2"}
{"v1":"version1","v2":"version2","v3":"version3"}

実行結果はこのようになります。

@Sinceアノテーションを用いれば,
同じオブジェクト・コードでバージョンによる差異を上手に扱えるみたいです。

もしGsonのインスタンスにバージョンの設定がされていないのであれば,
バージョンに関わらず全てのフィールドがシリアライズ・デシリアライズされるようです。


@Sinceアノテーションはクラスに設定することもできるようです。

package com.rmstar.gson.userguide;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.annotations.Since;

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

public class VersioningSupport {

    private static class V1 {
        final String v = "version1";
    }

    @Since(2)
    private static class V2 {
        final String v = "version2";
    }

    @Since(3)
    private static class V3 {
        final String v = "version3";
    }

    public static void main(String[] args) {
        Gson gson;

        List<Object> list = new ArrayList<Object>();
        list.add(new V1());
        list.add(new V2());
        list.add(new V3());

        System.out.println("===Version1===");
        gson = new GsonBuilder().setVersion(1).create();
        System.out.println(gson.toJson(list));

        System.out.println("===Version2===");
        gson = new GsonBuilder().setVersion(2).create();
        System.out.println(gson.toJson(list));

        System.out.println("===Version3===");
        gson = new GsonBuilder().setVersion(3).create();
        System.out.println(gson.toJson(list));
    }
}

実行結果は,

===Version1===
[{"v":"version1"},null,null]
===Version2===
[{"v":"version1"},{"v":"version2"},null]
===Version3===
[{"v":"version1"},{"v":"version2"},{"v":"version3"}]

となりました。





フィールドのネーミング
package com.rmstar.gson.userguide;

public class Animal {
    private final String mName;
    private final int mWeight;

    public Animal(String name, int weight) {
        mName = name;
        mWeight = weight;
    }

}

このようなクラスがあったとして,

package com.rmstar.gson.userguide;

import com.google.gson.Gson;

public class JSONFieldNamingSupport {
    public static void main(String[] args) {
        Animal pochi = new Animal("Pochi", 35);
        System.out.println(new Gson().toJson(pochi));
    }
}

この様にGsonを使うと,

{"mName":"Pochi","mWeight":35}

こうなります。
命名規約などでフィールドにmをつけなくてはいけない場合,
Gsonでの出力にもmがついてしまいます。



Animalクラスを次の用に書き換えます。

package com.rmstar.gson.userguide;

import com.google.gson.annotations.SerializedName;

public class Animal {
    @SerializedName("name")
    private final String mName;

    @SerializedName("weight")
    private final int mWeight;

    public Animal(String name, int weight) {
        mName = name;
        mWeight = weight;
    }

}

実行結果は以下のようになりました。

{"name":"Pochi","weight":35}

クラスのフィールド名は元のままですが,GsonでのJSON形式の文字列はmを取ることが出来ました。



シリアライズ・デシリアライズでのフィールドの除外

Gsonではシリアライズ・デシリアライズにおいて,
クラス・フィールド・ある型のフィールドを除外するための多くの仕組みが準備されています。




まずは,修飾子単位での除外

シリアライズの対象は以下のようなクラス。

package com.rmstar.gson.userguide;

public class Example1 {
    private String v0;
    static String v1;
    volatile String v2;
    transient String v3;
    static final String v4;

    static {
        v1 = "static";
        v4 = "constant";
    }

    public Example1() {
        v0 = "private";
        v2 = "volatile";
        v3 = "transient";
    }
}

これを普通にGsonを用いてシリアライズした場合,

{"v0":"private","v2":"volatile"}

となります。
static修飾子とtransient修飾子がついたフィールドは,無視されます。

無視したい修飾子を設定したい場合だとか,
逆にstaticなフィールドを無視したくない場合は,
GsonBuilderでGsonインスタンスを作成する際,
以下のように無視する修飾子を設定することが可能です。

package com.rmstar.gson.userguide;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;

import java.lang.reflect.Modifier;

public class ExcludingFields1 {
    public static void main(String[] args) {
        Gson gson;
        Example1 ex1 = new Example1();

        System.out.println("===Exclude Private===");
        gson = new GsonBuilder()
                .excludeFieldsWithModifiers(Modifier.PRIVATE)
                .create();
        System.out.println(gson.toJson(ex1));

        System.out.println("===Exclude Static===");
        gson = new GsonBuilder()
                .excludeFieldsWithModifiers(Modifier.STATIC)
                .create();
        System.out.println(gson.toJson(ex1));

        System.out.println("===Exclude Volatile and Transient===");
        gson = new GsonBuilder()
                .excludeFieldsWithModifiers(Modifier.VOLATILE | Modifier.TRANSIENT)
                .create();
        System.out.println(gson.toJson(ex1));

        System.out.println("===Exclude Constant===");
        gson = new GsonBuilder()
                .excludeFieldsWithModifiers(Modifier.FINAL, Modifier.STATIC)
                .create();
        System.out.println(gson.toJson(ex1));
    }
}

実行結果は,以下の通りです。

===Exclude Private===
{"v1":"static","v2":"volatile","v3":"transient","v4":"constant"}
===Exclude Static===
{"v0":"private","v2":"volatile","v3":"transient"}
===Exclude Volatile and Transient===
{"v0":"private","v1":"static","v4":"constant"}
===Exclude Constant===
{"v0":"private","v2":"volatile","v3":"transient"}




次に,@Exposeアノテーションを用いての除外
@Exposeアノテーションフィールを用いれば,
フィールド単位でシリアライズ・デシリアライズするフィールドを指定出来ます。

Gsonのインスタンス化を

new GsonBuilder()
.excludeFieldsWithoutExposeAnnotation()
.create();

のように行った場合,
@Exposedアノテーションがついたフィールドのみ
シリアライズ・デシリアライズされます。

package com.rmstar.gson.userguide;

import com.google.gson.annotations.Expose;

public class Example2 {
    @Expose
    private final String expose;

    private final String notExpose;

    public Example2() {
        expose = "expose";
        notExpose = "not expose";
    }
}

このようなクラスをシリアライズします。

package com.rmstar.gson.userguide;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;

public class ExcludingFields2 {
    public static void main(String[] args) {
        Gson gson;
        Example2 ex2 = new Example2();

        gson = new Gson();
        System.out.println(gson.toJson(ex2));

        gson = new GsonBuilder()
                .excludeFieldsWithoutExposeAnnotation()
                .create();
        System.out.println(gson.toJson(ex2));
    }
}


実行結果は,以下のようになります。

{"expose":"expose","notExpose":"not expose"}
{"expose":"expose"}




独自に定義するExclusion Strategyによる除外を紹介します。

package com.rmstar.gson.userguide;

import com.google.gson.ExclusionStrategy;
import com.google.gson.FieldAttributes;

public class MyExclusionStrategy implements ExclusionStrategy {
    private final Class<?> mTypeToSkip;

    public MyExclusionStrategy(Class<?> typeToSkip) {
        mTypeToSkip = typeToSkip;
    }

    @Override
    public boolean shouldSkipClass(Class<?> arg0) {
        return (arg0.equals(mTypeToSkip));
    }

    @Override
    public boolean shouldSkipField(FieldAttributes arg0) {
        return false;
    }
}

このようにExclusionStrategyインターフェースを実装したクラスを独自で定義して,

package com.rmstar.gson.userguide;

import com.google.gson.ExclusionStrategy;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;

public class ExcludingFields3 {
    public static void main(String[] args) {
        Person person = new Person("Alice", 25);
        ExclusionStrategy strategy;
        Gson gson;

        strategy = new MyExclusionStrategy(String.class);
        gson = new GsonBuilder()
                .addSerializationExclusionStrategy(strategy)
                .create();
        System.out.println(gson.toJson(person));

        strategy = new MyExclusionStrategy(int.class);
        gson = new GsonBuilder()
                .addSerializationExclusionStrategy(strategy)
                .create();
        System.out.println(gson.toJson(person));
    }
}

実行結果は,以下の通りです。

{"age":25}
{"name":"Alice"}






これでも出来ない場合は,自分で細かくカスタマイズすることもできるようです。
それはまた次回にでも。



それでは,また会えることを祈りつつ。