PavilionDV7の雑多なやつ

Qiitaから移行しました。UE4に関する記事から興味のあることまで色々書きます。

【UE5】敵キャラクターのスポナー(Spawner)を作る

はじめに

第19回ぷちコンに提出したゲームで実装した敵キャラクターのスポナーの実装を紹介する。

youtu.be

実装開始

基礎部分

実装するブループリントは「Actorクラス」を継承したブループリントとする。

コンポーネントはルートに「Box Collision Component」を設定したのみ。プロパティのBox ExtentのY軸の値を1000ほどにセットしておく。


Event Graphに次のように実装する。

Spawnイベントでは実際にアクターをスポーンした後、SpawnイベントをSet Timer by Eventノードで1秒後に再実行されるようにセットしている。これでSpawnイベントは1秒毎に呼び出されるので、アクターも1秒間隔でスポーンする。

決まった秒数を繰り返すだけならSet Timer by EventノードのLoopingフラグにチェックを入れるのが正攻法。ただし今回はスポーン間隔をプレイ中に変更したいのでこのような実装にした。

Spawn Transform Locationにて使用しているRandom Point in Bounding Boxは、文字通りCenterとHalf Sizeによって定義される直方体の範囲でランダムな位置を返す。今回はBox CollisionコンポーネントのBoundsを指定しているので、Box Collisionコンポーネントのサイズ内のランダムな位置を返すようになっている。

Spawn Actor from Classに指定しているブループリントクラスは一定間隔でスポーンすることを確認できるようなアクターであれば何でもいい。画像で指定したBP_Moverは次のように実装している。

スポーンレートをゲーム中に変化させる

スポナーブループリントのTickイベントに次のような処理を追加する。

毎フレーム、デルタ秒を加算することで経過時間(Elapsed Time)を求める事ができる。この経過時間をMap Range Clampedノードで0.0 ~ 1.0の数値に変換する。今回の実装では経過時間が10秒になったときMap Range Clampedノードは1.0を返すことになっている。

続いてSpawnイベントのSet Timer by Eventノードを次のように修正する。(Set Timer by Eventノードにあるコメントは消し忘れなので気にしないで)

Spawn Rate RatioとLerpノードの組み合わせでSet Timer by Eventに渡されるTimeの値は1.0 ~ 0.1に減少していく。Timeの値が減れば減るほどスポーン間隔が狭まるため多くのアクターがスポーンされることになる。

おまけ

スポーンされるアクターの確率を設定する

提出したゲームには4種類の敵キャラクターがいる。それぞれ移動速度や体力が異なるため、同じように出現してしまうと極端なゲームバランスになりゲームプレイが難しくなる可能性があったので、体力が多い敵は出現確率を低く、体力が少ない敵は出現確率を多くといった出現確率を設定できるように実装した。

このような偏った確率でランダムにアイテムを取り出すような仕組みを「重み付きランダム」「重み付きランダムサンプリング」「Weighted Random」と呼ぶ。

実装は次のようになる。

使い方として「文字列を格納した配列から重み付きランダムで文字を取り出す」例を示す。

各配列要素への重み(出現確率)は次の画像の赤線、緑線、青線に対応している。

試しに300のループを回して「Normal」「Fast」「Tank」の出現回数を調べてみた。

[BP_Spawner_C_1] Normal : 149 Fast : 97 Tank : 54

Normalが149個、Fastが97個、Tankが54個なので、それぞれを300で割ると

Normal : 149 / 300 = 0.49666...
Fast   :  97 / 300 = 0.32333...
Tank   :  54 / 300 = 0.18

となり、おおよそ出現確率テーブルで設定した値通りの結果が出ている。

【UE4/UE5】ブループリントでアイテムドロップ率とかに使う重み付き抽選を行う

【Unity】重み付きの確率抽選を行う方法 | ねこじゃらシティ

重み付き乱択を行う - Carpe Diem

【UE5】Box Collisionを使ってドラッグ&ドロップで範囲指定する

はじめに

第19回ぷちコンに提出したゲームではBox Collisionを利用してドラッグ&ドロップで拘束魔法の範囲を指定できる仕様を実装した。この記事ではその実装方法を紹介する。(ドラッグ&ドロップは以降「D&D」と書く)

拘束魔法の範囲指定の様子は次の動画で。

youtu.be

実装開始

まずは基礎部分

実装するブループリントは「Pawnクラス」を継承したブループリントとする。

コンポーネントはルートに「Box Collision Component」を設定したのみ。プロパティはいじらない。


D&Dの開始・終了地点の取得には「Get Hit Result Under Cursor by Channel」を使用する。その関数からヒット位置のみを取り出す関数を作成する。この関数は純粋関数(Pure)にしている。

続いてカーソルを常に表示するように設定する。

左マウスボタン(左クリック)イベントを追加して、次のように実装する。カスタムイベントを呼び出しているが、画面外に該当のカスタムイベントを定義している。

続いてTickイベントを次のように実装する。呼び出すBox Collision ComponentのSet Box ExtentでY軸とZ軸は定数であることに注目。実は今回紹介する実装方法はBox Collision ComponentのExtentのX軸をドラッグ開始地点から終了地点に合わせて伸び縮みさせるだけのものである。

とりあえず実装はここまで。


実装したブループリントをレベルに配置し「Auto Possess Player」を「Player 0」に設定しプレイする。D&Dの開始地点から終了地点までBox Collision Componentが伸びることが確認できるはず。

ただしBox CollisionのExtentのX軸を伸び縮みさせているだけなので、斜めにD&Dしてしまうと奇妙な結果になってしまう。

向きに対応する

斜めにドラッグ&ドロップしてしまうと奇妙な結果になるのは、アクターの回転(向き)を考慮していないため。これまではワールド座標のX軸に向かってD&Dの開始地点から終了地点までの距離を伸ばす実装になっていた。

これを「ローカル座標のX軸方向」に伸ばすように実装する必要がある。これはかなり簡単に実装出来る。

TickイベントのSet Actor Locationノードに続いて次のようなノードを繋げる。


プレイして今度は自由な方向にD&Dしてみる。恐らく求めていたBox Collisionの範囲指定の実装ができたと思う。

大きさに制限をかける

ぷちコンに提出したゲームでは伸ばせる距離に制限をかけている。D&Dの距離をそのまま範囲指定できてしまうとプレイヤーが非常に強くなってしまうのでそのような仕様にした。

伸ばす距離に制限をかけるにはTickイベントのSet Box ExtentノードとSet Actor Locationノードを次のように修正する。赤枠部分が修正箇所。


プレイすると1000cmの長さまでBox Collision Componentが伸ばせることが確認できる。

おまけ

ここまででD&DでBox Collisionを伸ばしたり、向きを変えたりすることができた。主要なメカニズムはこれで完了したので、以降はちょっとしたおまけの内容を書いていく。

チャージに対応する

提出したゲームでは左クリックが押された瞬間から左クリックを離した瞬間までの時間を計測し、その時間の長さに応じて拘束魔法の効果時間を決定している。この「チャージ」を実装していく。

TickイベントのSet Actor Rotationノードの後ろに次のような処理を繋げる。

続いてLeft Mouse ButtonイベントのReleasedピンのClock Tick Gateノードの後ろに経過時間をリセットする処理を加える。

これだけではきちんと動作できているかわからないのでTickイベントのどこかに次のようなデバッグ表示を追加する。


まずは左クリックが押された瞬間からの経過時間を計算するため、デルタ秒を毎フレーム加算する。これによって1秒経過後にはおおよそ1.0、2秒経過後にはおおよそ2.0という数値を取得出来る。この数値をMap Range Clampedノードを使って0.0 ~ 1.0の割合に変換する。画像ではIn Range Bに3.0を指定しているので3秒間かけて0.0~1.0までの数値を出力している。

直接チャージ時間を扱うのではなく割合で扱えるとで色々と便利。後述する「チャージ時間に応じて色を変化する」であったり「チャージ率が最大になったときジャキーンと音を鳴らす」みたいな処理を簡単に追加出来る。チャージ時間を直接扱ってしまうと「やっぱりチャージ時間を5秒にしよう」としたとき色々と計算のし直しが発生してしまう。

色をつける

チャージした時間に応じてDraw Debug Boxの色を変化させる。

チャージ時間の割合はすでに求めているので、実装はかなり簡単。TickイベントのSequenceノードのThen 1ピンの先にあるDraw Debug BoxノードにLerp(Linear Color)ノードを繋げるだけ。見やすさのためにDraw Debug BoxノードのThicknessを20.0ほどにしておく。


今回は色の変化にLerp(Linear Color)を利用したが、Curveアセット(Curve Linear Color)を利用するのもいい。色の補間具合によっては気に入らない色が出てきてしまうこともあるので、そういった場合はCurveアセットで細かに色の変化を定義するといい感じ。

[UE4] 色々な所で使えるCurveアセットの使い方|株式会社ヒストリア

