開発メモ」カテゴリーアーカイブ

開発技術に関する記事です。

[Unity]:Unity1週間ゲームジャムに参加しました

unityroom主催のUnity1週間ゲームジャムに参加しました。
お題は「space」。

投稿作品:スペースクリッカー

お題を文字通りに宇宙と捉えて、以前から作ってみたかった惑星を巡って交易や採取を行うようなゲームを当初は考えたのですが、短期間で作り上げないといけないということで、得意のクリッカー系ゲームを作ることにしました。

隕石をクリックで壊してポイント稼ぐ→稼いだポイントでより隕石を壊しやすいようパワーアップ。

方針は割とすぐに決まり月曜日のうちには制作に入って、水曜日にはほぼ形になりました。

普段、一般公開向けのアプリを作る時などは拡張性などを考慮するのですが、今回は一週間で完成することを目標に「後々の拡張などは考えない」とバッサリと切り捨てて考えました。

一番悩んだポイントはゲームの進行に伴って難易度をどう変化させていくか。
本来なら外出しの設定ファイルにして隕石の出現データを作るところですが、思い切って単純化して…

・隕石10個ごとに難易度アップ
・序盤は難易度アップする度により大きな隕石が出現
・全種類の隕石が出揃ったら隕石の耐久度を2倍、3倍…としていく

またパワーアップの内容・コストも同様の理由で線形で増えていくようになっています。
本来なら外部ファイルにしないまでも配列データかもう少し複雑な計算式にしたと思います。

○ 使用アセット紹介
最初はもちろんキューブやスフィアでプロトタイプを作ったのですが、そこからどのように肉付け・修正していったのか、導入した順で使用したアセットを紹介します。

・Asteroid Pack – by Pixel Make

まず最初に、本ゲームのメインとなる隕石をスフィアから置き換えました。
隕石アセットは無料でも色々ありますが、リアルよりの表現と隕石の種類の多さから選定しました。
S〜XXLの5種類の隕石+配布者のロゴ入り隕石がオマケに付いてきますw

このアセットの導入により隕石の種類は大きさにより5種類と決定しました。
モデルの導入に伴い、隕石の大きさによりコライダの大きさを調整したり、HUD(耐久度表示のバーとテキスト)の位置を調整しました。

またスフィアからモデルに置き換えたことで隕石を回転させた方が見た目が良い事に気付きました。
実際に回転させてみるとuGUIのWorld Spaceで隕石に追随させていたHUDが一緒に回転する問題が発生し、常にカメラの方を向くようスクリプトを追加して調整が必要でした。

・SCI-FI Modular Pack Free

次にゲーム画面の手前側に置いてある基地をキューブから置き換えました。
隕石・基地と見栄えの部分が変わるとゲームが出来てきた!って感じになりますからね^^;

このアセットは細かいパーツを組み合わせて使うのですが、今回の基地はデモシーンにあるものをそのまま流用させてもらいました。
電光掲示板風のパーティクルが含まれてまして、非常に気になったのですが今回は活用できませんでした^^;

・Universal Sound FX

音が付くとゲームらしさがぐっと上がるので、早い段階で音を鳴らす仕組みを実装して、音源は仮でも良いので入れておくと良いと思います。

このアセットは効果音を探す時に一番最初に見るアセットです。
まめに音源追加のアップデートがされており、現在は$40で5,000以上の音源を収録しています。
これだけあればという訳にはいかないのですが、かなり良心的で良いサウンド集だと思います。

見た目と音が出来たところで、演出・効果を追加していきます。

・War FX

隕石破壊時のパーティクルエフェクトに使用。
パーティクルには苦手意識があって、普段アプリ開発でもあまり使わないのですが、このパーティクル集はデモもしっかりしててプレファブをインスタンス化するだけで使えたので楽でした。

・iTween

隕石が基地に激突した時に画面が揺れる演出を追加。
やって見たら非常に簡単でiTweenのShakeRotation()でカメラを揺らしただけです。
最近はDOTweenが主流になりつつありますが、まだしっかり使いこなせていなくて…
iTweenは使い慣れているため、サクッと作りたい今回は助かりました^^;

・SmartPool

一通りゲームが完成したところで、初のwebGLビルド、unityroomへの投稿はすんなりいったのですが、ゲームがたまに止まる!

本来なら早い段階から実機・実際に稼働させる環境で試すべきでしたが、今回は製作期間も短いので油断していました。

止まるタイミング的に、隕石や爆発パーティクルを都度生成しているのがいけないと思い込んで、プーリングの仕組みを導入。
プーリングの仕組みをちゃんと使ったのは初めてだったのですが、このアセットはシンプルでREADMEとデモシーンを見て5分ほどですぐ使えました。

プーリングを導入した結果、ゲームの停止は大幅に改善したものの依然として少し止まります^^;
本来ならちゃんとプロファイリングなどして原因を特定してから対処すべきでした。

今回は見込みは外れてなかったものの、他にも原因がありそうということで。
おそらく隕石クリック時に光らせるのにマテリアルをいじっているので、そこが原因かと(ちゃんと調べてない…)

参考:
色を変えるがドローコールを増やさない場合 – テラシュールブログ
「ちなみに、こっちは余り知られていないかもしれないが、マテリアルに値を突っ込んだ際にインスタンス化したマテリアルは、親オブジェクトをDestroyしても破棄されない。」

