Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationEventAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
2
Help us understand the problem. What are the problem?

posted at

updated at

UE4でゆうなまを作りたい 第2回

初めに

 本記事は前回の続きになっています。出来れば見るようにお願いします。といっても、前回は全然ユウナマを作っている感が無く、前置きといった事になってしまいました。本記事はちゃんとユウナマを作っている感は出ているので安心して下さい。
 環境や目的などは前回をみて下さい。

UnrealC++のデバッグ方法

 最低限必要な機能を導入したら、プロセスにアタッチ(Ctrl + Shift + R)で「UE4~」を選択します。キチンと動作しているなら、ブレイクポイントが有効状態へ移行します。上手くいかない場合は、UE4を再起動してください。それでも無理ならリビルドをして下さい。

VS2019での最低限必要な機能

  • C++ プロファイリングツール
  • C++ AddressSanitizer(オプション)← 私はこの機能が足りていませんでした。
  • Windows 10 SDK(10.0.18362 以降)

参考サイト:UnrealC++を使う時の要注意ポイント - Qiita

実際に制作

 ようやく、ユウナマの制作過程を記述します(笑)。前回なんて無かった。ユウナマを作っていくといっても、前回は具体的な事までは記述していなかったので、今回は具体的に記述します。

方向性

 とりあえず、出来る限り元のゲームに似せていく方針で考えています。というのも、私自身一人でゲームの企画を立てられるほどアイデアは降りてきません。なので、企画部分は元のゲームに任せて、プログラム部分に集中したいと考えているからです。
 モデルなどアセットについてなのですが、基本的には無料アセットを使っていく予定ですが、個人的に納得できなかったり、直感で「これは良い!」となったアセットに関しては購入しています(ただし安い物に限る)。

進捗報告について

 基本はTwitterに動画を上げています。なのでQiitaに上げるのは、ある程度まとまった物になっています。最新の進捗を知りたい物好きな人はフォローしてあげてください(宣伝&土下座)。
HnniTns - Twiiter

 そもそも、なぜQiitaに上げているのかというと、進捗をまとめて残しておくというのは勿論なのですが、Twitterには収まらない情報を残しておきたいと考えているからです。

制作についての前置き

 これも前回にも書いていますが、私自身UE4についての知識があまりありません。ですので、「おかしいだろ」「こうした方がいいだろ」ポイントが沢山あると思いますが、優しく指摘してあげてください。それと、今回の制作自体「UE4のテスト」兼「Unreal C++のテスト」兼「個人制作」です。

ブロックの作成

 まず最初にブロックについて書いていく事にします。皆さんご存知の通り、ユウナマの地形は縦横72x60になっています。つまり、4320個のブロックを用意しなければいけません。普通に考えても量が多いです。なので、UE4にはUInstancedStaticMeshComponent(以下ISMと略します)があり、ISMをによってインスタンシング1をしてくれます。
 ISMを使うにあたって非常に苦労しました。というのも、UE4が使い始めだったのと、UE4でのISMの使い方になれなかったという事もあり、かなり時間を取られました。具体的には3週間ですね。
 調べているとAuto Instancingという機能が存在したのですが、試しにやったところ何故か働いてくれなかったりしたので、諦めてISMを使用する事にしました。
 そんなこんなで、どうにか表示する事が出来たものが下の動画になっています。

 この時点ではマテリアルに透明度のモードを適用していて、描画順がおかしい事になっていますが、これは後で修正されています。

超簡易的なクラス図

 基本的にはISM_GridBaseを継承して各種類ごとに分けてクラスを作成しています。更に、それらをまとめて管理する為のマネージャークラスを作成しています。詳細は下図の超簡易図をご覧ください。
ISMの超簡易図.png
 「ISM_GridBase」というグリッドクラスをブロッククラスである「ISM_BlockGrid」として継承して使用しています。上図には書いていないのですが、各ブロックの種類に応じて更に継承させています。例えば、「普通のブロック」「養分Lv.1~3」「魔分Lv.1~3」という感じに分けています。これらは流石にブループリントを使った継承クラスにしていますが...。つまり、これらは描画用のグリッド・ブロッククラス、それらを束ねるマネージャークラスといった形になっています。
 それとは別に、通常のグリッド・ブロック・マネージャークラスも用意しています。こっちは実際に配置していく更新用のグリッド・ブロッククラス、それらを束ねるマネージャークラスといった形になっています。