【UE5】パッケージングサイズを小さくしたかった

はじめに

ぷちコンの制作をしようと思ったら、なぜかパッケージサイズ削減の旅に出てしまった。

大した成果は得られなかったがパッケージングの設定や無効にしたプラグイン一覧を載せるので参考になれば幸い。

使用エンジン

UE5.1.1

まずはこちらのページを見よう

「プロジェクト - パッケージ化(Packaging)」を参照。

qiita.com

削減策の成果

「New Level...」で選択できる「Basic」をGames Default Mapにしてパッケージングしてみた。


パッケージング設定 + 不要なプラグインを無効

169MB



デフォルト設定

216MB


パッケージング設定

UE5やUE4.27(?)あたりからはお世話になるパッケージサイズ削減オプションに軒並みチェックが入っていたりするので、変更した箇所のみを載せる。

Project/Build Configulation

Development --> Shipping

プラグイン設定

画像で載せるのは大変なのでテキスト形式で載せる。

これまでのプロジェクトであまり使わなかったものや、ネットワーク関係、モバイル関係は軒並み無効にしている。とりあえずこの内容を真似してプラグインを無効化し、なにかパッケージに失敗したり起動に失敗したり、使いたい項目・機能が無かったら該当するプラグインを有効化する感じいいと思う。

{
            "Name": "Paper2D",
            "Enabled": false
        },
        {
            "Name": "OnlineSubsystemGooglePlay",
            "Enabled": false,
            "SupportedTargetPlatforms": [
                "Android"
            ]
        },
        {
            "Name": "AndroidPermission",
            "Enabled": false
        },
        {
            "Name": "AndroidFileServer",
            "Enabled": false
        },
        {
            "Name": "GooglePAD",
            "Enabled": false
        },
        {
            "Name": "AppleImageUtils",
            "Enabled": false
        },
        {
            "Name": "AppleMoviePlayer",
            "Enabled": false
        },
        {
            "Name": "AvfMedia",
            "Enabled": false
        },
        {
            "Name": "WebMMoviePlayer",
            "Enabled": false
        },
        {
            "Name": "AndroidMoviePlayer",
            "Enabled": false
        },
        {
            "Name": "AndroidMedia",
            "Enabled": false
        },
        {
            "Name": "LocationServicesBPLibrary",
            "Enabled": false
        },
        {
            "Name": "MobilePatchingUtils",
            "Enabled": false
        },
        {
            "Name": "GoogleCloudMessaging",
            "Enabled": false
        },
        {
            "Name": "OnlineSubsystemIOS",
            "Enabled": false,
            "SupportedTargetPlatforms": [
                "IOS",
                "TVOS"
            ]
        },
        {
            "Name": "CLionSourceCodeAccess",
            "Enabled": false
        },
        {
            "Name": "KDevelopSourceCodeAccess",
            "Enabled": false
        },
        {
            "Name": "CodeLiteSourceCodeAccess",
            "Enabled": false
        },
        {
            "Name": "NullSourceCodeAccess",
            "Enabled": false
        },
        {
            "Name": "RiderSourceCodeAccess",
            "Enabled": false
        },
        {
            "Name": "VisualStudioCodeSourceCodeAccess",
            "Enabled": false
        },
        {
            "Name": "XCodeSourceCodeAccess",
            "Enabled": false,
            "SupportedTargetPlatforms": [
                "Mac"
            ]
        },
        {
            "Name": "GitSourceControl",
            "Enabled": false
        },
        {
            "Name": "PerforceSourceControl",
            "Enabled": false
        },
        {
            "Name": "PlasticSourceControl",
            "Enabled": false
        },
        {
            "Name": "SubversionSourceControl",
            "Enabled": false
        },
        {
            "Name": "MacGraphicsSwitching",
            "Enabled": false
        },
        {
            "Name": "MediaCompositing",
            "Enabled": false
        },
        {
            "Name": "MediaPlate",
            "Enabled": false
        },
        {
            "Name": "ImgMedia",
            "Enabled": false
        },
        {
            "Name": "WindowsMoviePlayer",
            "Enabled": false
        },
        {
            "Name": "WmfMedia",
            "Enabled": false
        },
        {
            "Name": "AndroidDeviceProfileSelector",
            "Enabled": false
        },
        {
            "Name": "ExampleDeviceProfileSelector",
            "Enabled": false
        },
        {
            "Name": "IOSDeviceProfileSelector",
            "Enabled": false
        },
        {
            "Name": "LinuxDeviceProfileSelector",
            "Enabled": false
        },
        {
            "Name": "TcpMessaging",
            "Enabled": false
        },
        {
            "Name": "UdpMessaging",
            "Enabled": false
        },
        {
            "Name": "MobileLauncherProfileWizard",
            "Enabled": false
        },
        {
            "Name": "OpenImageDenoise",
            "Enabled": false
        },
        {
            "Name": "AlembicImporter",
            "Enabled": false
        },
        {
            "Name": "DatasmithContent",
            "Enabled": false
        },
        {
            "Name": "SpeedTreeImporter",
            "Enabled": false
        },
        {
            "Name": "ChaosClothEditor",
            "Enabled": false
        },
        {
            "Name": "ChaosCloth",
            "Enabled": false
        },
        {
            "Name": "ChunkDownloader",
            "Enabled": false
        },
        {
            "Name": "FacialAnimation",
            "Enabled": false
        },
        {
            "Name": "FastBuildController",
            "Enabled": false
        },
        {
            "Name": "Iris",
            "Enabled": false
        },
        {
            "Name": "LauncherChunkInstaller",
            "Enabled": false
        },
        {
            "Name": "OnlineSubsystemNull",
            "Enabled": false
        }

載せたテキストは.uprojectをメモ帳で開いて「Plugins」以下にコピペすると、そのプロジェクトでは無効化される。

追加で設定したほうがいい項目

サイズ削減に貢献しているか微妙だが、設定しておいた方がいい項目を挙げておく。

List of maps to include in a packaged build

ゲーム内で使用する.umapを指定する。

Forumでの報告によるとUE5.1から「Project Settings -> Maps & Modes -> Game Default Map」に指定された.umapのみがパッケージングの対象となるらしい。ステージごとに.umapを用意しているプロジェクトでは各レベルへの遷移に失敗してしまうはずなので、「List of maps to include in a packaged build」にゲームで使用される.umapすべて指定し、パッケージ対象のする必要がある。

Full Rebuild

チェックを入れる。

おわりに

本当は100MBあたりまで削れたら最高だけど、これ以上は厳しい気がする。

おまけ

Directories to never cookにエンジンコンテンツを指定する

試しに「Directories to never cook(クック対象に含めないディレクトリ)」にEngineフォルダを指定したけど120MBまで削るに留まった。デフォルトでついているBasic Shape(球体や立方体等)、マテリアル、マテリアル関数等々が使えなくなるのでゲームが起動できなくなる可能性が非常に高い。払う代償に対して得られるものはあまりにも小さいのでおすすめしない。

パッケージング後の各コンテンツの容量を調査

パッケージの設定で「Use Pak File」「Use Io Store」のチェックを外せば、各アセットは.pakにまとめられず.uasset形式でパッケージングされる。そこでどのファイルがどれくらい容量を取っているかWizTreeを使って見てみた。

赤枠がプロジェクト独自のコンテンツが納められるフォルダ。このあたりは削れる箇所はないので甘んじて受け入れるしかない。 最も容量を取っているものはBinariesフォルダにある実行ファイル。これだけで100MBの容量を取っている。

青枠がエンジンコンテンツが納められているフォルダ。Contentフォルダ内で最も容量を取っているものはEngineMaterial。自分が作るゲームは必ずどこかでデフォルトのマテリアルやテクスチャを使っているので、これを削るのは難しい。 それ以外のフォルダはほとんど容量を取っておらず、消したとしても得られる効果は低そう。

EngineフォルダはContentフォルダ以外にもPluginフォルダがあり、有効化されているプラグインで利用される各リソースが納められている。 「Interchange」というプラグインが多く容量を取っているが、これは恐らくアセットのインポートやエクスポートといった類の処理で利用されているものなので、無効化すると作業に非常に大きな影響を及ぼしかねないので無効化は難しい。 エンジンコンテンツと同じようにInterchange以外は小さな容量なので、無効化して得られる効果は低い。

【UE5】倉庫番を作ろう! ゲームループ・ブラッシュアップ編

はじめに

Unreal Engine (UE) Advent Calendar 2022 その3 16日目の記事です。

最後のパートです。

プロジェクトセットアップ・ゲームプレイ編
メインメニュー・ステージセレクト編
ゲームループ・ブラッシュアップ編(ここ)

ゲームループ編

ここからはメインメニューレベルからゲームプレイレベルまで一方通行だったものをきちんとループできるようにします。更に次のステージに遷移できるようにもしてみます。

ステップ19:ゲームクリアからステージセレクトへ戻る