【Unity】マテリアルやSetPassを増やさずテクスチャのUVを変える – テラシュールブログ

○ あとがき
短い期間で仕様考えたり、今まで使ったことないものを勉強して使ってみたり、とっても勉強と刺激になりました!
実は今年の春ごろにつくっていたアプリをエターナらせてしまいまして^^;
それから悶々と現実逃避してGame of warやらオーシャン&エンパイアとか色々なものに時間を費やしてしまいまして。
今回の一週間ゲームジャムが現実復帰の第一号になりました^^;

できればエターナったゲームもunityroomに上げて、さらにできることであれば、手を入れて完成まで持っていきたいと思っています。

[Unity]:プレファブのYAMLを直接編集して子を削除する

[検証バージョン] Unity 5.5.1p3

直接編集はファイルの破損などの危険があるため、各自で判断の上、バックアップをちゃんととってお試しください。

追記:

ヒエラルキービュー上でプレファブの子を削除するとプレファブとの関連付けが解かれたように見えますが、実はその状態でApplyをすることができます。
Twitterでご教示いただきました。ありがとうございます!


階層構造を持つオブジェクト群をプレファブ化した場合、その子の一部を消すことってできませんよね?

プロジェクトビューからは子の一部を削除することはできないし、ヒエラルキービューで子を削除しようとするとプレファブとの関連が解けてしまいます。
子を追加してApplyはできるけど、削除はできないんです。

※ もしUnityの通常の操作でプレファブの一部の子だけを消すことができるのでしたら教えてください。

なんとかならないかと思い、まずはプレファブのインスタンスが置かれているシーンファイルをのぞいてみました。
(シーンファイルはデフォルト設定ではバイナリになっているので、Editor SettingsでAsset SerializationのModeをForce Textにしないと中身を見ることができません)

%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!29 &1
 ・
 ・
 ・
(ライティングなどのシーン設定に関する記述)
 ・
 ・
 ・
--- !u!1001 &1314774805
Prefab:
  m_ObjectHideFlags: 0
  serializedVersion: 2
  m_Modification:
    m_TransformParent: {fileID: 0}
    m_Modifications:
    - target: {fileID: 4721125990716184, guid: 6071afa7dc4674b5ebaa3a0fa0c5c884, type: 2}
      propertyPath: m_LocalPosition.x
      value: 0
      objectReference: {fileID: 0}
    - target: {fileID: 4721125990716184, guid: 6071afa7dc4674b5ebaa3a0fa0c5c884, type: 2}
      propertyPath: m_LocalPosition.y
      value: 0
      objectReference: {fileID: 0}
    - target: {fileID: 4721125990716184, guid: 6071afa7dc4674b5ebaa3a0fa0c5c884, type: 2}
      propertyPath: m_LocalPosition.z
      value: 0
      objectReference: {fileID: 0}
    - target: {fileID: 4721125990716184, guid: 6071afa7dc4674b5ebaa3a0fa0c5c884, type: 2}
      propertyPath: m_LocalRotation.x
      value: 0
      objectReference: {fileID: 0}
    - target: {fileID: 4721125990716184, guid: 6071afa7dc4674b5ebaa3a0fa0c5c884, type: 2}
      propertyPath: m_LocalRotation.y
      value: 0
      objectReference: {fileID: 0}
    - target: {fileID: 4721125990716184, guid: 6071afa7dc4674b5ebaa3a0fa0c5c884, type: 2}
      propertyPath: m_LocalRotation.z
      value: 0
      objectReference: {fileID: 0}
    - target: {fileID: 4721125990716184, guid: 6071afa7dc4674b5ebaa3a0fa0c5c884, type: 2}
      propertyPath: m_LocalRotation.w
      value: 1
      objectReference: {fileID: 0}
    - target: {fileID: 4721125990716184, guid: 6071afa7dc4674b5ebaa3a0fa0c5c884, type: 2}
      propertyPath: m_RootOrder
      value: 0
      objectReference: {fileID: 0}
    m_RemovedComponents: []
  m_ParentPrefab: {fileID: 100100000, guid: 6071afa7dc4674b5ebaa3a0fa0c5c884, type: 2}
  m_IsPrefabParent: 0

どうやらプレファブとの関連付けが記述されているだけで、プレファブファイルの方を直接編集してやれば上手くいきそうです。

※ ただし、以下の場合は関連付け以外の情報がシーンファイルに含まれるので注意が必要です。
・プレファブ化した元のオブジェクトがそのままシーンに残っている場合
・プレファブをインスタンス化したものにコンポーネントや子オブジェクトを追加した場合。

それではプレファブのファイルを編集してみます。

%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!1001 &100100000
Prefab:
  m_ObjectHideFlags: 1
  serializedVersion: 2
  m_Modification:
    m_TransformParent: {fileID: 0}
    m_Modifications: []
    m_RemovedComponents: []
  m_ParentPrefab: {fileID: 0}
  m_RootGameObject: {fileID: 1006150272106676}
  m_IsPrefabParent: 1
