前回に引き続き初心者がUE4を絡めたPythonライブラリを作れないか色々進めていく記事です。
前回の記事 : #1
回転制御の検証のところから再開していきます。
回転制御をどう組もうか考える
前回、各立方体の位置の値が取れるようにしたり、ピボットポイント的なところを考えたりしていました。
とりあえず実装自体はなんとかなりそうではありますが、考えるべきことが結構あります。
必要な回転ば、オレンジ面での各列を縦方向に回すか、横方向に回すかと、側面(例えば黄色の面)で縦方向に回すか・・・の3パターンあると思います。
それぞれが3列あるので、以下のようなパターンでしょうか。
- rotateX1 -> オレンジ面から見て、上のブロックを水平方向に回転
- rotateX2 -> オレンジ面から見て、真ん中のブロックを水平方向に回転
- rotateX3 -> オレンジ面から見て、下のブロックを水平方向に回転
- rotateY1 -> オレンジ面から見て、左のブロックを垂直方向に回転
- rotateY2 -> オレンジ面から見て、真ん中のブロックを垂直方向に回転
- rotateY3 -> オレンジ面から見て、右のブロックを垂直方向に回転
- rotateZ1 -> 黄色の面から見て、左のブロックを垂直方向に回転
- rotateZ2 -> 黄色の面から見て、真ん中のブロックを垂直方向に回転
- rotateZ3 -> 黄色の面から見て、右のブロックを垂直方向に回転
また、それぞれ左右・上下などの方向が必要なので、さらに2倍の数が必要になってきます。
ある程度引数とかで抽象化しつつ、それぞれの関数をレベルのブループリントに追加していきます(関数名は適宜調整)。
基底クラスに名称の変数を追加しておく
少し横道に反れますが、回転制御などを造り込む際に、今どの立方体を扱っているのかが分かりやすいようにするため、識別用の名前を基底クラスに追加しておきます。
ブループリント名自体が取れたりするのかな?と思いましたが取れない?ようで・・・
参考 : How can I get a blueprints name? or a class reference to self in a blueprint?
この追加した変数にサブクラス側でブループリント名に準じた名前(Orangeなど)を設定していきます。
他の一通りのサブクラスにも同様に名前を付けておきます。
名称設定のチェック処理を入れておく
こちらも手作業で設定しているので、うっかりミスをしていないか確認するための関数を追加しておきます。
少し前に追加したものと同様に、forEachLoopで回していきます。
1次元目のループで、設定した名前が空(未設定)のものが無いかチェックしています。
もし空のものが残っていれば、対象の立方体の配列のインデックスをログ出力するようにしました。
また、赤文字部分や表示時間は統一するため定数化しました(TEXT_COLOR_ERRORとDURATION_ERROR_TEXT)。
色の設定は便利なことにColorクラスが用意されているようです。
ここで用意した定数は既存の他のエラーログ表示の箇所にも反映しておきます。
一旦確認で、ゲーム開始直後に実行されるようにしておいて、nameの値が未設定のものが無いかを確認しておきます。
試しに、1つのブループリントクラスでわざと名前の設定を切り落としてプレイしてみます。
無事エラーメッセージが出ました。大丈夫そうです。
続いて2次元目のループで、以下のような処理を作っていきます。
- 比較同士の立方体が同一のものじゃない場合に処理を実行(1つ目のBranch)
- それぞれのnameが同じ場合にエラーメッセージを出力(2つ目のBranch)
文字列設定とPrint String部分は今までとほぼ同じです。
俯瞰してみるとこんな感じになっています。
試しにわざとBP_GreenのnameをOrangeにしてみて、エラーメッセージが出るか確認します。
想定した感じにメッセージが出力されました。
なお、今更ですが、本当はこういったチェック、機能テスト的に切り分けてテストを動かす時のみ実行されるみたいな方が良さそうな気配がしますし、なにやらAssert○○というブループリントのノードがあったので、恐らくテストのための機能も用意されているのでしょう。
しかし、大した処理負荷ではないのと、どんな感じにブループリントでテストするのがベストプラクティスなのか良く分かっていない点、仕事ではないので厳密な感じじゃなくても良さそうなので実装しやすさ優先で引き続き進めていきます。
※追記 : 途中から、チェック用の関数がvalidate○○よりもassert○○の方がしっくり来るように感じてきたので、assertの方にリネームしてあります。今後の追加もassertの方を使っていきます。
IDE的に、リネームすると参照しているところも自動で修正してくれるのは楽でいいですね。
位置の値から対象の立方体を算出する関数を作る
回転制御をする上で、回転対象となる立方体の参照を位置の情報から算出する必要があるので、その関数を作っていきます。
引数にはXYZの3つの値、返却値には対象の立方体のブループリントの参照を返すようにします。
ループで各立方体に対して回して、XYZの位置情報の値でベクトルを作ります。
また、引数で指定されているXYZの値からもベクトルを作っておきます。
用意した2つのベクトルを比較して、一致していればその立方体の参照を返却するようにしておきます。
俯瞰すると以下のようになりました。
立方体の取得結果の動作確認用の関数を追加しておく
テスト的に、特定の位置を指定して取得された結果をチェックする関数を作っておきます。
これもゲーム開始時に実行されるようにしておきます。
オレンジ・赤・緑・白の面を持つ立方体を取得してみて結果がちゃんと合っているか、といった感じに対応します。
引数でチェック対象の立方体を渡して、初期位置情報を取得しておきます。
続いて関数軽油で取得されたアクターと引数に指定されたアクターを比較して、もち一致していない場合にエラーメッセージを表示します。
エラーの時にそれぞれの立方体がどれになってしまっているのかの確認で、それぞれnameの値を取得します。
エラーメッセージ回りは今まで通りです。
俯瞰すると以下のようになっています。
プレイ開始の箇所に追加しておきます。
オレンジ・赤・緑・白の面を持つ各ブループリントのものをそれぞれ実行するために連結しています。
一度ゲームをスタートしてみて問題ないことを確認します。
エラーメッセージ表示の関数を調整
チェック用の関数が増えてきたのはいいですが、記述が少し重複していて煩雑なのと、どの関数なのかが分かりやすいようにするため、この辺りの処理を関数化しておきます。
関数自体はシンプルですね。これで各チェック用の関数のエラーメッセージ出力の箇所を差し替えていきます。
X軸方向への回転対象の立方体のリストを取得するための関数を追加する
回転処理の前に、追加でX方向への回転(rotateX1など)で対象となる立方体のリストを算出する関数を用意していきます。
最初からrotateX1、rotateX2、...と個別に作っていってもいいのですが、一段階抽象化した状態の関数で、引数にY位置の値を受け付けるような関数を用意します。
また、立方体の基底クラスで定義してある位置情報の定数を、便利なようにレベルのブループリントでもダイレクトにアクセスできるようにしておきます(若干、生成済みのアクターから持ってくるのが気持ち悪い気がしないでもないですが・・・ただ、逆にレベルのブループリント側で定義されているものを各立方体側のブループリントに持っていくというのも違う(レベル依存になる)ような気がするので、これはこれでいいのでしょうか・・?)。
引数のY位置の値を、各所でアクセスしやすいように関数のローカル変数にしておきます。
UE4でローカル変数ってどう宣言するのだろう・・と少し調べていたら、そもそも関数の編集画面であればローカル変数のセクションがMy Blueprintウインドウに表示されるようです。気づかなかった・・
ブループリントの作業時は、My Blueprint タブを使って 変数 を 追加することができます。関数内からは、[My Blueprint] タブに Local Variables 用の追加セクションが表示されます。
ブループリントのベスト プラクティス
確かに左下にありました。こちらから追加するか、もしくは引数からノードを引っ張った際に、Promote to local variableというメニューが出るので、こちらからでも作れるようです(こちら経由の方がシンプルではあります)。
追記 : というより、普通に引数は変数化しなくても関数内であればどこでも参照できるようです。変数化要らなかった・・・
関数はcubeYという引数名でYの位置を受け付けるようにします。
また、見やすさのためにZの値に応じて処理のグループを分けるようにSequenceノードを使って3つに分割しておきます。
続いて1つ目のグループ(オレンジの面を基準に、手前の3つの立方体の取得処理)を作ります。
中央のグループも同様に対応します。真ん中の立方体の処理だけ特殊で、Yの値が1か3だったらノードを繋ぐ形に設定しています。これは、Y=2且つXとZも2の時の位置の立方体は存在しないためです。
奥の位置にある立方体も同様に対応します。手前側の処理とほぼ同じです。
そして、取得した各立方体で配列を作ります。Yが2かどうかで配列の件数が変わるのですが、こういったケースの場合配列のRemove Indexなどで取り除くかどうかという実装をする方がスムーズか?と思って触っていましたが、Remove Indexしてもなんだか配列の件数が減らない?気配がしたので、単純に配列を分ける形で対応しています(使い方が悪いだけな気がするので後で調査)。
俯瞰するとこんな感じになっています。
動作確認用の関数を書いていきます。
全部のパターンは確認しませんが、大雑把に以下のパターンを試していきます。
- Y=1を指定してみて、長さ9の配列が返却されること。
- Y=2を指定してみて、長さ8の配列が返却されること。
- Y=2を指定したときの、返却される立方体が想定通りになっていること。
まずはCUBE_Y_1を指定した時の返却される配列の長さが9にちゃんとなっていることを確認します。
エラーメッセージの表示部分はほぼ今まで通りです。
CUBE_Y_2を指定した時の配列の長さ(8想定)に関しても同様に対応します。
続いて配列内の立方体のアクターが想定したリストになっているのかの確認を進めます。
配列でFINDのメソッドが用意されているようなのでそちらを利用します(見つからなければ-1が返るので、-1が含まれていないことを確認します)。
Make ArrayでFINDの結果の配列を作り・・・
ForEachLoopでループを回して、-1が含まれていないことをチェックします。
俯瞰 :
他と同様、ゲーム開始時にチェックされるようにしておきます。
なお、最終的にはエラーメッセージ無く通ることを確認していますが、その過程で何度かつまづいています(あれ、この関数配列の長さ減らないの?とか、配列のピンに処理が通らないようにしていても長さは固定になってしまうのか、etc)。
若干単体テストみたいなことをしていますが、不慣れな技術なので、想定した動作になっているのかのチェック大事だな・・・と感じています(後々のデバッグの負担を減らしたいというのもありますが・・)。
基準点制御用の不可視の立方体を追加しておく -> 試してみてうまくいかず
回転の際のピボットポイントをずらすための不可視の立方体を基底クラスに追加しておきます。
とりあえず位置は回転タイミングで毎回変更するので、あまり気にせずにとりあえず隣に配置します。
配置後、Visibleのチェックを外して不可視にしておきます。
しかし、ここでうまくいかず。回転の基準点がずれてくれませんでした。どうやら、レベル上で立方体を追加してグループ化した時と異なり、ブループリント上で追加する形だと基準点がずれてくれない模様です・・・
※分かりやすいように一旦可視化してあります。
どうしようか・・・と考えていましたが、UE4 - Rotate Actor Around Point TutorialやHow to rotate Actor around non-pivot Location?を少し眺めていた感じ、何だか自分で書いてしまってもいい気がしてきました。
You need to do some vector math, ie calculate new location and rotation of actor.
というより、基準点関係を扱わなったり三角関数的なことを考えなくても、向きの回転と座標の一定値ごとのアニメーションくらいで問題ないような気もしてきました(若干アニメーションがルービックキューブっぽくないですが、そもそもUE4に慣れるのと強化学習用のシミュレーション環境を作るという目的から考えると)。
且つ、一つの軸だけで考えると簡単ですが、複数の軸で回転を考えると結構面倒くさい(横方向に回転させた後に縦方向に回転させるなど)ですし、時間もかかりそうです。
そこに時間かけるのも何だか微妙な気がしてきたため、基準点を考慮しない形のアニメーションで進めようと思います(結構長くなりそうなので、最初からこだわっていると終わらなくなりそうな気配もあるため・・)。
横軸・左方向への回転の関数を作る
少し考えた結果、回転方向ごとに関数を分けるようにしました。
まず最初に横軸の左方向への回転の関数を作るようにします。
関数名はrotateXLeftとし、引数に対象のキューブの縦方向の位置を指定するようにします(回転対象が1行目なのか2行目なのかetc)。
キューブの基底クラスに追加します。まずはじめに自身の縦軸の位置が引数に指定された価と一致しているかをチェックしています。
また、回転中の指定になっているかどうかの真偽値をクラスの変数に追加しました。
1~3で回転対象のキューブがどれなのかという指定をしています(上のキューブを回転させようとしているのか真ん中が対象なのかetc)。
今回の関数で扱ういずれかの真偽値がTrueになっていることをチェックする処理を加えておきます。
その後のシークエンスノードで、以下の3つの処理を順番に実行しています。なお、この関数はアニメーションや回転が終わるまで繰り返し実行する想定のため、1フレーム分の計算といったように組んでいきます。
- キューブの移動制御
- キューブの回転処理
- 回転前の位置情報の保持(回転開始時の1回のみ)
キューブの移動のアニメーション設定
現在のキューブの位置に応じた、キューブの移動アニメーションの方向や移動の設定をしていきます。
ルールでいけそうな気もしないでもないですが、対した量でもないので一つ一つ設定した方が早そうな印象を持ったため、単純に分岐させて設定していきます。
cubeYの値は引数で指定されているので、cubeXとZの位置情報に応じて分岐させていきます。
位置情報が該当した場合に、目標とする座標にアニメーションするように、1フレーム分のアニメーション設定をします。
回転が色々な軸に対して実行されるので、移動方向の制御が面倒くさくない?と思いましたが、ちゃんと世界の軸方向を規準とした移動の関数があるようです(AddActorWorldOffset)。
そのため、アクターが回転し続けていても、同じ方向に対して同じ量の移動をしてくれます。
200 / 90
や100 / 90
といった計算をしていますが、これはキューブの大きさが100で、キューブ1つ分の移動であれば100、2個分であれば200といった具合で設定してます。また、90という値は、回転を1フレーム辺り1度を想定しており、一度の回転で90度回転が必要なため、90フレームかけてアニメーションします。そのため、移動のアニメーションも回転と同じタイミングで完了するように90分割して1回の関数で移動するようにしています。
これを、各位置の組み合わせでそれぞれ作ります。
キューブの回転制御
続いて回転の方も作っていきます。
前述の通り、関数1回につき1度回転するようにしておきます。
また、現在の回転量を1~90で変数に保存しておくようにします。
もし回転量が90になっていたら、回転の各処理を停止するようにするため、判定の分岐を追加しておきます。まずは回転量の値を0にリセットさせます。
回転指定の真偽値もFalseにします。
各組み合わせによる位置情報を、移動後の位置に合わせてアップデートします。
この分岐も各パターンで作っておきます。
回転完了前に、アニメーション開始時の位置情報を保持しておく
回転量が1の時(=最初のアニメーション時)に、アニメーション開始時の位置情報を保持するようにしておきます。
クラスの変数にbeforeCubeXなどの値を追加しておいて、その値を別の関数でアップデートするようにしておきます。
アップデートの関数自体は以下のようなシンプルな内容になっています。
これで横軸の左方向への回転の関数は完了です。
若干煩雑な箇所がある気が・・しないでもないですが、関数分割したりしてもそこまで変わらなそうな気配もするので、一旦このまま進めます(他も実装していて、あまりに気になったら抽象化とかもう少し考えます)。
俯瞰 :
チェック用の関数を追加しておく
回転の関数の挙動のチェック用の関数をレベルのブループリントに追加しておきます。
アニメーション完了時の実行を想定するため、回転量の値が0になっている時のみ実行されるようにしておきます。
また、その後のシークエンスノードで、以下の3つの条件でチェックします。
- 回転制御用の真偽値がFalseにリセットされていること。
- cubeYの位置情報がアニメーション前と後で、変わっていないこと(横方向の回転なため)。
- 各位置情報の組み合わせに応じた、アニメーション後の位置情報が想定した値になっていること。
順番に対応していきます。
回転制御用の真偽値がFalseにリセットされていることを確認する
シンプルに、各真偽値の変数がFalseになっていることを確認します。
cubeYの位置情報がアニメーション前と後で、変わっていないことを確認する
上中下とそれぞれチェックする必要があるので、配列を作っておいてループで回してチェックします。
後は、アニメーションの前のcubeYの値やアニメーション後の後のcubeYの値が引数に指定されたものになっているかを確認します。
各位置情報の組み合わせに応じた、アニメーション後の位置情報が想定した値になっていることを確認する
チェック用のデータで、ブループリントで多次元配列を作ろうとしたら、ストレートに作れない?気配があります(擬似的には使えるっぽい)。
BPは使えるデータ構造が少なく1次元配列しか使えません。1次元配列だけでも組むこと自体はもちろん可能ですが、せめて2次元配列ぐらいは欲しい所です。
UE4 BPで2次元配列を実現する
そのため、多次元配列ではなく4つの1次元配列の組み合わせでチェックしていきます。
チェック対象のX位置・Z位置の組み合わせで、想定している移動後のXとZの位置の値を格納した配列を用意して進めていきます。
ループでアニメーション開始時の位置の値を参照して、対象の組み合わせを探します。
アニメーション後のcubeXの位置情報が想定したものになっていることを確認します。
同様にアニメーション後のcubeZの位置情報が想定したものになっていることを確認します。
これでチェック用の関数も一旦完了です。
縦軸・上方向への回転の関数を作る
同様に縦軸の上方向への回転の関数も作っていきます。
といっても、軸が変わることによって、引数の値が変わったり、各参照や結果の位置などの値が変わるだけで、実装の流れはほぼ同じなので、大部分は省略します。
回転の関数 :
回転の関数の俯瞰 :
チェック用の関数 :
チェック用の関数の俯瞰 :
俯瞰 :
回転させてみる
縦方向と横方向への回転を最低限のものを追加したので、少し動かしてみます。
タイマーによる各フレームでキューブの数だけループする処理を追加します。
用意した2の回転の関数ごとにノードを分けつつ、さらに3つの位置によってさらに分岐するようにしておきます。
もし対象のキューブが回転するための真偽値設定になっていたら、用意した関数を実行するようにしておきます。また、関数の後にアニメーション結果が問題ないかの確認として、追加したチェック用の関数を追加しています。
横軸・左への回転 :
縦軸・上への回転 :
また、一時的な記述ですが、機動後に一定の遅延を挟みつつ、回転設定の真偽値を有効にして回転がされるようにしておきます。以下のように繋げていきます。
プレビューしてみると、大分それっぽい感じで回転してくれています(Ubuntuで作業しているので、gifアニメとかのツールを入れていないのでアニメーションは割愛します・・)。
まだまだ先は長いですが、一旦本記事はこの辺で区切って次の記事に回します。
余談メモ1 : アニメーション後の値の確認は、シミュレートを使うと便利だった
回転や移動後のデバッグで数値を確認などしていたのですが、その際にエディタ上のPlayのオプションでSimulateを選択してPlayを押すと、編集中のようにアクターの選択ができたり、Detailウインドウで値が確認できたりと便利でした(作業中、わずかに数値がずれた際に調査などで使いました)。
余談メモ2 : 途中からライトを4方向からのスポットライトに変更
配置位置などをちゃんとした値にし、且つライトの種類をスポットライトに調整しました。
なんだかフォトショのレイヤースタイルを使っているような感じで、若干グラデーションっぽくなりましたがこれはこれでいいでしょう・・。
余談メモ3 : 変数名の規則
- UE4の動画を見ていて、関数内のローカル変数のプリフィクスにLと付けているのを見かけました。確かにローカル変数の区別としてそういった名前の付け方のルールの方が良かった感が・・・今回は、付けずに進めてしまっているので、そのまま統一して付けない形で進めます。
- 同様に、動画で変数名などをSampleVarといったような命名規則で付けられていたのですが、今回のものだとsampleVarといったように先頭を小文字で進めています。ブループリント界隈だと、どちらが一般的なのでしょう・・・