WBP_GameClearを開いて、Button_StageSelectのOn Clickedイベントを作成し次のように処理を組みます。

そのままP_MainMenuに戻るとメインメニュー画面が表示されてしまうので、Opsitionsを使って「直接ステージセレクトメニューを開く」ことを示します。

次にGM_MainMenuを開きます。Begin PlayのShow Mouse Cursorノードに次のように処理を追加します。

動作確認します。ステージクリア画面を表示して「ステージセレクト」ボタンを押せばステージセレクト画面が表示されればOKです。

しかしステージクリアからステージセレクトに戻ったとき、更に「戻る」ボタンを押してメインメニューに戻ろうとしても何も反応しません。これはGM_MainMenuのOnClicked_ReturnButtonイベントでWBP_MainMenu_RefをValidated Getしたときに「Is Not Valid」に処理が流れてしまっているためです。

この不具合を修正します。GM_MainMenuを開いてOnClicked_ReturnButtonに次のように処理を追加します。

再度、動作確認をして不具合が解消されていればOKです。

ステップ20:次のステージに遷移する

ステージクリア画面の「次へ」ボタンを押すと次のステージに遷移するようにします。

その前にOpen LevelノードのOptionsに指定する文字列やParse OptionのKeyに指定する文字列を定数化しておきます。

定数化した文字列を使っている部分を先程定義したマクロ版に置き換えておきましょう。(画像省略)

WBP_GameClearを開き、Button_NextのOn Clickedイベントを作成します。

Button_NextのOn ClickedイベントにつながるOpen Levelノードにブレークポイントを設置すれば、ステージ名の文字列がどのように変化していくかを覗くことができます。

今回のプロジェクトでのステージ名のようなレベルやデータアセットが「連続した名前」になっていると、「次へ進む」といった処理を作りたいときにとても楽に作ることができます。

動作確認をして「次へ」ボタンを押したとき正しく次のステージが読み込まれてプレイできることが出来ればOKです。


用意したステージを超えて「次へ」を押した場合、エラーメッセージが出力されます。このような用意したステージ数を超えてしまった場合の処理を組んでいきます。

ここでは最後のステージをクリアしたときに「次へ」ボタンを押したときはステージセレクトに戻るようにします。

BP_StageBuilderを開きGet Data Table RowのRow Not Foundピン以降の処理を次のように組みます。

再度、動作確認をします。最後のステージから「次へ」ボタンを押したときにステージセレクトに遷移したらOKです。

ブラッシュアップ編

前回のステップでゲームとして遊ぶための要素は全て追加しました。ここからはあまりゲームの要素とは関係がない部分を実装していきます。

ステップ21:クリアしたステージにCompletedマークを付ける

現状ではステージをクリアしたかが判別できないので、クリアしたステージには「COMPLETED」というマークを付けます。

ステージのクリア状況はゲームを終了しても保持しておく必要があるのでSave Gameクラスを利用することにします。

ゲームを保存して読み込む | Unreal Engine ドキュメント

ステージクリア状況を記録するためのブループリントを作成します。

  • 親クラス:Save Game
  • 名前:SG_StageCleared

SG_StageClearedを開いて変数を1つ作成します。

まずStageClearedの型を「String」にしておき、型名の右にあるコンテナタイプを「Map」に変更します。次に一番右にある型を「Boolean」型に変更します。


次にWBP_StageButtonを開いて「COMPLETED」というラベルを追加します。パーツを次のように配置します。
ここで注意点ですがボタンパーツは子パーツを1つしか受け入れないのでCanvas Panelを配置できません。TextBlock_Stageを右クリックして「Wrap With...」から「Canvas Panel」を選択すればTextBlock_Stageの親パーツとしてCanvas Panelが自動的に配置されます。

  • WBP_StageButton
    • [CanvasPanel]
      • [TextBlock_Stage]
        • Anchor : 中心
        • Position X : 0
        • Position Y : 0
        • Alignment : X & Y=0.5
        • Size to Content : チェック入れる
      • [TextBlock_Cleared]
        • Is Variable:チェック入れる
        • Anchor : 中心
        • Position X : 40
        • Position Y : -20
        • Alignment : X & Y=0.5
        • Size to Content : CLEARED
        • Text : チェック入れる
        • Color and Opacity (Hex Linear):FF8000FF
        • Font Size:19
        • Visibility:Hidden
        • Transform Angle:20


色々パーツを配置しましたがCLEAREDラベルがボタンの中心に小さく表示されており、イメージする新しいボタンのレイアウトとはかけ離れてしまっていると思います。ウィジェットブループリントではルートに配置されたパーツは自動的に想定するスクリーンサイズにフィットするようにリサイズされてしまいます。

そこでエディタの右上にある「Fill Screen」と書かれているプルダウンメニューから「Desired」を選択するとボタンが本来表示されるサイズに変化します。


同じくWBP_StageButtonにカスタムイベントを1つ作成します。


次にセーブ時に使う文字列と数値を定数化しておきます。BPML_Constantsを開いて次のようにマクロを組みます。

  • 名前:SaveSlotNameIndex
  • Compact Node Title:SaveSlotData

次にWBP_StageSelectを開き、Constructイベントに続いて次のように処理を組みます。


ここまででステージクリアデータを読み込むことはできたので次は実際にステージをクリアしたときにセーブデータに書き込みます。

GM_GamePlayを開いてクリア画面表示後にDisable Inputする処理の続きに次のような処理を組みます。

動作確認をします。適当なステージをクリアしたあとにステージセレクトに戻ると「CLEARED」のラベルが貼られている。ゲームを終了&再開してもCLEAREDのラベルが貼られたままであればOKです。


このセーブデータは「.uproject」があるフォルダの「Saved / SaveGames」にあります。

もしセーブデータを削除したいときはSaveGamesフォルダ内にある「.sav」ファイルを直接削除(ゴミ箱行き)してください。エディタが起動中でも削除できます。

ステップ22:ステージセレクトのページ数を増やす

ゲームとしては結構しっかりとしたものになったと思うので、あと4つほどステージを増やしてみます。

WBP_StageSelectを開き、Scroll Boxの子パーツである「Size Box」とその子パーツを全てScroll Box内にコピペします。
次のようにプロパティを変更します。

  • WBP_StageSelect
    • [CanvasPanel]
      • [Scroll Box]
        • [Size Box]
          • 省略
        • [Size Box]
          • [Vertical Box]
            • [Text]
              • Text:STAGE 2
            • [Uniform Grid Panel]
              • [WBP_StageButton]
                • StageName:2-1
              • [WBP_StageButton]
                • StageName:2-2
              • [WBP_StageButton]
                • StageName:2-3
              • [WBP_StageButton]
                • StageName:2-4

更に新しく左右にスクロールするためのボタンパーツを配置します。

  • WBP_StageSelect
    • [CanvasPanel]
      • [Button_Left]
        • Anchor : 中心
        • Position X : -600
        • Alignment : X & Y=0.5
        • Size to Content : チェック入れる
        • [Text]
          • Text:<<
      • [Button_Right]
        • Anchor : 中心
        • Position X : 600
        • Alignment : X & Y=0.5
        • Size to Content : チェック入れる
        • [Text]
          • Text:>>

後々使うのでScroll Boxの名前を変更しIs Variableにチェックを入れます。

  • 名前:ScrollBox_StageList
  • Is Variable:チェック入れる


同じくWBP_StageSelectに新しく変数を1つ追加します。

  • 名前:TargetScrollOffset
  • 型:Float

Button_RightとButton_LeftのOn Clickedイベントを作成します。

次にTickイベントを作成します。

新しく追加した2-1、2-2、2-3、2-4は新しいステージとしてデザインしましょう。ステージがデザインできたあとは忘れずに、DT_StageAssetPathにデータを追加します。

動作確認をして、ステージセレクトで左右のボタンを押すてスクロールすればOKです。クリアしたステージはCLEAREDのラベルが正しく貼られることも更にOKです。

ステップ23:メインメニューとステージセレクトでキー入力を無効化

実はメインメニューやステージセレクトでWASDキーを押すとレベルを動き回れてしまいます。何か不具合が起きてはまずいのでメインメニューとステージセレクトではキーボード操作を無効にします。

WBP_MainMenuを開いてConstructイベントに次のように処理を組みます。

次にWBP_StageSelectを開いてConstructイベントに同じ処理を組みます。

動作確認をしてメインメニューとステージセレクトでWASDキーに何も反応がなければOKです。


しかしSet Input Modeの設定はレベルを跨いでも引き継がれます。ステージセレクトからゲームプレイに移行するとキーボード操作が効きません。ですのでキーボード操作を有効にしたい場合はきちんと設定し直す必要があります。

BP_PlayerControllerを開いてBegin Playイベントの続きに次のような処理を追加します。

再度、動作確認をします。メインメニュー&ステージセレクトはキーボード操作ができず、ゲームプレイではキーボード操作を受け付ければOKです。