--- !u!1 &1006150272106676
GameObject:
  m_ObjectHideFlags: 0
  m_PrefabParentObject: {fileID: 0}
  m_PrefabInternal: {fileID: 100100000}
  serializedVersion: 5
  m_Component:
  - component: {fileID: 4721125990716184}
  m_Layer: 0
  m_Name: GameObject
  m_TagString: Untagged
  m_Icon: {fileID: 0}
  m_NavMeshLayer: 0
  m_StaticEditorFlags: 0
  m_IsActive: 1
--- !u!1 &1271471559630830
GameObject:
  m_ObjectHideFlags: 0
  m_PrefabParentObject: {fileID: 0}
  m_PrefabInternal: {fileID: 100100000}
  serializedVersion: 5
  m_Component:
  - component: {fileID: 4955832607490314}
  - component: {fileID: 33287708229778766}
  - component: {fileID: 65699820418463966}
  - component: {fileID: 23228631480229132}
  m_Layer: 0
  m_Name: Cube
  m_TagString: Untagged
  m_Icon: {fileID: 0}
  m_NavMeshLayer: 0
  m_StaticEditorFlags: 0
  m_IsActive: 1
--- !u!4 &4721125990716184
Transform:
  m_ObjectHideFlags: 1
  m_PrefabParentObject: {fileID: 0}
  m_PrefabInternal: {fileID: 100100000}
  m_GameObject: {fileID: 1006150272106676}
  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:
  - {fileID: 4955832607490314}
  m_Father: {fileID: 0}
  m_RootOrder: 0
  m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!4 &4955832607490314
Transform:
  m_ObjectHideFlags: 1
  m_PrefabParentObject: {fileID: 0}
  m_PrefabInternal: {fileID: 100100000}
  m_GameObject: {fileID: 1271471559630830}
  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: 4721125990716184}
  m_RootOrder: 0
  m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!23 &23228631480229132
MeshRenderer:
  m_ObjectHideFlags: 1
  m_PrefabParentObject: {fileID: 0}
  m_PrefabInternal: {fileID: 100100000}
  m_GameObject: {fileID: 1271471559630830}
  m_Enabled: 1
  m_CastShadows: 1
  m_ReceiveShadows: 1
  m_MotionVectors: 1
  m_LightProbeUsage: 1
  m_ReflectionProbeUsage: 1
  m_Materials:
  - {fileID: 10303, guid: 0000000000000000f000000000000000, type: 0}
  m_StaticBatchInfo:
    firstSubMesh: 0
    subMeshCount: 0
  m_StaticBatchRoot: {fileID: 0}
  m_ProbeAnchor: {fileID: 0}
  m_LightProbeVolumeOverride: {fileID: 0}
  m_ScaleInLightmap: 1
  m_PreserveUVs: 1
  m_IgnoreNormalsForChartDetection: 0
  m_ImportantGI: 0
  m_SelectedEditorRenderState: 3
  m_MinimumChartSize: 4
  m_AutoUVMaxDistance: 0.5
  m_AutoUVMaxAngle: 89
  m_LightmapParameters: {fileID: 0}
  m_SortingLayerID: 0
  m_SortingOrder: 0
--- !u!33 &33287708229778766
MeshFilter:
  m_ObjectHideFlags: 1
  m_PrefabParentObject: {fileID: 0}
  m_PrefabInternal: {fileID: 100100000}
  m_GameObject: {fileID: 1271471559630830}
  m_Mesh: {fileID: 10202, guid: 0000000000000000e000000000000000, type: 0}
--- !u!65 &65699820418463966
BoxCollider:
  m_ObjectHideFlags: 1
  m_PrefabParentObject: {fileID: 0}
  m_PrefabInternal: {fileID: 100100000}
  m_GameObject: {fileID: 1271471559630830}
  m_Material: {fileID: 0}
  m_IsTrigger: 0
  m_Enabled: 1
  serializedVersion: 2
  m_Size: {x: 1, y: 1, z: 1}
  m_Center: {x: 0, y: 0, z: 0}

「— !u!1001 &100100000」のような記述から始まるひとかたまりが一つのオブジェクトやコンポーネントを表しているのがわかります。
前半がオブジェクトに関する記述、後半がコンポーネント。親子関係はTransformコンポーネントの部分に記述されています。

「m_Name:」にオブジェクト名が書かれているので、今回消したい子オブジェクト’Cube’を探します。

--- !u!1 &1271471559630830
GameObject:
  m_ObjectHideFlags: 0
  m_PrefabParentObject: {fileID: 0}
  m_PrefabInternal: {fileID: 100100000}
  serializedVersion: 5
  m_Component:
  - component: {fileID: 4955832607490314}
  - component: {fileID: 33287708229778766}
  - component: {fileID: 65699820418463966}
  - component: {fileID: 23228631480229132}
  m_Layer: 0
  m_Name: Cube
  m_TagString: Untagged
  m_Icon: {fileID: 0}
  m_NavMeshLayer: 0
  m_StaticEditorFlags: 0
  m_IsActive: 1

先頭に書かれている「— !u!1 &1271471559630830」の「1271471559630830」がこのオブジェクトの固有のIDです。
このIDでファイル内を検索すると4つのコンポーネントが付いていることがわかります。

そのうちの一つTransformコンポーネントに関する記述をみてみます。

