Unity - Playモード中に生成したオブジェクトをSceneに残す

Game Play中に生成したオブジェクトをSceneに残す方法。

Scene中でオブジェクトを配置するとき、同じようなものが散らばっている状況がほしい場合、一つ一つ設置・位置調整という手順をいちいち行うのは面倒。
スクリプトから乱数で適当に配置したものをSceneに残し、その後手動で微調整するのがたぶん楽。
ただし通常、プレイモード終了時には開始時の状態に戻るため、一手間必要。

http://dymn.hatenablog.com/entry/2014/02/21/205409

で解説したオブジェクトが残る現象を逆に利用する。
f:id:DYMN:20151113232956p:plain
今回はこんな感じで室内に散らばった本を作りたい。

もっとスマートな方法があるのかもしれないが、知らぬ。

for( int ii = 0 ; ii < booksAmount ; ii++ )
{
	bookKind = Random.Range(0, booksObjPrefab.Length);

	bookPosition.x = Random.Range(xMin, xMax);
	bookPosition.z = Random.Range(zMin, zMax);

	bookRotate.eulerAngles = new Vector3(0.0f, Random.Range(0.0f, 360.0f), 90.0f);

	thisBook = (GameObject)Instantiate(booksObjPrefab[bookKind], bookPosition, bookRotate);
	thisBook.transform.parent = bookFolder.transform;

	bookLocation[ii].kind = bookKind;
	bookLocation[ii].pos  = bookPosition;
	bookLocation[ii].rot  = bookRotate;
}

オブジェクトのランダム配置は基礎的な知識で対応できるだろうから省略する。
今回の場合は本Objectを設置するにあたり、「本の種類」「位置」「回転」が重要なのでその3つを保存しておく。
Quaternionを使うときは事前に「Quaternion bookRotate = new Quaternion()」を忘れずに。

private class SetBookData
{
	public int kind;
	public Vector3 pos;
	public Quaternion rot;
}

private SetBookData[] bookLocation;

bookLocation = new SetBookData[booksAmount];

for (int ii = 0; ii < bookLocation.Length ; ii++)
{
	bookLocation[ii] = new SetBookData();
}

データクラスはこんな感じ。C#の場合初期化で配列各要素に対しコンストラクタを呼び出しておかないとエラーが出る。

void OnDestroy()
{
	if (saveFlag)
	{
		bookFolder = CreateFolder("Book");

		for (int ii = 0; ii < booksAmount; ii++)
		{
			GameObject thisBook = (GameObject)Instantiate(booksObjPrefab[bookLocation[ii].kind], bookLocation[ii].pos, bookLocation[ii].rot);
			thisBook.transform.parent = bookFolder.transform;
		}
	}
}

以前はOnDestroy中でオブジェクトを生成したからSceneに残ったが、今回はあえてOnDestroy中にインスタンスを生成する。
保存しておいた本の情報に従い、再度本オブジェクトを生成。プレイモードを終了するとScene中に残っているのが確認できる。
(エラーが出るが、今回はオブジェクトを残すのが目的なので甘受する)

ただし、プレイモード中に生成したオブジェクトを残した場合、終了直後のSceneではHierarchyで当該オブジェクトを確認できないケースがある。
この場合再度プレイモードを実行し、すぐに終了すれば確認できるようになる。
なので生成処理はプレイモードに入ったら自働で行うより、ボタン等のイベントで起こす形にした方がよい。
(Scene側で手動でスクリプトをON/OFFするならその限りではないが)
f:id:DYMN:20151113234528p:plain

Unity - ドアを使ってSceneを移動する

SceneとSceneの間をドアで移動する。
Scene間のドアが一つだけなら前後のSceneを参照すれば事は済むが、たとえば建物と庭が表口と裏口で繋がっている場合などは単純なScene名の参照では適切な場所へプレイヤーを配置できない。
そこを解決しながらSceneを切り替える。youtu.be
たぶんもっと上手い方法があるんだろうが、とりあえず今はこうする。

■Scene移動用ドアのPrefab
f:id:DYMN:20151109005312p:plain

SceneDoor
- Aside(Object)
- Bside(Object)

両Scene間で名前は同一にする。
SceneDoor_SceneA_SceneB
SceneDoorA_SceneC_SceneD/SceneDoorB_SceneC_SceneD
みたいな感じ。

■ドアに持たせるステータス

SceneDoorStatus
- thisDoorName : string
- sceneName : string[2] (Serialized)
-------------------------------------
+ GetOppositeSceneSide(string crntSceneName) : DoorSideID
+ GetSceneNameFromSide(DoorSideID sideID) : string