おわりに

見た目はとてもシンプルですがゲームとして立派に遊べるものには出来たと思います。

ここからプレイヤーキャラクターをきちんとしたアニメーションがついているものに変更すると、更にゲーム感がアップすると思います(Animation Blueprintとの連携についての知識が必要です)。ブロックも単色ですがきちんとしたモデルを用意したり、テクスチャを貼ってリアルに見せるのも面白いでしょう。音を付けてみるのも良いですね。ブラッシュアップ編ではゲームのクリア状況のみをセーブしていましたが、例えば「クリアするまでの歩数」や「押せるブロックを押した回数」等を記録すると競技感がアップするかもしれません。

この記事がお役に立てば幸いです。

参考資料

記事中に貼っていたリンク先をまとめました。

qiita.com

【UE5】Enhanced Input | キシロラボ

Unreal Engine の Enhanced Input | Unreal Engine 5.1 ドキュメント

20年オヤジのUnreal Engine 4 TIPS - SEGA TECH Blog

[UE4] 動きに緩急をつけるEaseノードの紹介|株式会社ヒストリア

Paper 2D | Unreal Engine 5.1 ドキュメント

アセットの参照 | Unreal Engine ドキュメント

第007回UE4のアセットの参照方法について、そのロードの違い | CC2の楽屋裏

ハード参照とソフト参照 - おかわりはくまいのアンリアルなメモ

BPの参照連鎖を断つ手法 | 株式会社ヘキサドライブ | HEXADRIVE | ゲーム制作を中心としたコンテンツクリエイト会社


上記以外に参考にした記事&資料です。

Unreal Engine のゲームプレイ フレームワーク | Unreal Engine 5.1 ドキュメント

【UE4】DataTableの使い方-その① ~基本編~【★~★★】 | キンアジのブログ

【UE5】倉庫番を作ろう! メインメニュー・ステージセレクト編

はじめに

Unreal Engine (UE) Advent Calendar 2022 その3 16日目の記事です。

パート2です。

プロジェクトセットアップ・ゲームプレイ編
メインメニュー・ステージセレクト編(ここ)
ゲームループ・ブラッシュアップ編

メインメニュー・ステージセレクト編

ここからはタイトル画面とステージセレクト画面を作っていきます。

ステップ15:メインメニューを表示

新しいレベルを作ります。

  • テンプレート:Basic
  • 名前:P_MainMenu

次にメインメニュー用ウィジェットブループリントを作成します。

  • 親クラス:User Widget
  • 名前:WBP_MainMenu

WBP_MainMenuを開いて次のようにパーツを配置します。

  • WBP_MainMenu
    • [CanvasPanel]
      • [Border]
        • Anchor:全面
        • Offset Right : 0
        • Offset Bottom : 0
        • Brush Color (Hex Linear): 00000080
      • [Text]
        • Anchor : 中心
        • Position Y : -100
        • Alignment : X & Y=0.5
        • Size to Content : チェック入れる
        • Text : SOKOBAN
        • Font Size : 168
      • [Button_StageSelect]
        • Anchors : 中心
        • Position Y : 200
        • Alignment : X & Y=0.5
        • Size to Content : チェックを入れる
        • [Text]
          • Text : ステージセレクト
          • Font Size : 72
      • [Button_Quit]
        • Anchors : 中心
        • Position Y : 400
        • Alignment : X & Y=0.5
        • Size to Content : チェックを入れる
        • [Text]
          • Text : やめる
          • Font Size : 72

同じくWBP_MainMenuでButton_QuitのOn Clikedイベントを作成します。


次はメインメニュー用ゲームモードブループリントを作成します。

  • 親クラス:Game Mode Base
  • 名前:GM_MainMenu

GM_MainMenuを開き、Begin Playから次のように処理を組みます。

忘れずにP_GamePlayでGM_MainMenuを使うように設定します。

動作確認をし「やめる」ボタンを押したときエディタのプレイ状態が終了すればOKです。

ステップ16:ステージセレクトメニューを表示

ステージセレクトウィジェットブループリントを作成する前にステージセレクトウィジェットで使うButtonパーツを自作します。

  • 親クラス:User Widget
  • 名前:WBP_StageButton

WBP_StageButtonを開いて次のようにパーツを配置します。

  • WBP_StageButton
    • [Button_Stage]
      • [TextBlock_Stage]
        • Is Variable:チェック入れる
        • Font Size:48

Is Variableはパーツ選択時にDetailsパネルに表示される、パーツ名の右隣にあります。

同じくWBP_StageButtonに変数を1つ追加します。

  • 名前:StageName
  • 型:String
  • Instance Editable:チェック入れる

次にPre Constructイベントに続けて処理を組みます。


次はステージセレクトウィジェットブループリントを作ります。

  • 親クラス:User Widget
  • 名前:WBP_StageSelect

次のようにパーツを配置します。(パーツ数が一番多いのでがんばってください)

  • WBP_StageSelect
    • [CanvasPanel]
      • [Border]
        • Anchor : 全面
        • Offset Right : 0
        • Offset Bottom : 0
        • Brush Color(Hex Linear): 00000080
      • [Text]
        • Anchor : 中心
        • Position Y : -400
        • Alignment : X&Y=0.5
        • Size to Content : チェック入れる
        • Text : STAGE SELECT
        • Font Size : 96
      • [Scroll Box]
        • Ancrhor : 中心
        • Size X : 1000
        • Size Y : 400
        • Alignment : X&Y=0.5
        • Scroll Orientation : Horizontal
        • [Size Box]
          • Width Override : 1000
          • [Vertical Box]
            • [Text]
              • Horizontal Alignment : 中心
              • Text : STAGE 1
              • Font Size : 84
            • [Uniform Grid Panel]
              • Slot Padding : 10
              • [WBP_StageButton]
                • Is Variable:チェック外す
                • Horizontal Alignment : 全面
                • Row : 0
                • Column : 0
                • Stage Name : 1-1
              • [WBP_StageButton]
                • Is Variable:チェック外す
                • Horizontal Alignment : 全面
                • Row : 0
                • Column : 1
                • Stage Name : 1-2
              • [WBP_StageButton]
                • Is Variable:チェック外す
                • Horizontal Alignment : 全面
                • Row : 1
                • Column : 0
                • Stage Name : 1-3
              • [WBP_StageButton]
                • Is Variable:チェック外す
                • Horizontal Alignment : 全面
                • Row : 1
                • Column : 1
                • Stage Name : 1-4


次はGM_MainMenuを開きます。変数を1つ追加します。

  • 名前:WBP_MainMenu_Ref
  • 型:WBP_MainMenu Object Reference

WBP_MainMenuの生成部分に処理を追加します。

次にShow Mouse Cursorにチェックを入れている箇所から続いて処理を組みます。

動作確認をします。メインメニューUIの「ステージセレクト」ボタンを押すとステージセレクトUIが表示されればOKです。現状は一方通行で構いません。

ステップ17:各ステージのボタンが押されたときにそのステージを読み込む

ここからメインメニューレベルとゲームプレイレベルが繋がります。が、しかし「選択したステージをどうやって別レベルにいるBP_StageBuilderに伝えるか」という問題があります。これにはいくつかの方法がありますが代表的なものはGame Instanceを使う方法です。Game Instanceはゲーム全体に1つだけ存在するオブジェクトであり、レベルを跨いでも情報を保持することができます。ステージセレクトで選択したステージ名をGame Instanceに保存し、ゲームプレイレベルでGame Instanceからステージ名を取得すれば対象のステージを生成することができます。

UE4 GameInstanceでグローバルに値を扱う - UE4初心者が頑張ってるブログ

ですが、このプロジェクトでは読み込みたいステージ名を文字列でゲームプレイレベルに渡せればいいので、Game Instanceを使う方法は手順が少し増えてしまい手間です。そこでOpen Levelノードの「Options」という引数を使うことにします。

2つのLevel間でデータをやり取りする3つの方法 | それとこれとあれかどれかと


WBP_StageButtonを開き、Button_StageのOn Clickedイベントを作成します。

次にBP_StageBuilderを開きます。Begin PlayのGet Data Table Rowノードに次のような処理を追加します。

動作確認をしてステージセレクトで選択したステージがゲームプレイレベルで正しく読み込まれていればOKです。


Open LevelノードのOptionsには「項目名1=データ1?項目名2=データ2」と「?」区切りで文字列を渡すことができます。Optionsに渡した文字列は読み込んだレベルに設定されたGame Modeが「Options String」という変数に保存しています。Parse Optionノードの「Key」に取り出したいデータ項目名を入力することで該当するデータを文字列で取り出せます。

便利でお手軽ですが文字列のみのやり取りに限りますので、今回のような「名前」「数値を文字列に直したもの」であれば問題ありませんが「構造体」といった文字列で表すことが難しいデータについてはGame Instanceを使ったほうがいいでしょう。

ステップ18:メインメニューとステージセレクトメニューを行き来させる