--- !u!4 &4955832607490314
Transform:
  m_ObjectHideFlags: 1
  m_PrefabParentObject: {fileID: 0}
  m_PrefabInternal: {fileID: 100100000}
  m_GameObject: {fileID: 1271471559630830}
  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: 4721125990716184}
  m_RootOrder: 0
  m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}

今度はこのTransformコンポーネントのID「4955832607490314」で検索すると、親のTransformの記述が見つかります。

--- !u!4 &4721125990716184
Transform:
  m_ObjectHideFlags: 1
  m_PrefabParentObject: {fileID: 0}
  m_PrefabInternal: {fileID: 100100000}
  m_GameObject: {fileID: 1006150272106676}
  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:
  - {fileID: 4955832607490314}
  m_Father: {fileID: 0}
  m_RootOrder: 0
  m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}

ここの「m_Children:」というところに子のTransformのIDが書かれているので、この行を削除してしまいます。

--- !u!4 &4721125990716184
Transform:
  m_ObjectHideFlags: 1
  m_PrefabParentObject: {fileID: 0}
  m_PrefabInternal: {fileID: 100100000}
  m_GameObject: {fileID: 1006150272106676}
  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}
  m_RootOrder: 0
  m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}

あとは、先ほど見つけたオブジェクトに関する記述と4箇所のコンポーネントに関する記述を消すだけです。

%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!1001 &100100000
Prefab:
  m_ObjectHideFlags: 1
  serializedVersion: 2
  m_Modification:
    m_TransformParent: {fileID: 0}
    m_Modifications: []
    m_RemovedComponents: []
  m_ParentPrefab: {fileID: 0}
  m_RootGameObject: {fileID: 1006150272106676}
  m_IsPrefabParent: 1
--- !u!1 &1006150272106676
GameObject:
  m_ObjectHideFlags: 0
  m_PrefabParentObject: {fileID: 0}
  m_PrefabInternal: {fileID: 100100000}
  serializedVersion: 5
  m_Component:
  - component: {fileID: 4721125990716184}
  m_Layer: 0
  m_Name: GameObject
  m_TagString: Untagged
  m_Icon: {fileID: 0}
  m_NavMeshLayer: 0
  m_StaticEditorFlags: 0
  m_IsActive: 1
--- !u!4 &4721125990716184
Transform:
  m_ObjectHideFlags: 1
  m_PrefabParentObject: {fileID: 0}
  m_PrefabInternal: {fileID: 100100000}
  m_GameObject: {fileID: 1006150272106676}
  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}
  m_RootOrder: 0
  m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}

これでプレファブファイルがUnityで読み込まれたタイミングでプレファブと全てのインスタンス化されたオブジェクトから子オブジェクト’Cube’が消えます。

インスタンス化してシーンに置かれたオブジェクトに個別に編集を行なっている場合にはシーンファイルにゴミが残る可能性があるので、くれぐれもご注意ください。

以前にUnityのYAMLを読んでみようみたいな記事を見かけたことがあったのですが、まさか本当に自分が読むことになるとは思ってもいませんでした^^;
しかし実際にのぞいてみるとかなりシンプルで分かりやすいですね。
ちなみにプレファブをインスタンス化したものに施した変更はシーンファイルの中の「Prefab:」のところに追記される形で記述されます。

警告はしましたが実際はコンポーネントのパラメータを変更したり、コンポーネントを削除してる場合は今回の編集では影響受けないみたいです。
ただし削除するオブジェクトにコンポーネントを追加していたり、配下にさらに子を追加したりしているとシーンファイルにゴミが残るみたいです。

以下は削除したCubeオブジェクトに適当なスクリプトを追加、また配下にSphereを追加していた場合のシーンファイルです。

--- !u!1001 &1314774805
Prefab:
  m_ObjectHideFlags: 0
  serializedVersion: 2
  m_Modification:
    m_TransformParent: {fileID: 0}
    m_Modifications:
    - target: {fileID: 4721125990716184, guid: 6071afa7dc4674b5ebaa3a0fa0c5c884, type: 2}
      propertyPath: m_LocalPosition.x
      value: 0
      objectReference: {fileID: 0}
    - target: {fileID: 4721125990716184, guid: 6071afa7dc4674b5ebaa3a0fa0c5c884, type: 2}
      propertyPath: m_LocalPosition.y
      value: 0
      objectReference: {fileID: 0}
    - target: {fileID: 4721125990716184, guid: 6071afa7dc4674b5ebaa3a0fa0c5c884, type: 2}
      propertyPath: m_LocalPosition.z
      value: 0
      objectReference: {fileID: 0}
    - target: {fileID: 4721125990716184, guid: 6071afa7dc4674b5ebaa3a0fa0c5c884, type: 2}
      propertyPath: m_LocalRotation.x
      value: 0
      objectReference: {fileID: 0}
    - target: {fileID: 4721125990716184, guid: 6071afa7dc4674b5ebaa3a0fa0c5c884, type: 2}
      propertyPath: m_LocalRotation.y
      value: 0
      objectReference: {fileID: 0}
    - target: {fileID: 4721125990716184, guid: 6071afa7dc4674b5ebaa3a0fa0c5c884, type: 2}
      propertyPath: m_LocalRotation.z
      value: 0
      objectReference: {fileID: 0}
    - target: {fileID: 4721125990716184, guid: 6071afa7dc4674b5ebaa3a0fa0c5c884, type: 2}
      propertyPath: m_LocalRotation.w
      value: 1
      objectReference: {fileID: 0}
    - target: {fileID: 4721125990716184, guid: 6071afa7dc4674b5ebaa3a0fa0c5c884, type: 2}
      propertyPath: m_RootOrder
      value: 0
      objectReference: {fileID: 0}
    m_RemovedComponents: []
  m_ParentPrefab: {fileID: 100100000, guid: 6071afa7dc4674b5ebaa3a0fa0c5c884, type: 2}
  m_IsPrefabParent: 0