image.png

ISMとインスタンスカウント

 なぜ、上記の形にしているかというと、一言でいうなら「ISMの仕様」だからです。ISMでインスタンシングをしようとすると、ISMに対して追加・削除を行わなければなりません。その為、それらの置き場として描画用にブロックの種類ごとでブループリントクラスを作成しました。「使うクラスを追加する」みたいな感じだったら楽だったんですが...。残念ながらインスタンスカウントを使用した形になっています。つまり、途中のISMを削除しようものなら、他のインスタンスカウントを調整しなければなりません。もっと、良い方法があるのでしょうか?ありそうですね。
 他にも、座標を変更したい時だったり(UpdateInstanceTransform)、後述するSetCustomDataValueなどではインスタンスカウントを使用して変更します。
image.png

SetCustomDataValuePerInstanceCustomData

 トランスフォームの更新にはUpdateInstanceTransformを使用しますが、それとは別に個別のデータを与えたいとなった場合はSetCustomDataValue使用すれば可能ですが、floatでの管理なので少しめんどくさかったりもします。
 マテリアル側での取得はPerInstanceCustomDataノードで取得可能ですが、指定方法がノードの「Data Index」に値を直接指定しなければなりません。更に、ISM側の「NumCustomDataFloats」を指定しなければ、配列外アクセスでクラッシュレポート行きです。ついでに言うと、マテリアル側で「Used with Instanced Static Meshes」を指定しなければマテリアルがISMに適用されません。
 注意点として、SetCustomDataValue「bMarkRenderStateDirty」をTrueにしないと、ISM側に同期されません。つまり、SetCustomDataValueを使う場合の同期タイミングはユーザー任せになっています。ほかに更新されるタイミングは「ISMの追加・削除」「トランスフォームの更新」ぐらいだと思います。気を付けないと「あれ?変更したはずなのに更新されてねぇ!」となります。逆に言えば、連続で変更したい場合は、最後だけTrueにすることで負荷を軽減することが可能です2

ISMの注意点

以上で上記の事をまとめると

  • ISM:「NumCustomDataFloats」に変更したい種類の上限値を設定する(FVector(X, Y, Z)を変更したいなら3を指定)
  • プログラム:「SetCustomDataValue」関数の引数「bMarkRenderStateDirty」をTrueにする
  • マテリアル:「Used with Instanced Static Meshes」にチェックを入れる
  • マテリアル:「PerInstanceCustomData」ノードを使い、「Data Index」に種類のインデックスを設定する(FVectorならX=0, Y=1, Z=2)

以上、これらを守ればちゃんと使えます。これから勉強する人が、無駄に時間を食わないようにここに書き残します。

ISMStaticMeshの速度差

 このような感じで、使うと使わないでは大きく速度差が生じています。ISMは一癖ありますが、速度は圧倒的に早くなります。出来ればAuto Instancingを使いたかったですが...。これも、勉強の一環ですね。

ピッケルの作成

 これは簡単で、単純にPawnを使用して、入力を受け取るようにすればOKですね。モデルに関しては遊び心として、マイ〇ラのダイヤモンドピッケルを使用しています。

 因みに、カメラとピッケルは分けています。というのも、カメラはピッケルの動きに同期するつもりはないので、別々で動いてくれたほうが便利だからです。その為、カメラはカメラアクターとして別にスポーンしています。

操作方法について

 ここで何気に重要な操作方法について話したいと思います。現状考えているのは、基本操作をマウスで完結するようにしたいと考えています。「ポーズ」などはキーボードで行いますが、「掘る・つつく」「カメラ移動」「ズームイン・アウト」「情報を見る」などはマウスで完結するようにします。
 因みに初代ユウナマを参考にするので、「ダンジョンクエイク」はありませんのでご了承ください。もしかしたら、追加するかもしれませんが...。