WBP_StageSelectを開き、次のようにパーツを配置します。(追加部分だけ載せます)

  • WBP_StageSelect
    • [CanvasPanel]
      • [Button_Return]
        • Position X:50
        • Position Y:50
        • Size to Content:チェック入れる -[Text]
          • Text:戻る
          • Font Size:36


次はGM_MainMenuを開いてBegin Play以降の処理を組み直します。

続いて変数を1つ追加します。

  • 名前:WBP_StageSelect_Ref
  • 型:WBP_StageSelect Object Reference

更にOnClicked_StageSelectButtonイベント以降と新しいカスタムイベントを追加して処理を組みます。

Is ValidとIs Not Validがセットになったノードは次のリンク先で紹介されている機能です。

UE5/UE4 変数の「Get」時に「IsValid」を同時に行う「検証済みゲット」(Convert to Validated Get) 凛(kagring)のUE5/UE4とゲーム制作と雑記ブログ

動作確認をしてステージセレクトの戻るボタンを押すとメインメニューに戻り、そこからステージセレクトへ進むことができればOKです。

次のパートへ...

ゲームループ・ブラッシュアップ編

【UE5】倉庫番を作ろう! プロジェクトセットアップ・ゲームプレイ編

はじめに

Unreal Engine (UE) Advent Calendar 2022 その3 16日目の記事です。

UE5を使って倉庫番を作ります。

全部で3パートあります。

プロジェクトセットアップ・ゲームプレイ編(ここ)
メインメニュー・ステージセレクト編
ゲームループ・ブラッシュアップ編

このチュートリアルではUnreal Engineの操作から教えるものではなく、Unreal Engineを使ってゲームを作るチュートリアルです。Unreal Engineの操作(例:プロジェクトの作成、レベルの作成、ブループリントの作成等)は把握していることを前提としています。

今回のプロジェクトで必要な操作を大まかに挙げてみます。

  • プロジェクトの作成ができる
  • 新規レベルを作成できる
  • 任意のクラスを継承してブループリントを作成できる
    • Actorを継承したブループリント、Game Modeを継承したブループリント等
  • ブループリントのDetailsパネルからプロパティを変更できる
    • Transformの変更、Collision Presetの変更等
  • 構造体アセットとDataAssetアセットを作成して任意のデータ形式を作成できる
  • ウィジェットブループリントでUIのデザインができる

挙げた内容に不安がある方は公式オンラインラーニングにある

dev.epicgames.com

dev.epicgames.com

この2つを受講すれば問題ないと思います。

注意点

使用するUEバージョン:UE5.1.0
エディタの言語:英語

UE5.1.0以外での制作についてはサポートしません。

エディタの言語は英語です。よってスクショや文章でも英語設定に準拠した名前・項目名となります。

完成図

このチュートリアルが無事に完了できれば次のビデオのような倉庫番ゲームができます。

GitHubでプロジェクトのダウンロード

お試しとしてGitHubにプロジェクトをアップしてみました。各ステップの最後にはコミットへのリンクを貼っておくのでエラーが出て動かない、手順通りにやったはずなのに動かないという場合はそのステップ時点でのプロジェクトをダウンロードして比較すれば解決しやすいはずです。

GitHubからのプロジェクトのダウンロードは緑色の「Code」というボタンを押して、表示されたメニューの一番下にある「Download ZIP」をクリックするとダウンロードできます。

GitHub - bullyleo/Tut_SOKOBAN

プロジェクトセットアップ編

ステップ1:プロジェクトを作成

Epic Games Launcherからプロジェクトを新規作成します。プロジェクト名はおまかせですが、ここでは「Tut_SOKOBAN」としました。

プロジェクトが作成できたら新規レベルを作成します。使用テンプレートは「Blank」とします。

作成したレベルを「Content -> [作成したプロジェクト名] -> Maps」フォルダに保存します。名前は「P_GamePlay」とします。

Project Settings -> Maps & Modes -> Editor Startup Mapに先程保存したP_GamePlayをセットします。


筆者のPCは8年前に購入したおじいちゃんスペックなので低スペック向け設定をしています。
設定変更の常連は次の記事にある「Lumenと仮想シャドウマップを無効化する」「スケーラビリティの品質を下げる」の2つです。

qiita.com

上記の設定に加えて、新規レベル作成時に配置されている「Directional Light」「Sky Light」のMobilityを「Stationaly」にしています。

  • [Directional Light] & [Sky Light]
    • Mobility : Stationaly

この設定によって高スペックPCを持つ方とはスクショやビデオでの見え方が大きく異なる可能性がありますが、動作については変化はないので心配はありません。

ゲームプレイ編

このチュートリアルでは実装する順番を次のようにします。

1.ゲームプレイ(ステージが読み込まれて、ステージクリア画面が表示されるまで)
2.メインメニュー&ステージセレクト
3.ゲームループ(メインメニュー -> ステージセレクト -> ゲームプレイ -> ステージセレクトまたはメインメニューのループ)
4.ブラッシュアップ

まずはゲームの核となる要素(移動、押す・引くといった動作等)から手をつけます。そこから1ステージを始まりから終わりまできちんと遊べるようにした後、ステージを量産して正しく動作するかチェックします。それが終わればメインメニュー画面とステージセレクト画面に着手しますが、この時点では独立したシーンまたは一方通行でシーン遷移するだけです。メインメニュー -> ステージセレクト -> ゲームプレイと遷移ができれば、ゲームプレイからメインメニューもしくはステージセレクトへ戻れるようにします。ここが完成するとゲームがきちんとループするようになります。
ブラッシュアップはUIの項目を増やしたり、クリアしたステージに「CLEARED」といったラベルを貼ったりします。

ここから多くのアセットを作りますが、アセットの保存場所については特に指定はないのでわかりやすいようにフォルダ分けしてください。GitHubにアップしているプロジェクトを参考にしてもOKです。

ステップ2:Enhanced Inputで入力処理

Input Actionアセットを作成します。

  • 名前 : IA_Move

IA_Moveを次のようにセットアップします。

TriggerにPressedを設定して押下時に1度だけイベントが通知されるようにします。ボタンの長押しはサポートしません。


続いてInput Mapping Contextアセットを作成します。

  • 名前 : IMC_Move

IMC_Moveをセットアップします。


プレイヤーブループリントを作成します。

  • 親クラス : Pawn
  • 名前 : BP_Player

BP_Playerをセットアップします。

セットアップが完了すれば次のようになります。


続いてプレイヤーコントローラーブループリントを作成します。

  • 親クラス : Player Controller
  • 名前 : BP_PlayerController

BP_PlayerControllerを開き、次のように処理を組みます。


入力処理がうまく動作しているか確認していきます。
まずレベルにBP_PlayerとCamera Actorを設置し、次のように各プロパティをセットします。

続いてゲームプレイ用ゲームモードブループリントを作成します。

  • 親クラス : Game Mode(「Game Mode Base」では無い)
  • 名前 : GM_GamePlay
  • Class Defaults
    • Player Controller Class=BP_PlayerController
    • Default Pawn Class=None

GM_GamePlayのセットアップが終わればP_GamePlayにGM_GamePlayをセットします。

レベルブループリントを開き次のように処理を組みます。

BP_Playerというノードはレベルに設置したBP_Playerアクターを選択した状態で、レベルブループリントで右クリックすると「Create Reference to BP_Player」とありますので、それをクリックすると出てきます。

ここまで準備できれば実際にプレイをしてWASDキーを押して画面上に値が出てきているか確認します。人によってはプレイ時に一度画面をクリックしないとWASDキーが反応しないこともあります。

正しくセットアップできていればWSADの順でキーを押すと次のような値が出力されます。

  • W : X=0 or -0 Y=-1
  • S : X=0 or -0 Y=1
  • A : X=-1 Y=0 or -0
  • D : X=1 Y=0 or -0

このプロジェクトでは2DゲームのようにX軸とY軸で動き回ることになります。基準となる向きは右方向に正X軸、下方向に正Y軸としています。ですので、上方向に動くWキーを押せば負Y軸であるY=-1となり、Sキーを押せば下方向なので正Y軸であるY=1になります。左右も同様です。

Enhanced Inputについては以下のリンクが参考になります。

【UE5】Enhanced Input | キシロラボ

Unreal Engine の Enhanced Input | Unreal Engine 5.1 ドキュメント

ステップ3:プレイヤーの移動処理

BP_PlayerControllerを開き、次のように処理を組みます。

これでキー入力に応じてプレイヤーが移動するようになります。

ここで「100」という数値を移動距離としましたが、100という数値は後に登場するブロックの1辺の長さに由来します。この「100」という数値は今後、変更する予定も無く、意味のある数値ですのでしっかりと定数として扱います。しかしUnreal Engineのブループリントでは定数が扱えないのでマクロライブラリを利用し、それを定数の代わりとします。