--- !u!1 &1314774806 stripped
GameObject:
  m_PrefabParentObject: {fileID: 1271471559630830, guid: 6071afa7dc4674b5ebaa3a0fa0c5c884,
    type: 2}
  m_PrefabInternal: {fileID: 1314774805}
--- !u!114 &1314774807
MonoBehaviour:
  m_ObjectHideFlags: 0
  m_PrefabParentObject: {fileID: 0}
  m_PrefabInternal: {fileID: 0}
  m_GameObject: {fileID: 1314774806}
  m_Enabled: 1
  m_EditorHideFlags: 0
  m_Script: {fileID: 11500000, guid: c75fe1ad4fe3e493ab421c02f41fed06, type: 3}
  m_Name: 
  m_EditorClassIdentifier: 
--- !u!4 &1314774808 stripped
Transform:
  m_PrefabParentObject: {fileID: 4955832607490314, guid: 6071afa7dc4674b5ebaa3a0fa0c5c884,
    type: 2}
  m_PrefabInternal: {fileID: 1314774805}

Unityのヒエラルキービュー上では問題なさそうに見えますが、シーンファイルにはしっかりゴミが残っているようです。

[Unity]:MonoBehaviour.OnApplicationPause()の挙動

[検証バージョン]
Unity 5.1.2p2
iOSシミュレータ 8.4
Android Genymotion 4.2.2 API 17

アプリがバックグラウンドに入った時・および復帰した時の処理を記述するMonoBehaviour.OnApplicationPause()

この中に記述する処理が重く時間がかかり途中でアプリが強制終了された場合、どのような挙動になるのか検証してみた。

・検証方法
OnApplicationPauseでバックグラウンド退避時の処理中に数秒待機してデバッグプリントを行う。

using UnityEngine;

public class GameController : MonoBehaviour {
	void OnApplicationPause(bool isPause) {
		if (isPause) {
			Debug.Log("OnApplicationPause()");
			System.Threading.Thread.Sleep(3000);
			Debug.Log("3 seconds lator..");
		}
	}
}

・iOSの場合
アプリ起動中にホームボタン2度押し→アプリを終了

-> applicationWillResignActive()
OnApplicationPause()
3 seconds lator..
-> applicationDidEnterBackground()
-> applicationWillTerminate()

ちゃんと3秒後にデバッグプリントされた後に、バックグラウンド→終了となっている。

ただし、OnApplicationPauseをコルーチンにした場合はバックグラウンドに入った時点で処理が一時停止してしまうので注意が必要。

・Androidの場合
ハードウェアボタンでアプリの一覧を表示→アプリを終了

OnApplicationPause()のデバッグプリントのみで、処理は途中で中断されてしまった?

Androidの場合は、OnApplicationPause()での処理には注意が必要みたい。

[Unity, NGUI]:スクロールビューにスクロールバーを追加する

[検証バージョン]
Unity 5.1.2p2
NGUI 3.9.1

スクロールビューの基本についてはこちらをどうぞ。

1.スクロールバーの背景となる部分とつまみの部分、計2つのスプライトを作成。
スクロールバーのスプライトを追加

ヒエラルキー上での配置はスクロールビューの配下以外であればどこでも大丈夫。

垂直方向のスクロールバーの場合は2つのスプライトの高さを合わせておく必要がある。
見た目的につまみ部分が前面に表示されるようにDepthの設定もお忘れなく。

2.スプライトにコライダーを設定
スクロールバーの背景部分をクリックして移動する機能が必要なければ、背景用のスプライトにはコライダーを設定しなくても大丈夫。
コライダーの設定は目的のスプライトを選択した状態で、メニューバーから「NGUI」-「Attach」-「Collider」とするのが簡単。

3.適当なオブジェクトにUIScrollBarコンポーネントを追加
UIScrollBar

UIScrollBarコンポーネント用にNGUI Widgetを作ってその配下に先ほど作成したスプライトを配置しておくのが綺麗だと思う。
目的のオブジェクトを選択した状態でメニューバーから「NGUI」-「Attach」-「Scroll Bar Script」。

UIWidgetではなく適当なオブジェクトでも大丈夫だが、UIScrollView自体にUIScrollBarコンポーネントを追加するとスクロールバーの挙動がおかしくなる。

4.UIScrollBarの設定
UIScrollBarの設定

Foreground, Backgroundにそれぞれつまみ用・背景用のスプライトを設定。
Directionも適切に。

5.スクロールビューにスクロールバーを設定
UIScrollViewにスクロールバーを設定

スクロールビューのインスペクタにScroll Barsの項目があるので、Horizontal/Vertical目的の方にスクロールバーを設定。