■他のオブジェクト(ゲームマスター的なオブジェクト)が持つシーンローダー

SceneLoader
- nextSceneSide : DoorSideID
- tgtDoorName : string
- sceneLoadedByDoor : bool
-------------------------------------
- OnLevelWasLoaded() : void
- LevelWasLoadedByDoor() : void
+ LoadSceneByDoor(GameObject tgtSceneDoor) : void
- LoadScene(string newSceneName) : void

■使用した列挙型

+ DoorSideID : Enum / Aside, Bside

■現在Sceneの名前を取得する
Application.loadedLevelName
■Sceneをロードする
Application.LoadLevel(string sceneName)

プレイヤーのカメラ内でクリックしたオブジェクトを取得する処理を入れておき、SceneDoorが取れたら(取る方法はTagかGetComponentあたりがいい?)SceneLoaderにそのオブジェクトを引数として渡す。
あらかじめSceneDoorStatus.sceneNameには両面のSceneNameを入れておき、現在Scene名を入れると反対側Scene名が取れる関数も仕込んでおく。

ただし、実際のSceneのロードはApplication.LoadLevel実行の後次のフレームで行われるようなので、新Sceneの処理は別途OnLevelWasLoaded関数(Unity既存関数)に記述する必要がある。
ここでは新Sceneにおけるプレイヤーオブジェクトの初期位置を決定している。新Scene内でのDoorの検索はFindを使っている。たぶんこれのせいで若干遅いんだろうな……

	void OnLevelWasLoaded()
	{
		if (sceneLoadedByDoor)
		{
			LevelWasLoadedByDoor();
			sceneLoadedByDoor = false;
		}
	}

	private void LevelWasLoadedByDoor()
	{
		string sceneDoorPath = "/Door/" + tgtDoorName;
		GameObject doorInNextScene = GameObject.Find(sceneDoorPath).gameObject;

		if (doorInNextScene == null)
		{
			Debug.LogWarning("Failue to find door in next scene.");
		}
		else
		{
			string sideObjName = nextSceneSide.ToString();

			GameObject doorSideObj = doorInNextScene.transform.FindChild(sideObjName).gameObject;

			CharacterLoader charaLoader = CommonFunction.GetCharacterLoaderComponent();
			GameObject mainPlayerObj = charaLoader.MainPlayerObject;

			mainPlayerObj.transform.position = doorSideObj.transform.position;
		}
	}

Scene名を入れたりする部分がStringの手打ちになって不安なので、このへんなんとかしたいけどScene型の変数は無いから別途他の手段を探す必要がありそうだ。

Unreal Engine - 別のBlueprintからBlueprintを呼び出す(CustomEvent使用)

コンポーネント内部からさらに別のコンポーネントを呼び出したいことは多々ある。
UnityでいえばSendMessageとかGetComponentからの他コンポーネントの抱える関数の呼び出しとか。
Blueprint Interfaceとかイベントディスパッチャとかあるようだが、色々試したところそれほど複雑でない処理ならCustomEvent使用が一番手軽かつ確実なのではないだろうか。

参考リンク
ブループリント通信の使用方法 | Unreal Engine
8 - Adding Interaction - YouTube
10 - Dynamic Material Instances, Part 2 - YouTube

やること

シーン中に設置してあるボタンを押すことでライトの色を変更する。
ボタン毎に変更後の色は異なる。www.youtube.com

全体構成

f:id:DYMN:20150610011439p:plain
Light(のBP)を各Switch(のBP)で呼び出す。SwitchBPは全く同一のBPであり、保持する指定色変数が異なる以外は同じ挙動を行う。
LightBPは呼び出された時に引数で渡された色で色を変えることのみ行い、呼び出し元が何であるかは考慮しない。

スイッチ側の設定(RGB_LightSwitch_BP)

  • スイッチとなるアクタを設置し、その周囲をTriggerBoxで囲う。
  • 新変数として「NewColor(LinearColor)」「TargetLight(Actor)」を追加、公開してアクタのDetailsから設定できるようにする。

f:id:DYMN:20150610011932p:plain
f:id:DYMN:20150610012121p:plain
※DetailからNewColorに変更後の色を、TargetLightに変更対象となるLightのアクタを設定する。