定数用マクロライブラリブループリントを作成します。

  • 親クラス : Object
  • 名前 : BPML_Constants

マクロを組みます。

BP_PlayerControllerに戻り、先程の「100」としたところをBoxEdgeLengthに置き換えます。

動作確認をして置き換える前と変化がないことを確認します。

移動ができたので次は移動方向に回転させます。
BP_PlayerControllerを開き、Set Actor Locationノードに続いて次のように処理を組みます。

動作確認をして移動方向に回転することを確認します。

マクロライブラリを定数の代わりとして利用する方法は次の記事を参考にしました。

20年オヤジのUnreal Engine 4 TIPS - SEGA TECH Blog

ちょっと小話 ~定数化する基準~ 自分が定数化する基準は大体、次のようなものです

  • 複数のブループリントに点在する
  • タイプミスをする可能性がある

特に「複数のブループリントに点在する」は定数化するきっかけとして最も多いです。同じ意味を持つ数値や文字列が点在してしまうと、検索や修正が非常に面倒なことになります。マクロで定数化してあげることで「名前を付ける」ことで検索しやすくなりますし、マクロ側の数値を変更すれば利用している箇所全てに適用できます。

1つのブループリントで複数のイベントで利用される場合、マクロは使わずに普通の変数を使います。

文字列を扱う場合、最も怖いのはタイプミスです。少文字の「i」と「l」はとても危険です。コピペすれば間違いようがないと思われるかもしれませんが、コピー元が間違っているときもあります(やらかしたことあります)。このようなタイプミスを防ぐにも定数化は有効です。

文字列の場合、定数化だけではなくEnum(列挙型)で指定できるようにすることも有効です。

ステップ4:スムーズに移動する

BP_PlayerControllerを開き、次の通りに変数を追加します。

続いてIA_Moveイベントの処理を組み直します。

Timelineノードを追加し、次のようにセットアップします。

  • 名前:Timeline_Move

Timeline_Moveノードから次のように処理を組みます。

再びIA_Moveイベントに戻り、bMovingにTRUEをセットしている箇所からTimeline_Moveコンポーネントを呼び出します。

TImelineノードを起動するには、左側にある実行ピンに別の実行ピンを繋げるだけではなく、「変数」の欄にある同名のTimelineコンポーネントを呼び出し、そこから「Play from Start」を呼ぶことで起動させることができます。

動作確認をすると1つ前のステップのときよりもスムーズに移動・回転します。bMovingフラグのお陰でTimelineノードの実行は入力を受け付けないようにしています。移動と回転の速度を早くしたければTimelineノードのLengthをより小さい値に、遅くしたければ大きい値にすれば調整できます。EaseノードのFunctionには多くの種類がありますが、それぞれ数値の加速具合が異なるので色々変えてみると面白いです。

[UE4] 動きに緩急をつけるEaseノードの紹介|株式会社ヒストリア

ステップ5:ステージを作る

ブロックの基底クラスとなるブループリントを作成します。

  • 親クラス:Actor
  • 名前:BP_Block_Base

BP_Block_Baseを開き、次のようにセットアップします。

BP_Block_BaseのTickは無効にしておきます。

BP_Block_Baseを継承してテスト用ブロックブループリントを作成します。

  • 親クラス:BP_Block_Base
  • 名前:BP_Block_Test

BP_Block_Testを次のようにセットアップします。

変数を追加してセットアップします。

続いてBegin Playから処理を組みます。


ステージ生成用ブループリントを作成します。

  • 親クラス:Actor
  • 名前:BP_StageBuilder

このアクターはTickが不要なのでStart with Tick Enabledのチェックを外します。

Begin Playノードから次のように処理を組みます。

Make Literal Stringの中身は次のようになっています。

11111
10131
10201
14001
11111

Make Literal Stringでは「Shift + Enter」を押すことで改行することができます。そこから続くParse Into ArrayノードでDelimiterに指定した文字を境にSource Stringを分割します。画像中では「Shift + Enter」をDelimiterに指定しています。For Each Loopノードで1行ずつ要素が取り出せるので、更にGet Character Array from Stringで文字列を1つの文字に分割します。

Switch on Intノードで文字を数値に変換しつつ、数値に対応するブロックを生成します。数値は次のように対応します。

  • 0:ブロックなし(床)
  • 1:壁ブロック
  • 2:押出し可能ブロック
  • 3:ゴールブロック
  • 4:プレイヤー

ここではテスト段階なので生成するブロックはすべてBP_Block_Testを生成します。違いはBlock Nameに指定する文字列だけです。

動作確認の前にステップ2でレベルに配置したBP_Playerとレベルブループリントの処理を削除します。

削除できたら、BP_StageBuilderをレベルに配置します。

動作確認すると文字の向きはおかしいですがMake Literal Stringで指定した数値がそのままブロックに置き換わっています。

現状ではブロックを無視して動き回れますが、後のステップで修正するので気にしないでOKです。

ステップ6:Paper2Dを使ってステージを作る

ステージをテキストで表現する方法はお手軽ですが一覧性が悪いのでPaper2DのTile SetとTile Mapを使って同様の表現をします。

Paper 2D | Unreal Engine 5.1 ドキュメント

その前にTile Setで使うテクスチャを作成する必要があります。ここでは筆者が作ったテクスチャを置いておきます。お好みの場所に保存してください。

自分の手でテクスチャを作ってみたい方に向けて上記のテクスチャを作った際の情報を載せておきます。

  • GIMPで作成(エディタはお好み)
  • 1パーツ:64 x 64
  • 全5パーツ:320 x 64
  • 左から床、壁、押出し可能ブロック、ゴールブロック、プレイヤー
  • 名前:T_StageTile

テクスチャが作成できたらプロジェクトにインポートし、Tile Setアセットを作成します。

  • 名前:TS_StageTile

TS_StageTileを開いてTile Sizeを1パーツのサイズに設定します。筆者のテクスチャをダウンロードした方は次の数値を入力してください。自作の方は1パーツあたりのサイズを入力します。

  • Tile Size:64 x 64

続いてTile Mapアセットを作成します。Tile Setアセットを右クリックすると「Create Tile Map」という項目があります。

  • 名前:1-1

Tile Mapアセットを開くとエディタが出現します。ざっくりと使い方を紹介します。

エディタ左にあるのがタイルパーツです。左から内部では0,1,2,3,4と数字が割り振られています。選択中のパーツは細いですが白枠が付きます。
パーツを選択しエディタ中央の白枠内をクリックすると選択したパーツが塗られます。エディタ中央の白枠のマス数を増やしたければエディタ左にある「Map Width、Map Height」の数値を増やしてください。上の画像では5 x 5のステージなのでMap Width、Map Heightは共に「5」という数値をセットしています。

では、テキストで表現したものと同じものを作ってください。上の画像と同じようになるはずです。


作成したTile MapアセットをBP_StageBuilderで読み込んでステージを生成します。

BP_StageBuilderを開き、次のようにセットアップします。

追加したPaper Tile Mapコンポーネントの「Visible」のチェックを外します。必要な情報は「各マスに何番のパーツが貼られているか」なので描画が不要なためです。

続いてBegin Play以降の処理を組み直します。

プレイするとテキストで表現していたときと全く同じステージが生成されたことが確認できます。 試しに1-1を再編集してブロックを変えてみたりステージのサイズを変えてみると正しく機能していることがよく分かると思います。

ちょっと小話 ~量産を手助けするツール~ 「物量」がものをいうゲームの場合、量産を手助けするツールの存在はとても重要です。今回、制作する倉庫番のようなシンプルなゲームでは「ステージ数」が大事なので、ステージを量産しやすくするためにPaper 2DのTile Mapエディタをステージエディタとして転用することにしました。

Unreal EngineにはEditor Utility Widget(EUW)という機能があります。UMGを使ってエディタを拡張することができ「あるアクターを全て、レベルから削除する」「レベルに多く配置されているアクターのプロパティに値をセットする」といったエディタに搭載されていない独自の機能を簡単に作ることができます。このEditor Utility Widgetを使ってアクターをレベルに配置する作業を楽にすることもできると思います。

例えば次のようなもの。

【UE4】NavLinkProxy配置作業をEditor Utility Widgetでちょっと楽にする - Qiita

2022年11月20日に開催されたUnreal Fest Westでの講演の1つ「「ライブアライブ」28年前の作品をHD-2Dで蘇らせる挑戦」では大量の技の演出や攻撃可能範囲等のチェックをEUWを使って効率よく実行できるようにしています。

EUWだけではなくData Asset使ってもいいです。例えば「スポーンタイミング」「スポーン位置」「タイプ(攻撃型、防御型等)」「性格(攻撃的、日和見的)」というデータをData Assetで定義すれば戦闘デザインを実験・調整・修正しやすくなります。

ステップ7:ステージを量産する

Tile Mapエディタを使うことでステージのデザインがとても簡単にできるので、あと3つほどステージを作ります。作成したTile Mapアセットの名前は次のようにします。

  • 名前:1-2
  • 名前:1-3
  • 名前:1-4