ここで一度実行すると、UIScrollBarのValueなどの値が反映されるので、そこでスクロールバーの初期位置などを設定できる。
UIScrollBarの設定

Sizeが、スクロールビュー配下の全体サイズに対する表示範囲。スクロールビューに対して2倍のサイズのものを配置していれば0.5となる。
Valueがつまみの初期位置、0〜1。

[Unity, NGUI]:スクロールビューの基本

[検証バージョン]
Unity 5.1.2p2
NGUI 3.9.1

・基本的な考え方
ヒエラルキー上でスクロールビューの配下に配置されているUIをスクロールビューの範囲に限定して表示する。

スプライトがスクロールビュー配下にない場合

スプライトがスクロールビュー配下にない場合

スプライトがスクロールビュー配下にある場合

スプライトがスクロールビュー配下にある場合

またその名の通り、スクロールビュー配下に配置されたUIは、実行時にドラッグなどにより見える範囲を動かすことができる。

よくグリッドビューと組み合わせて紹介されることが多いが、グリッドビューはあくまで多数の項目を整列するだけのもので、スクロールビュー自体とは特に関係ない。

・作り方
UI Rootが存在する状態で、メニューバーから「NGUI」-「Create」-「Scroll View」を選択する。
作成されたScroll Viewの配下にUIを配置する。

ドラッグでスクロールさせたい場合は、配下のUIにコライダーとUIDragScrollViewを追加する。
コライダーの追加は目的のUIを選択した状態でメニューバーから「NGUI」-「Attach」-「Collider」とするのが簡単。
UIDragScrollViewの追加は目的のUIのインスペクタで「Add Component」-「NGUI」-「Interaction」-「Drag Scroll View」を選択。

スクロールビューのインスペクタでドラッグで動かせる方向などを指定できる。
スクロールビューのインスペクタ

[Unity, PlayMaker]:DontDestroyOnLoadの不具合

PlayMaker 1.7.8.2

アクションカテゴリ「Level」のアクション「DontDestroyOnLoad」で指定したGameObjectがシーンロード時に消えてしまいます。

DontDestroyOnLoadアクションのソースを見てみると…

// (c) Copyright HutongGames, LLC 2010-2013. All rights reserved.

using System.Collections;
using UnityEngine;

namespace HutongGames.PlayMaker.Actions
{
	[ActionCategory(ActionCategory.Level)]
	[Tooltip("Makes the Game Object not be destroyed automatically when loading a new scene.")]
	public class DontDestroyOnLoad : FsmStateAction
	{
		[RequiredField]
        [Tooltip("GameObject to mark as DontDestroyOnLoad.")]
		public FsmOwnerDefault gameObject;

		public override void Reset()
		{
			gameObject = null;
		}

		public override void OnEnter()
		{
			// Have to get the root, since the game object will be destroyed if any of its parents are destroyed.
			
			Object.DontDestroyOnLoad(Owner.transform.root.gameObject);

			Finish();
		}
	}
}

せっかく引数で指定したgameObjectが指定されていません。
代わりにこのアクションを実行しているFSMを持つGameObjectがDontDestroyOnLoadになっているようです。

25行目を以下のように変更します。

			if (gameObject != null) Object.DontDestroyOnLoad(gameObject.GameObject.Value);

[Unity]:Unity5対応でハマった点

Unity4.5.5で作成した「あ〜ちゃ〜の大冒険」をUnity5対応する時にハマった点。

Unityバージョン4.5.5f1 -> 5.0.0p2

・シーン全体が暗くなった
アンビエントライトのせい。
メニュー「Window」-「Lighting」を開き調整する。

Unity5-lighting

・NavMeshAgentの挙動がおかしい
具体的には、あ〜ちゃ〜が一度立ち止まると、その後、移動しなくなる。
従来はNavMeshAgent.Stop()後にSetDestination()で座標を設定すると動いていたのだが、Resume()を呼び出す必要がある。

[Unity, PlayMaker]:Androidビルドで勝手にパーミッションが追加される

検証バージョン:Unity 4.5.5f1, PlayMaker 1.7.7f6

PlayMakerをインポートしたプロジェクトをAndroid向けにビルドすると、以下の権限(パーミッション)が勝手に追加されます。

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.WAKE_LOCK" />

Unity自体が、特定のAPIを使用した場合にビルド時に自動的に権限を追加する仕様になっています。
一旦Androidプロジェクトを吐き出してからビルドする場合は変更できますが、直接apkを書き出す場合は厄介です。

もし該当する機能を使っていないのならば、PlayMakerのアクションを記述してあるスクリプトファイルを最初に削除してしまえば済むことです。

・android.permission.INTERNET
/PlayMaker/Actions/Network フォルダごと削除

・android.permission.VIBRATE
/PlayMaker/Actions/DeviceVibrate.cs

・android.permission.ACCESS_FINE_LOCATION
/PlayMaker/Actions/GetLocationInfo.cs
/PlayMaker/Actions/StartLocationServiceUpdates.cs
/PlayMaker/Actions/StopLocationServiceUpdates.cs

・android.permission.WAKE_LOCK
/PlayMaker/Actions/DevicePlayFullScreenMovie.cs

[参考]:apkに付与されている権限を調べる方法
aaptというADT付属のツールで調べることが出来ます。
詳しくはこちらを参考にさせて頂きました。
Yukiの枝折 – 「apkからマニフェストの内容を読み取る方法」