Event Graph
  • TriggerBoxへのBegin/EndOverlapを開始イベントとして入力受付の可否を設定する。
  • Blueprint Interfaceやディスパッチャも少しいじったりしてみたが、単純な処理の場合はおそらくカスタムイベントで充分かと思われる(というかBPIF等はまだ理解してない)
  • Key Inputを開始イベントとしてRGB_Light_BP(対象となるライトアクタのBP)へのキャストを作成する。対象となるObjectには変数Target Lightを接続し、さらにカスタムイベント「CE_ChangeLightColor」へと繋げる。

f:id:DYMN:20150610012544p:plain
f:id:DYMN:20150610012557p:plain

ライト側の設定(RGB_Light_BP)
  • 他から呼び出されるカスタムイベントノード「CE_ChangeLightColor」を設置。そこからSetLightColorの処理を呼び出す。
  • Targetは自身が抱えるLightComponent、指定するNewLightColorはカスタムノードから受け取ることができる。

※正確な順番としてはライト側でAdd Custom Event->CE_ChangeLightColor作成->CustomEventのDetails内Inputsに必要な変数情報を設定->Switch側からのCustomEvent呼び出しという形になる。
f:id:DYMN:20150610013115p:plain

おまけ:スイッチアクタの色を変える

設定した色別にスイッチのシンボルの色を変更する

  • Construction Scriptで色を変更するメッシュのマテリアルを取得する。今回の場合はDetails内のMaterials、Element 1を修正するのでIndexには1を指定。
  • MaterialEditor内でシンボルの色を指定している部分を確認する。今回は「0.57,0.333,0.0342」というノードがオレンジ色を指定していると思われる。
  • 現在色をしているConstant 3Vectorノードは名前の設定ができないため、新たにVector Parameterノードを作成、代わりとしてLerpに接続する。別に色はなんでもいいのだが、デフォルト値としてひとまず元ノードと同じ値を設定しておく。
  • Construction Scriptの操作に戻る。Parameter Nameで変更するノード、この場合は新たに作成した「OrderColor」を取得し、ValueにNew Colorをセットする。
  • 以上により、アクタのDetailsにてライトの新たな色を指定すると同時にシンボルの色も変わるようになる。

f:id:DYMN:20150610013354p:plain
f:id:DYMN:20150610013338p:plain

艦これ - 春イベント突破

合間を見て進めていた春イベント、どうにかこうにか全部甲で突破完了。
E4までは楽勝、E5で若干足踏み、E6ラスダンでかなり詰まったが重量戦艦に電探積んで命中重視構成にすることで理想的な形で夜戦に移行することができた。
最期は空母棲姫を一撃昇天させた綾波のナイスアシストもあり、雪風がまえばで〆。
その後のRoma掘り中にU-511もドロップし、無事雲龍も発見できたしでほぼ理想的な形でイベント終了。おつかれさまでした。

さて、貯まっていた任務を処理していかなければ。

最終構成+決戦支援
f:id:DYMN:20150518024937p:plain
結果
f:id:DYMN:20150518025416p:plain
成果(谷風以上)
f:id:DYMN:20150518025444p:plain

Unreal Engine - エラー対応

1. Graph is linked to private object(s) in an external package

Version:4.7.3
症状:メインウィンドウでSAVEしようとすると以下のエラーメッセージが表示され失敗する。

Graph is linked to private object(s) in an external package.
External Object(s):
BoxComponent_5
Try to find the chain of references to that object (may take some time)?

Graph is linked to external private object (unknown culprit) (unknown property ref)

原因:Blueprintウィンドウ内のViewport編集画面でアクタを「Ctrl+C->Ctrl+V」でコピペすると発生するっぽい。
対策:発生した場合は上記原因で作成したアクタを削除する。アクタのコピペはDuplicateで行うようにすると発生しないっぽい。


Unreal Engine - ドアの実装(両面開き自動ドア)

前回に引き続き両面開きの自動ドアを実装する。www.youtube.com
以下動作条件。

  • 開閉はプレイヤーの接近に応じ自動で行う(プレイヤーのキー操作に拠らない)
  • プレイヤーがドアの前に立つことで自動的にドアが開く。
  • プレイヤーがドアの前にいる限り開いた状態であり続ける。
  • プレイヤーがドアの前から離れると閉じる。

Viewport図

f:id:DYMN:20150515021506p:plain
両面それぞれを親子の関係にはしない。

Blueprint図

f:id:DYMN:20150515021521p:plain
※OCBO・OCEO=OnComponentBeginOverlap・OnComponentEndOverlap