こういったデザインが苦手な方はジェネレータがweb上にありますので是非活用しましょう。画面左にあるつまみを全て左側に寄せると簡単な問題が作りやすくなります。

alliballibaba.github.io

3つステージが出来れば、BP_StageBuilderを開いてSet Tile Mapノードの「New Tile Map」を読み込みたいステージに変更して正しくステージが作れているか確認します。

ステップ8:ステージデータのロード処理

いくつかのステージデータをロードしたいとき、わかりやすいものは「Tile Mapのオブジェクト参照型の配列を用意し、予めロード対象を格納しておく」という方法があります。しかしこれには問題があり、配列に格納したTile Mapデータをすべてメモリに展開してしまいます。もしステージを100個、200個作るとしたら無視できないメモリ消費量になる可能性があります。

このプロジェクトではロード対象のTile Mapアセットをソフト参照という形で保持することにします。ソフト参照にすることで「このアセットをロードします」と明示的に宣言したときだけ対象のアセットがメモリに展開するようになります。


構造体アセットを作成します。

  • 名前:SStageAssetPath
  • 変数1
    • 名前:StageDataPath
    • 型:Paper Tile Map Soft Object Reference

Data Tableアセットを作成します。

  • Row Structure:SStageAssetPath
  • 名前:DT_StageAssetPath

DT_StageAssetPathを開いて次のようにデータを追加します。

Row Nameは自分で入力する必要があります。Row NameとStageDataPathの末尾部分が一致していること確認してください。


次にBP_StageBuilderを開きます。Begin Play以降の処理をまるっと別のイベントに移します。カスタムイベントを作成します。

  • 名前:BuildStage
  • 引数
    • 名前:Target
    • 型:Paper Tile Map Object Reference

空いたBegin Play以降にステージ読み込みの処理を組みます。

動作確認をします。前ステップと全く変わらない結果が出ればOKです。


このステップによって変化した部分はステップの冒頭でも書いた「アセットの参照」です。
Content BrowserでBP_StageBuilderを右クリックして「Reference Viewer」を選択すると、そのアセットが参照を持つアセットを閲覧することができます。Set Tile Mapに読み込みたいTile Mapアセットを直接指定したときのReference Viewerは次のようになります。(一部抜粋)

BP_StageBuilderから「1-1」というTile Mapアセットに白い矢印でつながっています。これは「ハード参照」を意味し、BP_StageBuilderをロードすると自動的にTile Mapアセットがロードさせることを意味します。

新しくBegin Play以降に追加したロード処理を挟むとReference Viewerは次のようになります。(一部抜粋)

Tile Mapアセットが無くなり、ロード対象から外れます。


この「アセットの参照」は重要なトピックではあるものの、ぷちコンで作るようなコンパクトなゲームでは過敏に反応する必要はないと思います。ですが、大まかでいいのでアセット参照の問題点と解決法は知っておいて損はありません。幸い日本語で読める資料はたくさんあるので興味が出たら一読してみることをオススメします。

アセットの参照 | Unreal Engine ドキュメント

第007回UE4のアセットの参照方法について、そのロードの違い | CC2の楽屋裏

ハード参照とソフト参照 - おかわりはくまいのアンリアルなメモ

BPの参照連鎖を断つ手法 | 株式会社ヘキサドライブ | HEXADRIVE | ゲーム制作を中心としたコンテンツクリエイト会社

ステップ9:壁ブロックと押せるブロック その1

現状ではカメラがレベルの中心で固定になっており見た目が悪いので、ステージ生成時にカメラをステージの大体中心に持ってくるようにします。

BP_StageBuilderを開き、次のように変数を追加します。

  • 名前:CameraActor
  • 型:Actor Object Reference
  • Instance Editable:チェック入れる

イベントBuild Stageの外側のループ(「Row」とコメントが有るところ)のCompletedから続いて次のように処理を組みます。

動作確認をし、カメラがおおよそステージの中心に来ていればOKです。


ブロックのブループリントを作る前にそれぞれに割り当てるマテリアルを作成します。
まずブロックに適用するマテリアルの親マテリアルを作ります。

  • 名前:M_Block

M_Blockを開いて次のように処理を組みます。

M_Blockからマテリアルインスタンスを2つ作成します。Content BrowserでM_Blockを右クリックし「Create Material Instance」を選べばOKです。各マテリアルインスタンスの名前と設定する色は次のようにしました。色については特に指定は無いので好きな色で構いません。テクスチャを用意できるならそれを使ってもOKです。その場合はマテリアルに少し変更を加える必要があります。

  • 名前:MI_Block_Push
    • 色(Hex Linear):FF990000
  • 名前:MI_Block_Wall
    • 色(Hex Linear):4D1A0000

BP_Block_Baseを継承して壁ブロックブループリントを作成します。

  • 名前:BP_Block_Wall

BP_Block_Wallを開いたら、まずアクタータグを設定します。アクタータグは「Class Defaults -> Actor -> Advanced -> Tags」にあります。

  • Tags:Wall

続いてCubeメッシュコンポーネントに先程作成したMI_Block_Wallマテリアルを割り当てます。

次にBP_Block_Baseを継承して押せるブロックブループリントを作成します。BP_Block_Wallと同様にアクタータグを設定しマテリアルを割り当てます。(画像省略)

  • 名前:BP_Block_Push
  • Tags:Push
  • マテリアル:MI_Block_Push

BP_StageBuilderで壁ブロックと押せるブロックを生成している箇所をBP_Block_Wall、BP_Block_Pushにそれぞれ差し替えます。

BP_StageBuilderを開き、次に示すノードに渡すパラメータを変更します。

Spawn ActorノードのClassを変更したことで「Block Name」が無くなり、エラーが出ますがノードを右クリックして「Refresh Nodes」を選択することで解決することができます。

動作確認をして正しく各ブロックブループリントが生成されているかチェックします。

ステップ10:壁ブロックと押せるブロック その2

まず進行方向にブロックがあるときは移動できないようにします。

BP_PlayerControllerを開き、新しく関数を追加します。

  • 名前:CanMoveTo
  • 返り値:Boolean型
  • 返り値2:Actor Object Reference

続いてイベントIA_Moveを書き直します。

動作確認をして移動先にブロックがあるときは移動できないかチェックします。ここではLine Traceが「何にも衝突しなかったとき」は移動可能としているので、押せるブロックでもゴールブロックでも移動できないようになっています。


次は移動方向に押すブロックがあるとき、押せるブロックを押して自身も移動するようにします。

まずはBP_Block_Pushに「移動する処理」と「移動可能かチェックする処理」を追加します。
BP_Block_Pushを開いて変数を追加します。

  • 名前:StartLoc
  • 名前:GoalLoc

続いてTimelineノードを追加します。ここでの処理はBP_PlayerControllerにあるTimeline_Moveと同じです。

続いて関数を追加します。

  • 名前:TryMoveTo
  • 引数:Vector2D
  • 返り値:Boolean型

再びBP_PlayerControllerを開いてCan Move To関数から続けて次のように処理を組みます。(画像一部省略)

動作確認して押せるブロックが目の前にある時、押して移動することができる事と押せるブロックと壁ブロックが隣り合ったとき、押せるブロックは壁ブロック方向に押せないことを確認できればOKです。

ステップ11:ゴールブロックとゴール検出

ゴールブロックに割り当てるマテリアルを作成します。M_Blockからマテリアルインスタンスを作成します。

  • 名前:MI_Block_Goal
    • 色(Hex Linear):00FF0000

次にBP_Block_Baseを継承してゴールブロックブループリントを作成します。

  • 名前:BP_Block_Goal

BP_Block_Goalを開いてセットアップします。はじめにCubeメッシュコンポーネントに先程作成したMI_Block_Goalを割り当てます。

続いてCubeメッシュコンポーネントのScaleとCollision Presetを変更します。

  • Scale:XYZ = 0.8
  • Collision Preset:Invisible Wall

次にBox CollisionコンポーネントのBox Extentを変更します。

Box CollisionのCollision PresetをInvisible Wallにすることでプレイヤーの移動時に実行するLine Trace処理でBP_Block_Goalが衝突することが無くなります。BP_PlayerControllerで実行するLine Traceは「Visibilityチャンネル」で衝突の判定をしているため、Visibilityチャンネルの衝突が無効になっているInvisible WallプリセットにすることでLine Traceの判定を無視します。

セットアップが完了すると緑色のCubeの頭からひょっこりBox Collisionがはみ出てるようになります。

ゴールブロックができたのでBP_StageBuilderのゴールブロック生成部分をBP_Block_Goalに差し替えます。

動作確認をしてゴールブロックがBP_Block_Goalに差し替わっていること、プレイヤーがBP_Block_Goalに移動できること、BP_Block_PushをBP_Block_Goalに押せることをチェックします。