掘る・つつく

 これは、左クリックで行います。ゲーム環境に直接影響が出る操作はこれだけです。あとは、モンスター・勇者達が勝手に作っていきます。
 動画の段階では、まったく動いていませんが、後々調整されています。掘るタイミングでピッケルが動く(Rotateを変更している)ようにしているのですが、実は直接関係はありませんでした。といっても、1フレーム単位で掘られると色々困るので、ピッケルが動いている間は掘れないようにしています。更に現状では、どこでも掘れるようにしていますが、これはデバッグしやすいからです。プログラム的には「実行」を渡すだけで隣接しているブロックのみ掘れるようにしています。

 今はつつく相手がいないので寂しいですが、後々出てくるのでご安心ください。

ピッケルの当たり判定など

 掘る時の当たり判定のお話です。最初にピッケルは行・列数を常時更新して保持しています。というのも、そのようにやった方が軽量化に繋がると考えたからです。というのも、ただ単純にLine Trace By Channelのような関数を使って当たり判定を行っている訳ではありません。つまり、エンジン側の当たり判定関数だと全てのブロックの当たり判定を確かめなければならないはずです3。その為、当たり判定をエンジンに頼らず、ピッケルの行・列数を元に当たり判定を求めています。正確な当たり判定ではありませんが、ブロックとの当たり判定などでは十分だと思います。

image.png

カメラ移動など

次にカメラ移動・ズームイン・ズームアウトなどを実装していきます。とりあえず、簡易的に動く感じでやっています。正確には上下左右の動く範囲は画面内で収めないといけません。なので、仮実装という感じになっています。

ブループリント インターフェース

 各アクター間での通信にはこの機能をとてもよく使います。似たような機能にイベントディスパッチャーというのもありますが、今回はインターフェースの方を説明したいと思います。
 そもそも、こいつは何者?となると思います。簡単に言うと「アクター間の依存度を下げつつ情報を受け渡しする」機能です。例えば、エネミーアクターがプレイヤーの体力を参照したいといった事がある場合、GetAllActorsOfClassなどを使用し、キャストをする事で取得が可能です。ですが、それでは直接情報を取得する事になりますし、参照がより複雑化していくため、重くなる上に、バグ解決で非常に厄介な事になってしまいます。なので、直接取得する前に「インターフェース」という「ワンクッション」を置く事で参照を減らすことが可能になります。
 詳細は「【UE4】InterfaceとDispatcherの使い分け方-自分流【★★☆】 - キンアジのブログ」をご覧ください。

実装方法

 「コンテンツブラウザ > 右クリック > ブループリント > ブループリントインターフェース」で作成が可能です。
image.png
 そして、作成したい関数をブループリントインターフェース側に設定します。ただし、ここでは引数・戻り値のみ設定可能です。内容はここでは実装しません。
image.png
 実際に使用するには、呼び出される側のブループリント「クラス設定 > インターフェース > 実装インターフェース > 追加 > 作成したインターフェース」で設定します。
image.png
 そして、呼び出される側のブループリントのインターフェースに作成した関数が出現します。ここで注意する点があります。関数の実装時には必ず「作成した関数 > 右クリック > イベントを実装」をして下さい。ただし、戻り値を設定する際には「作成した関数 > 右クリック > グラフを開く」で実装して下さい。やり方が戻り値の有無で二通りある上に、初見だと割と躓く点だと思います。
image.png
 実装する関数は下図のように戻り値の有無で変化します。割と紛らわしいですが仕方ありません。
image.png
 呼び出す側では、「呼び出したい側のブループリント上で右クリック > クラス > 作成したインターフェース > 作成した関数」でメールのイラストがある関数を召喚し、呼び出される側のオブジェクトをターゲットの引数に設定します。
image.png
 詳細は「Blueprint InterfaceでBlueprint間の通信を行う - note」や「 【UE4】InterfaceとDispatcherの使い分け方-自分流【★★☆】 - キンアジのブログ」をご覧ください。

モンスターの出現

 ブロックの種類で出現するモンスターを変更しています。モンスターモデルは下記のものをセール中に購入し使用しています4。因みに、魔王は一番下の「Hara」を採用しようと思っています5