[Unity]:PlayMaker カスタムアクション入門

PlayMakerではアクションという処理の単位を組合せて、処理を記述していきます。

PlayMaker公式アクションリファレンス(英語)

標準でもかなりの数のアクションが用意されていますが、実際にPlayMakerを使って作業していると標準のものでは処理を実現できそうもない場合が出てきます。
その場合はカスタムアクションを自分で作成して、標準のアクションと同様にPlayMakerで使用することができます。

ここではPlayMaker標準で用意されている「IntOperator」アクションを例にとり、アクションの記述の仕方を解説していきます。
「IntOperator」は2つのInt値と計算方法を指定し、その結果を指定のInt型変数に代入するアクションです。

namespace HutongGames.PlayMaker.Actions
{
	・・・
}

独自のネームスペースを使うことも可能ですが、特に理由が無い限りはHutongGames.PlayMaker.Actionsを使用するのが無難です。

namespace HutongGames.PlayMaker.Actions
{
	[ActionCategory(ActionCategory.Math)]
	[Tooltip("Performs math operation on 2 Integers: Add, Subtract, Multiply, Divide, Min, Max.")]
	public class IntOperator : FsmStateAction
	{
		・・・
	}
}

PlayMakerのアクションブラウザに表示される時のカテゴリを指定します。

PlayMakerActionBrowser

標準のカテゴリに追加することもできますし、独自のカテゴリに追加することもできます。
独自のカテゴリに追加したい場合は以下のようにカテゴリ名を文字列で指定します。

	[ActionCategory("CustomCategory")]
namespace HutongGames.PlayMaker.Actions
{
	[ActionCategory(ActionCategory.Math)]
	[Tooltip("Performs math operation on 2 Integers: Add, Subtract, Multiply, Divide, Min, Max.")]
	public class IntOperator : FsmStateAction
	{
		・・・
	}
}

アクションブラウザに表示するアクションの説明文を指定します。

PlayMakerActionBrowserPreview

省略しても構いません。

namespace HutongGames.PlayMaker.Actions
{
	[ActionCategory(ActionCategory.Math)]
	[Tooltip("Performs math operation on 2 Integers: Add, Subtract, Multiply, Divide, Min, Max.")]
	public class IntOperator : FsmStateAction
	{
		・・・
	}
}

HutongGames.PlayMaker.FsmStateActionを継承させます。

		[RequiredField]
		public FsmInt integer1;
		[RequiredField]
		public FsmInt integer2;
		public Operation operation = Operation.Add;
		[RequiredField]
		[UIHint(UIHint.Variable)]
		public FsmInt storeResult;
		public bool everyFrame;

PlayMakerIntOperator

PlayMakerエディタ上に表示されるパラメータとなります。
「RequiredField」属性を指定すると省略不可となり、エディタ上で適切な値を指定しない場合にエラーとなります。

「UIHint」属性を指定することで、エディタ上での表示の仕方を変えることができます。
「UIHint.Variable」を指定した場合には、FSMで用意した変数から選択することしかできなくなります。

メンバ変数の型を「Fsm〜」という型で指定することにより、エディタの機能を利用した便利な指定ができるようになります。
ここではFsmIntしか登場しませんが、他にFsmFloatやFsmStringなど様々な型が用意されています。

また、通情のC#の型も使用することができます。ここでは「everyFrame」がbool型で宣言されています。

		public override void Reset()
		{
			integer1 = null;
			integer2 = null;
			operation = Operation.Add;
			storeResult = null;
			everyFrame = false;
		}

新規にアクションを追加した場合の初期値、またはエディタ上でリセットをかけた場合にセットされる値を指定します。

		public override void OnEnter()
		{
			DoIntOperator();
			
			if (!everyFrame)
				Finish();
		}

アクションが実行される時に呼び出されます。
この中に直接処理を記述しても構いませんが、ここでは実際の処理は「DoIntOperator()」の中に記述されています。
処理の終了には必ず「Finish()」を呼び出す必要があります。
ただし、ここでは毎フレームの処理を行う場合は処理を終了させたく無いのでeneryFameフラグにより条件分岐されています。

		// NOTE: very frame rate dependent!
		public override void OnUpdate()
		{
			DoIntOperator();
		}

毎フレーム実行される処理を記述します。

		void DoIntOperator()
		{
			int v1 = integer1.Value;
			int v2 = integer2.Value;

			switch (operation)
			{
				case Operation.Add:
					storeResult.Value = v1 + v2;
					break;

				case Operation.Subtract:
					storeResult.Value = v1 - v2;
					break;

				case Operation.Multiply:
					storeResult.Value = v1 * v2;
					break;

				case Operation.Divide:
					storeResult.Value = v1 / v2;
					break;

				case Operation.Min:
					storeResult.Value = Mathf.Min(v1, v2);
					break;

				case Operation.Max:
					storeResult.Value = Mathf.Max(v1, v2);
					break;
			}
		}

「Fsm〜」型の変数はValueプロパティにより実際の値にアクセスすることができます。
注意点としてはin/outの区別が無いので、処理を記述する際に余計な値を変更してしまわないようにしましょう。