OCBO(TriggerBox)->SlidingDoorTimeline(Play)
OCBO(TriggerBox)->Delay->SlidingDoorTimeline(Reverse)
プレイヤーが自動ドアに近づいた時、SlidingDoorTimelineノードを呼び出す。
Timelineノードは値を0から100程度まで一定時間で変化させている。
※プレイヤーが離れた時に閉まる処理は1テンポ送らせるため、Delayノードを噛ませている。

Timeline->(*-1)->Make Vector
Timeline->MakeVector
Timelineノードで生成した値から移動量を作成する。MakeVectorノードのY値に入れることでVectorへと変換する。
片方は反対方向へ移動するため、2つの出力の内片方は-1をかけて符合を反転する。

MakeVector+InitialLocationDoorA/B->Set Relative Location
あらかじめ取得してあったドアの初期位置に生成した移動量ベクトルを足しこみ、対象アクターの新位置ベクトルへとセットする。

EventBeginPlay->Sequence->SET:InitialLocationDoorA/B<-RelativeLocation<-SMGlassWindowA/B
イベントが始まると同時に実行される。ドア両面アクタの初期位置を取得しておく。

※Sequenceノードを使用することで同時に別個の処理を呼び出すことが可能となる。

Unreal Engine - ドアの実装

Blueprintを使用しシーン中、プレイヤーのアクションに応じて開閉するドアを実装する。

www.youtube.com

基本的な動作条件は以下の通り。

  • 開閉は押し引きで行う。
  • プレイヤーがドアの前に立った状態でボタンを押すたび開閉を行う。
  • プレイヤーがドアの前に立ち、操作を一切行っていない場合はガイドメッセージを表示する。
  • 一度操作を行った後、ドアの前から離れるまではガイドメッセージは再表示しない。

Viewport図

f:id:DYMN:20150515010731p:plain
TriggerDoor:ドアの開閉を受け付ける当たり判定。ドアの前後に伸びている。
TriggerDoorFront/TriggerDoorBehind:プレイヤーがドアのどちら側にいるのか判断する当たり判定。それぞれ大きさはTriggerDoorの半分。TriggerDoorと重なるようにドアの前後に設置する。

※ドア本体(この場合はDoorActor)の子に当たり判定を設置しないこと。さもないとドアが開いた時に当たり判定自体も回転してしまう。

Blueprint図

f:id:DYMN:20150515010736p:plain

※OCBO・OCEO=OnComponentBeginOverlap・OnComponentEndOverlap

上部

OCBO(TriggerDoorFront/Behind)->SET:Current Door Trigger
プレイヤーが現在、「ドアのどちら側にいるか?」を取得する。取得した側のボックスコリジョンを変数「Current Door Trigger」にセットする。
OCEO(TriggerDoorFront/Behind)->Set Visibility(FALSE):MsgDoorHandleB/F
ドア前後の当たり判定を「抜けた」時には自動的にガイドメッセージを消去する。

中部

OCBO(TriggerBox)->Enable Input->Set Visibility(TRUE)
プレイヤーが新たにTriggerBoxの中に入った場合、入力を受付可能にすると共にガイドメッセージを表示する。
OCEO(TriggerBox)->Disable Input->Set Visibility(FALSE)
プレイヤーがTriggerBoxの中から抜けた場合、入力受付を不可にすると共にガイドメッセージを消去する。
CurrentDoorTrigger->Get Child Component->Set Visibility
Visibilityをセットする際、対象となるテキストアクタはあらかじめ取得してあるCurrentDoorTrigger変数に格納されているアクタの子を取得する。

下部

プレイヤーの操作を受けて実際にドアアクタの角度を変更する。
F:Fキーの入力を受け付ける
Branch:変数IsCloseの内容に従い、処理を振り分ける(ドアを開けるか、閉めるか)
IsCloseがTrue(閉まっている)場合はIsCloseをFalse(開いている)に変更し、TimeLineノードのPlayに接続する。
IsCloseがFalse(開いている)場合はIsCloseをTrue(閉じている)に変更し、TimeLineノードのReverseに接続する。
※TimeLineノードは単純に数値を0~90まで1.5秒かけて増加させている。Easingを入れるか否かはお好みで。
0~90(Play)または90~0(Revrese)の値をMake Rotノードで受け取りRotationに変換する。回転はヨーイングで行うのでYawへ接続。
DoorActorを対象とするSet Relative Rotation(相対回転)ノードのNew RotationにMake Rotの出力を接続し、実際のドアの回転を行う。
ドア開閉の入力を受け付けた後はガイドメッセージを消去する。