※ 使うたびに思うのですが、Epic Games Launcherさん使い勝手が悪すぎませんかね...。余りにも使いづらいので、Chromeで検索・購入して、ダウンロード時にランチャーを開くというやり方で使っています。特に購入済みコンテンツの並び替えが無いのが致命的すぎるのですが...。

超簡易クラス図

 2回目の超簡易図です。基本はC++で実装して、各キャラクターはブループリントクラス継承、スポーンした各キャラクターをマネージャーが管理するといった形にしています。これはあくまで私の考えで正しいかは分かりません。ですが、とりあえずこのやり方で行っていきます。このやり方の利点はマネージャーで追加・削除・変更などを一括管理出来る事だと私は考えています。更に、マネージャー同士でブループリントインターフェースを使って各キャラクターの情報をやり取りする事で、2段階のクッションを挟めることも利点だと考えています。
 なぜ、モンスターと勇者の基底クラスを同じにしているかというと、ダンジョンでは勇者もモンスターの一員と考える事が出来るからです。詳しい理由は、ユウナマのCEDECで語られているので、「勇者のくせになまいきだ cedec」と調べて見てみて下さい。関連するPDFの資料が無料で見れます6
キャラクター簡易図.png

追加機能

 ただただ壊すだけだと違和感が凄いので、壊した瞬間にヒットストップ・被破壊メッシュの実装もしました。細かいですが、デーモンの実装もしています。ただし、魔法陣の実装はまだできていないので、単純に出現するのみになっています。これについては後々実装します。

被破壊メッシュ(Destructible Mesh)

 被破壊メッシュについてなのですが、これは下記の動画を見ていただければ分かるようにバラバラに散らばりながら消滅します。
image.png
 これはかなり良い機能だと思いますが、Epicさんは既存のバグを修正するつもりは無く、余りやる気が感じられません。というのも、Chaos Destructionという新しい機能を押しているからです。なので、移行する事も考えておくべきかもしれません。下図を見ていただければ、確かにこの出来栄えはEpicさんが推すわけですね。
Chaos Destruction.png
 被破壊メッシュの注意点としては、破片一つ一つに当たり判定があるので、不要ならNo Collisionにしておくべきです。更に、完全に削除するには被破壊メッシュ用アクターそのものを削除します。でないと破片が残ったまま止まり続けます。ちなみに、Set Materialを使う事で同じDestructible Meshで別のマテリアルを使う事が可能です。
 なので、今回はこの機能を使いブロックが削除される瞬間に、被破壊メッシュ用アクターをスポーンして、初期化時にマテリアルを変更する方法を取りました。そして、一定時間後に徐々に小さくしつつ削除します7。最初に実装した時になぜか適用されず、マテリアルそのものが適用されていない灰色になっていました。再起動したら治りましたが...なぜだ...。

 詳細は「被破壊メッシュ(DestructibleComponent)についてのメモ - Qiita」「UE4 新しい物理破壊システムChaos Destructionを使ってみよう - Hatena Blog」をご覧ください。

ヒットストップ・スロー演出

 今回はお試しも含めて、掘る際にヒットストップをほんの少しだけかけるようにしました。やり方はとても簡単で、Set Global Time Dilation関数に値を設定するだけです。とっても簡単!
ヒットストップ.gif
 詳細は「UE4 ワールドの時間速度(ゲームスピード)を早くしたり遅く(スローモーション)したりする - FC2」「UE4 ヒットストップの実装について - Hatena Blog」をご覧ください。サイトにもかかれているのですが、これらの影響を受けたり受けなかったりするものがあるので注意して下さい。詳細は参考サイトを確認して下さい。

マテリアル(エミッシブ関係)

 実はブロックを選択した時に、ブロックを点滅させるようにしました。これは本家にはない機能なのですが、3D化でどのブロックを選択しているかが分かりにくくなってしまいました。その為、ブロックを点滅させることによってかなり違うと思ったからです。
 ただ、単純に全体を点滅させるだけでは、視認性が落ちてしまいます。つまり

  • 「点滅の光が消えた瞬間」と「ピッケルを移動した瞬間」が重なると、どのブロックを選択したかが分からなくなる
  • 「点滅の光がついた瞬間」と「ブロックを選択している時」が重なると、どの種類のブロックかがぱっと見で分からなくなる