ーーーーー
以上のようにカスタムアクションはとても簡単に作成することができます。
GameObjectの参照をとったり、より複雑な処理をする場合は、標準のアクションの中から似たような処理をするものを見つけ、そのスクリプトを参考にすると良いでしょう。
アクションブラウザ上でアクションの右クリックメニューから簡単にスクリプトを参照することができます。

[Unity]:PlayMaker実践解説 (2)

今回はタイトル画面でスタートボタンが押された時の処理を解説していきます。

ArchuTitle ArchuTitleHierarchy2

○ StartButton
NGUIのUIButtonコンポーネントを持つボタンのGUI部品です。
NGUIのイベントをFSMに通知するためにPlayMakerの開発元から提供されているNGUIアドインをセットしてあります。

スタートボタンを押すと初回はプロローグ画面へ、その後はゲーム画面へ遷移するようになっています。

ArchuStartButtonFSM

ArchuStartButtonFSMEvent

ArchuStartButtonFSMVariable

1. Init
シーンが読込まれPlayMakerFSMが動き出した時に初めに到達する状態(ステート)です。
前回紹介したTitleControllerのInitとはほぼ同時(順不同)で動作します。
UnityのスクリプトにおけるAwake()の実行順と同じとイメージすれば良いでしょう。
ただしPlayMakerの場合は実行順を制御する機構がないようですので、FSM同士の関連には注意が必要です。
(※:未確認ですがUnityのスクリプトの実行順制御によりPlayMakerFSMの実行順も制御できるかもしれません。PlayMakerFSM自体も一種のコンポーネントですので。)

ArchuStartButtonInit

1-1. Stop Activity Indicator(カスタム)
処理中を示すインジケーター(くるくる回転するアイコン)を停止・非表示にします。

2. Idle
何も行いません。
ボタンが押下された時に「NGUI / ON CLICK」イベントが来るのを待ち受けます。

ArchuStartButtonIdle

3. OnClick
Idleの状態からボタンが押下された時に遷移する状態です。

ArchuStartButtonOnClick

3-1. Stop BGM(カスタム)
再生中のBGMを停止します。

3-2. Play Start Button SE(カスタム)
スタートボタン押下の効果音を再生します。

4. CheckPrologue
スタートボタン押下後に到達するステートです。
初回起動かどうかを判断して、プロローグかゲーム画面に遷移を振り分けます。
上記のOnClick中に書いても良い処理ですが、分かり易さのために分けてあります。

ArchuStartButtonCheckPrologue

4-1. PlayerPrefs Get Int(PlayerPrefs)
PlayerPrefに保存されている物語の進行度を表す数値を取り出し、変数storyにセットします。
Unity標準のPlayerPrefs.GetInt()と同様のアクションですが、複数の値を同時に扱えるのが少し便利です。

4-2. Int Compare(Logic)
さきほど取り出したstoryの値が1以上なら「START_GAME」イベントを発生しゲーム画面へ遷移する処理へ、1未満なら「FINISHED」イベントを発生しプロローグへ遷移する処理へ移ります。

5. StartPrologue
初めてスタートボタンを押した時に到達するステートです。

ArchuStartButtonStartPrologue

5-1. Activate Game Object(GameObject)
インスペクタ上で指定したフェードアウト用のNGUIスプライトを有効にします。
Unity標準のGameObject.SetActive()と同様のアクションです。

5-2. Wait(Time)
1.1秒間待機した後に指定のイベント(今回は指定してないのでFINISHEDイベント)を発生します。
このように待機した後に処理を行いたい場合は、別の状態を用意してイベントにより遷移させる必要があります。
このアクションが途中に挟まれていたとしても、以降のアクションも全て実行される点に注意が必要です。

6. LoadLevelPrologue
プロローグ画面へ遷移します。

ArchuStartButtonLoadLevelPrologue

6-1. Move Off Ad Banner(カスタム)
広告バナーを画面外に隠します。

6-2. Load Level(Level)
「Prologue」という名前のシーンをロードします。
UnityのApplication.LoadLevel()にあたるアクションですが、Additive/Asyncなどがオプションとして選択できるようになっています。

7. StartGame
すでにゲームをやったことがある場合にスタートボタンを押すと到達するステートです。
内容は前述の「5. StartPrologue」とほぼ同様なので説明は省略します。

ArchuStartButtonStartGame

8. LoadLevelGame
ゲーム画面へ遷移します。
さきほどのプロローグ画面への遷移と違いロード中画面を挟んだ遷移なので、アクションの内容が異なっています。

ArchuStartButtonLoadLevelGame

6-1. Play Game BGM(カスタム)
ゲーム画面用のBGMの再生を開始します。

6-2. Load Lovel Transition(カスタム)
ロード中画面を表示した上で「Stage01」という名前のシーンをロードします。

ーーーーー
今回の中では「2. Idle」の何もアクションの無い状態が特徴的だったと思います。
こういう状態を用意することで、外部からの操作でイベントを飛ばしてもらって処理を行うというような記述が可能になります。

また、プロローグの場合とゲーム画面の場合でフェードアウトの処理を変えていたり、BGMを止めたり、ロード中画面を挟んだりなど微妙な調整を色々としています。
これがどのような違いを生むかは、もうじきリリース予定の「あ〜ちゃ〜の大冒険」でぜひ確認してみて欲しいと思います。