次に押せるブロックがゴールブロックに重なったとき、メッセージを出力するようにします。

BP_Block_Goalを開き、Box Collisionの「On Component Begin Overlap」「On Component End Overlap」イベントを作成します。
それぞれ次のように処理を組みます。

動作確認し押せるブロックがゴールブロックに重なったとき、メッセージが出力されることをチェックします。ゴールブロックから外れたときもメッセージが出力されることもチェックします。


押せるブロックがゴールブロックに重なったとき(外れたときも)同じ内容のメッセージが2つ同時に出力されたはずです。これは不具合なので修正します。

原因を突き止めるためOn Component Begin OverlapのOther ActorとOther ComponentでそれぞれGet Display Nameでオブジェクト名をメッセージとして出力させます。

プレイをしてメッセージを出力されると次のように表示されます。

下から上に向かって順に「重なったことのメッセージ」「重なった相手のアクター名」「重なった相手のコンポーネント名」が出力されます。重なった相手のアクター名はどちらも「BP_Block_Push」ですが重なった相手のコンポーネント名が異なります。これはBP_Block_Pushが持つBox CollisionコンポーネントとCubeメッシュコンポーネントが2つともBP_Block_Goalと重なったイベントを発生させていることを示しています。

原因がわかったのでBP_Block_PushのBox CollisionコンポーネントとCubeメッシュコンポーネントのどちらかを重なったときイベントを発生させないようにします。今回はCubeメッシュコンポーネントを修正します。

BP_Block_Pushを開いて、Cubeメッシュコンポーネントの「Generate Overlap Events」のチェックを外します。

再び動作確認をして押せるブロックとゴールブロックが重なったときに「1度だけ」メッセージが出力されることをチェックします。

確認できればBP_Block_GoalのGet Display Nameを繋いだPrint Stringノードは不要なので削除してOKです。

ステップ12:ゲームクリアの検出

このゲームでのゲームクリア条件は「全ての押せるブロックがゴールブロックと重なった」です。これを検出する処理を組んでメッセージを出力します。

GM_GamePlayを開いて変数を2つ追加します。

  • 変数1
    • 名前:NumOnGoalBlock
    • 型:Integer
  • 変数2
    • 名前:TargetNumGoalBlock
    • 型:Integer

続いてカスタムイベントを2つ追加します。

  • 名前:IncreaseNumOnGoalBlock
  • 名前:DecreaseNumOnGoalBlock

まずはGM_GamePlayとBP_Block_Goalの連携部分を作っていきます。2つのカスタムイベントの処理を次のように組みます。

次はBP_Block_Goalを開き、GM_GamePlayに作成した2つのカスタムイベントを呼び出す処理を組みます。

動作確認をして押せるブロックとゴールブロックが重なると数値が出力され、外れると1減った数値が出力されます。


実はこの実装では次のようにゴールブロックが隣り合うステージのとき...

1つの押せるブロックをゴールブロックからゴールブロックへ移動すると一瞬ですが「押せるブロックが2個ゴールブロックと重なった」と判定されてしまいます。

この不具合の原因はBP_Block_GoalのコリジョンとBP_Block_Pushのコリジョンの大きさにあります。


原因がわかったので修正していきます。

BP_Block_Pushを開き、Box CollisionコンポーネントのBox Extentを次のように設定します。

設定を終えたら動作確認をして、ゴールブロックが隣り合った状況で押せるブロックを移動しても正しい数値が出力されることを確認します。


GM_GamePlayは「ステージにゴールブロックがいくつあるか」という情報を知らないので、誰かが教えて上げる必要があります。

BP_StageBuilderを開き、変数を1つ追加します。

  • 変数
    • 名前:NumGoalBlock
    • 型:Integer

Spawn Actor from ClassでBP_Block_Goalをスポーンしたあとに続いて次のように処理を組みます。

同じBP_StageBuilderのCamera ActorのSet Actor Locationを呼び出したあとに次のように処理を組みます。

次はGM_GamePlayを開いてイベントIncreaseNumOnGoalBlockの続きに次のような処理を組みます。

動作確認をして全ての押せるブロックがゴールブロックと重なったときステージクリアを示すメッセージが出力されればOKです。今はメッセージが出力するだけなので、ステージクリアしてもプレイヤーは引き続き操作でき、ブロックを動かせてしまいますが問題ありません。

ステップ13:やり直し機能

詰み防止のために「やり直し機能」を実装します。ここでの「やり直し」はブロックとプレイヤーの位置をステージ開始時点に戻すものであり、いわゆるUNDO(1つ前に戻る)ではありません。

ゲームプレイ用ウィジェットブループリントを作成します。

  • 親クラス:User Widget
  • 名前:WBP_GamePlay

ここからウィジェットブループリントを作っていきますが、その前にこの記事でのウィジェットのパーツ構成の表記を紹介します。

  • WBP_GamePlay
    • [CanvasPanel]
      • [CanvasPanel_GamePlay]
        • Anchor : 全面
        • Offset Right & Bottom : 0.0

この記事では上の例のように箇条書きでパーツ構成を表記していきます。

[](大かっこ)で囲われたものがパーツで[パーツ名_識別名]と表します。かっこが無く:(コロン)で区切られたものは1段上のパーツのプロパティを表します。表記がないプロパティはデフォルト値とします。CanvasPanel_GamePlayの1段下にAnchor:全面とありますが、このときはCanvasPanel_GamePlayのAnchorをプリセットの一番右下にあるものを選択します。

Offset Right & BottomはOffset RightとOffset Bottomの両方とも0.0にセットします。


WBP_GamePlayを開いてパーツを配置します。

  • WBP_GamePlay
    • [CanvasPanel]
      • [CanvasPanel_GamePlay]
        • Anchor:全面
        • Offset Right & Bottom:0.0
        • [Button_Retry]
          • Position X&Y:50.0
          • Size to Content:チェック入れる
          • [Text]
            • Text:やり直す
            • Font Size:36

Button_RetryのOn Clickedイベントを作成します。


作ったウィジェットを画面に表示します。GM_GamePlayを開いてBegin Playに続けて次のように処理を組みます。

動作確認をします。適当にブロックを動かしたあと、画面左上にある「やり直し」ボタンを押すとステージがリセットされればOKです。

ステップ14:ステージクリアUIを出す

ステージクリア用ウィジェットブループリントを作成します。

  • 親クラス:User Widget
  • 名前:WBP_GameClear

WBP_GameClearを開いて次のようにパーツを配置します。

  • WBP_GameClear
    • [Canvas Panel]
      • [Border]
        • Brush Color(Hex Linear):00000080
      • [Text]
        • Anchor : 中心
        • Position Y : -100
        • Alignment : X & Y=0.5
        • Size to Content : チェックを入れる
        • Text : STAGE CLEAR
        • Font Size : 144
      • [Button_StageSelect]
        • Anchor : 中心
        • Position X : -300
        • Position Y : 200
        • Alignment : X & Y=0.5
        • Size to Content : チェックを入れる
        • [Text]
          • Text : ステージセレクト
          • Font Size : 36
      • [Button_Next]
        • Anchor : 中心
        • Position X : 300
        • Position Y : 200
        • Alignment : X & Y=0.5
        • Size to Content : チェックを入れる
        • [Text]
          • Text : 次へ
          • Font Size : 36


次にGM_GamePlayを開いて変数を1つ追加します。

  • 名前:WBP_GamePlay_Ref
  • 型:WBP_GamePlay Object Reference

前ステップで組んだWBP_GamePlayの生成部分に処理を追加します。

続いてゲームクリア判定の処理から次のように処理を組みます。

動作確認をして、ゲームクリア時にやり直しボタンのUIが消えて「STAGE CLEAR」のUIが表示され、操作もキーボード操作も受け付けない状態になればOKです。STAGE CLEARのUIにあるボタンは何も処理を書いていないので押しても反応はありません。

次のパートへ...

メインメニュー・ステージセレクト編

Lubuntu 22.04 LTSでノートPCのスクリーン輝度を調整したい

環境

OS:Lubuntu 22.04 LTS

PC:Aspire 5733Z

問題

・ショートカット「Shift + Ctrl + F6 or F7」が効かない

・設定の「明るさ」を操作するとバックライトではなくコントラストが変更される

・xbacklightを導入しても「No outputs have backlight property.」と表示される

解決策

LXQt - ArchWiki

上記リンクの項目「スクリーン輝度」より

pkexec lxqt-backlight_backend --inc 
pkexec lxqt-backlight_backend --dec

または

brightnessctl -d intel_backlight set +5%
brightnessctl -d intel_backlight set 5%-

を使えばバックライトを調整することができた。

一応、/etc/default/grubGRUB_CMDLINE_LINUX_DEFAULTを以下のように書き換えた。(効果があったかは不明)

GRUB_CMDLINE_LINUX_DEFAULT="quiet splash acpi_backlight=vendor"

書き換え後は再起動すること。