という自体になりかねません。特に下の「種類がぱっと見で分からなくなる」については致命的です。絶対ストレスが溜まります。絶対ダメです。
 つまり、常にどのブロックを選択している事が分かりつつ、常にブロックの種類の分かるようにしなければなりません。自分の技量と問題解決を両立させた結果、下図のような解決策を取りました。
ブロック点滅.gif
 私が取った解決策は「ブロックの周りのみ常時点滅させる」といった手段です。これがベストなのかは分かりませんが、少なくとも問題は解決出来ているのでよしとしました。
 やり方は下図をご覧ください。VectorToRadialValueノードを使う事で、UV上での(0.5,0.5)座標(ブロックの面の中心)から距離を計算する事が出来ます。そして、Smoothstepと組み合わせる事で円状になります。これについては「【UE4】マテリアルの「step」と「smoothstep」で遊ぼう!【★★】 - キンアジのブログ」を参考にしました。キンアジのブログ様ありがとうございます。
image.png
 TimeToEaseInValueマテリアル関数の中身は下図の通りです。

  1. TimeノードをSineノードを使って0~1の値に変更
  2. LinerSmoothstepマテリアル関数(これも上記のサイトを参考にしています)で直線化、InOutQuintマテリアル関数でイージング
  3. 最低値を設定

image.png

 標準のマテリアル関数にはもっと面白いのもあるので調べてみるのも良いかもしれません。「[UE4]マテリアル表現の幅が広がる!おもしろいノード&マテリアル関数集 1 - historia」などが参考になります。

次回へ続く

 ゲーム部分ではブロック・ピッケル・モンスターなどをやりました。技術部分はISM・ブループリントインターフェース・被破壊メッシュ・エミッシブなどをやりました。次回はモンスターのAIなどをする予定です。現状では何回に分かれるかは不明ですが、出来る限り継続的に上げていきます。

 出来る限り早く上げるといっておきながら、遅くなってしまいました。期待してくれている人には申し訳ありません。本当は早めに上げたいのですが、学校も始まり、卒業制作もあるので、個人製作のかける時間が減ってしまうので、更にペースが落ちると思います(確信)。忙しいぃ~。

参考サイト


  1. インスタンシングを一言で言うなら、「描画命令を最小限で同じモデルを大量描画する機能」です。詳細は「【Unity】大量のメッシュを軽く描画!GPUインスタンシングの基礎知識とシェーダの書き方まとめ - Hatena Blog」をみて下さい。 

  2. FVectorやFColorなどで変更したい時です。 

  3. 実際にエンジン部分の内部処理を見たわけではないので「絶対こうだ!」とは言えません。「恐らくそうなっているだろう」という仮定の話です。 

  4. セール中だと半額で買う事が可能です。大量のモデルとアニメーションがありかなりお買い得なので、かなりお勧めです。ただ、UE4はリアル指向のゲームがほとんどだと思うので、作りたいゲームに合うかどうかは微妙ですが...。なにせ、かなりデフォルメされているので...。 

  5. 本当は別のモデルにしたかったのですが、探していてもしっくり合うのが無く、探していた中で一番しっくりきたこのモデルにしました。といっても、性別は反転してるし、羽が生えてるし、頭身も他のモンスターと違うし、魔王の口調を違和感なくどう喋らせるかが困っています。魔王の娘の方が合っているとは言ってはいけません。本当にキャラクター設定どうしましょ?ツンデレ系が違和感はないと思いますが(※私の解釈)、しゃべらせる内容に困ります。頑張れ未来の自分! 

  6. このPDFについては以前から知っていたのですが、非常に興味深く面白いです。直接リンクをのせるのはまずいかなと思ったので、各自で調べてみて下さい。特にゲームAIに興味があるなら尚更です。※ゲームAI関係の資料はとても見ていて面白いですよね! 

  7. なぜ小さくしているのかというと、前述した通り、消さない限りバラバラに分解された状態で残るので、違和感が無いように削除したいからです。透明度を設けると変に重くなりかねないですし、小さくするだけでも割と違和感はありません。 

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
2
Help us understand the problem. What are the problem?