ue5_minigame
skonishi1125.icon
Chapter13 まで進めたが、もっと簡易的なゲームを作る。コイン集めとかそういうの
実装する内容
Git管理
Landscapeモードで土地の作成
キャラのアニメ用意
キャラアニメのBlendTree
メモ
Solution Explorer で Active Itemを有効化しておき、追いやすくしておく
https://scrapbox.io/files/69ddfabc3caed06dfed81638.png
まず
Git管理の方法をまとめる
Git
Git LFS 導入
cloneしてLFSの適用。できたらUE5 Projectを作成。
作成しようと思ったがGit管理のファイルを指定できなかったので、Blankを移動させて、そこで解凍してみることにした
File > Zip Project.
https://scrapbox.io/files/69dc82873caed06dfe7d19c7.png
こちら側でProjectを開いて、リビルド
ワールド設定
こんな感じ。
https://scrapbox.io/files/69dc83fc3caed06dfe7d1db6.png
あらかじめUE5で空プロジェクトを作る
zip にする
git管理下で展開 が楽かも。
UEGitPlugin を入れる場合
Pluginsを自分で作って展開。
visual studio の設定ファイルをgenerateする
https://scrapbox.io/files/69dc84fe3caed06dfe7d2080.png
作業に沿ってすすめて、uprojectを更新
Blank could not be compiled. Try rebuilding from source manually.
.slnからエディタを開いて、そちらでビルドを試す。
1>Unable to load the previously-compiled assembly file 'C:\Users\watas\Ue5Project\ue5_minigame\Intermediate\Build\BuildRules\BlankModuleRules.dll'. Unreal Build Tool will try to recompile this assembly now. (Exception: Could not load file or assembly 'C:\Users\watas\Ue5Project\ue5_minigame\Intermediate\Build\BuildRules\BlankModuleRules.dll'. アプリケーション制御ポリシーによってこのファイルがブロックされました。 (0x800711C7))
Windows の スマートアプリコントロール をオフにする
他も細かいコンパイルエラーが出るので直して、ビルドを通す。
チーム開発の時はここからC++ クラスを作る親を変更する調整を行う。個人ならここまでする必要はそこまで無い
最低限の手順
リポジトリ作る
SourceTree > リポジトリ > Git LFS > リポジトリの初期化
UE5でProjectを作る
Zip Project をして、用意したGitリポジトリ下で展開。
.slnを作る。開いてビルド。UE5を開く。
上手くいってるっぽい
マップ作成
File > New level。UEにおけるレベルとは、マップのこと。
Emptyで色々配置していってみよう。
Window > Place order.
Sky Atomoshere
Directional Light(指向性ライト: 影などが同じ角度に落ちるようにする)
Sky light
Exponential Height Fog
Volumetric Clouds
Landscape Modeで地面を作る
テクスチャはこの後blendさせる。
BPでitemを用意する
コインを作ろう。まずは親クラスのItem C++ Classを作る。
/All/Classes_Game/Blank/Public/Items/Item.cpp
これをベースにBP_Itemを作ってみる
Git LFSの警告
https://scrapbox.io/files/69dcd5893caed06dfe7dd42a.png
リポジトリのGit LFSのトラッキング管理で、以下を追跡するようにする
code:txt
*.uasset
*.umap
Itemクラスでやること
StaticMeshComponentの付与
SphereComponentの付与(判定)
OnOverlapBeginの付与
StaticMesh割当
code:cpp
// Root に Scene(空コンポーネント)を付与して、子要素にMeshを付与する
void AItem::AttachMeshComponent()
{
RootScene = CreateDefaultSubobject<USceneComponent>(TEXT("RootScene"));
SetRootComponent(RootScene);
ItemMesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("ItemMeshComponent"));
ItemMesh->SetupAttachment(RootScene);
}
アセットはItemMeshに割り当てて、ここで大きさなどを調整するといい。
ふわふわ & 回転させる
ふわふわ。sinを使う。
code:cpp
// sin
return Amplitude * FMath::Sin(RunningTime * TimeConstant);
// 使用時
AddActorWorldOffset(FVector(0, 0, TransformedSin()));
TimeConstant: 2πの波の周期の速さを調節するパラメータ
Amplitude: 振幅。 デフォルトの±1 の返り値をこの振幅分だけ大きくする
回転
code:cpp
void AItem::RotateObject(float DeltaTime)
{
// deg/s * s = deg : 1フレーム(delta)あたり 何度回転するのか const float DeltaRotation = RotationSpeed * DeltaTime;
AddActorLocalRotation(FRotator(.0f, DeltaRotation, .0f));
}
という感じで、LocalRotationにTickで常に値を与え続けることになる
キャラの用意
Slashからechoを借りてくることにする。手順は以下
migrateで移行する
Characterフォルダでmigrate -> Blank の content へ移行。
使ってみる
import した echo のskeletal meshをクリックすれば、どんな3Dモデルかを目視できる。一通り見た後は、New C++ クラスを作成して、このキャラクター用のcppファイルを用意。
Input の割当て、の前に、Pawnを使ったテストでおさらいする
Birdというクラスを用意。間違えて作った場合などは、エクスプローラーでフォルダを消してビルドし直す。Inputは元々用意されているSetupPlayerInputComponentで受け取ることが出来る。
C++ クラスを作ったBPクラスBP_Birdを作る。動物のアセットを引っ張って持ってくる。素材を割り当てるためのコンポーネントを付与する必要がある。Meshでもいいが、処理が重くなる。なので、CapsuleComponentを割り当てて、簡易的にいろいろ検知できるようにする。
鳥自身のメッシュを持たせるために、SkeletalMesh コンポーネントを持たせる。
? StaticMeshComponent とSkeletalMeshComponentを分ける意図
SkeletalMeshなら、アニメーションを付与できる。なので、アニメのあるActorならSKを選ぶ。
? 調べ方
BPクラス側で、Skeletal Meshに関連するコンポーネントがあるかを軽く調べる。
https://scrapbox.io/files/69ddf9b43caed06dfed811f8.png
これをC++で割り当てればよい。だいたい、命名規則としてU + Component名 である。
今回は、include しなくともビルドが通った。これは推移的インクルードによるもの。
IWYU原則に従って、明示的にしよう。
USkeletalMesh に、カラスのSKとアニメを割り当てると動く(StaticMeshでなく、SkeletalMeshならアニメも割り当てられる)。
Inputを割り当てる
プレイヤー所持権を自身にしたい場合は、Object のAuto Possess Player を指定すればよい。
https://scrapbox.io/files/69df49a03caed06dfee4cf6b.png
AutoPossessPlayer = EAutoReceiveInput::Player0;でもOK
⭐Axis / Action でなく、UE5の標準である Enhanced Input を使ってみる
移動
右クリック
Input > InputAction でIA作成
Input > Input Mapping ContextでIMC作成
IMC_Moveなど、WASDを割り当てたりする箇所はここ。
カメラ処理
カメラを持たせる
Cameraコンポーネントを持たせることで、三人称視点のような形で扱うことが出来るようになる。使い方としては、rootcomponent に SpringArm を持たせて, その子要素としてカメラを繋ぐ。SpringArm で持たせておくことで、カメラが壁にぶつかった場合などに自動でズーム調整してくれるようになる
https://scrapbox.io/files/69df6e9a3caed06dfee5accc.png
今回はC++で持たせる。SpringArmのRotateを調整することで角度を持たせるようにする。
SpringArm に回転を加えて、見下ろし視点にする
code:cpp
SpringArm->AddLocalRotation(...)
? 引数として渡す値はどのように判断すべきか
回転程度なら探せばコピペで対応できるが、1から調べる場合はどうするか。まずインテリセンスを見る。
https://scrapbox.io/files/69e05b6f3caed06dfeec28b5.png
https://scrapbox.io/files/69e05b673caed06dfeec28a5.png
1 of 2で最初に渡されているのは、FQuat。クォータニオンはエンジン内部の計算では使うが、角度の直接指定の場合にはそこまで使わない。2 of 2で渡されているのはFRotatorであり、Pitch, Yaw, Roll の3つの角度を持つデータ。今回はこちらを使う。
その他引数の説明を知りたければ、メソッド定義箇所を見る。
code:UE_5.5\Engine\Source\Runtime\Engine\Classes\Components\SceneComponent.h
/**
* Adds a delta to the rotation of the component in its local reference frame
* @param DeltaRotation Change in rotation of the component in its local reference frame.
* @param SweepHitResult Hit result from any impact if sweep is true.
* @param bSweep Whether we sweep to the destination (currently not supported for rotation).
* @param bTeleport Whether we teleport the physics state (if physics collision is enabled for this object).
* If true, physics velocity for this object is unchanged (so ragdoll parts are not affected by change in location).
* If false, physics velocity is updated based on the change in position (affecting ragdoll parts).
*/
ENGINE_API void AddLocalRotation(FRotator DeltaRotation, bool bSweep=false, FHitResult* OutSweepHitResult=nullptr, ETeleportType Teleport = ETeleportType::None);
こちらで反映できる。なお、今回はAddLocalRotation()で角度をコンストラクタ実行時に加えたが、本来はSetRelativeRotation()で、初期角度を決めるのが一般的。
Add があるならGet / Setもあるだろうという見切り、Blueprint側のエディタで辞書代わりに検索して、より好ましい関数があるかどうかを探してみるなどが有効そう。
マウス操作で、視点移動を実装する
新しいインプットアクション、NIA_Lookを追加
IMCに作成したIAを追加。
https://scrapbox.io/files/69e08ff73caed06dfeed57cf.png
Mouse XY 2D-Axis: マウスのXYの動いた値を取る。NIA側のValue TypeはVector 2Dにしておく。
Modifier
Negate: 反転。
Yへのチェック : Y軸だけ逆にするということ。
マウスを上に動かしたとき(-Y), UEのカメラを上に上げる(+)というしくみなので、Negateで反転させる。
ちなみに X にチェックを入れると、マウスを右に向けると左を向くようになる。 Z はそもそも 2D で値を取り込んでいるので何も起きない。
Bird.hで、このIAを持たせるための変数を用意。
Bird.cppで今回作ったものを、毎フレーム呼び出す関数を指定して実装。
code:cpp
# Look
if (Controller != nullptr)
{
AddControllerYawInput(LookAxisVector.X);
AddControllerPitchInput(LookAxisVector.Y); // Negate としてないなら、-Y を渡せば実質同じ挙動になる
}
回転設定
code:cpp
SpringArm->bUsePawnControlRotation = true;
bUseControllerRotationYaw = true;
マウスを動かしたとき、Controllerという要素が回転する。この目に見えない要素を、その他コンポーネントにも適用するのかという設定値。SpringArmが追従するとカメラが回るようになる。ABird自身もYawだけそれに応じて振り向くようにしている。
もし空を飛びたいなら、bUseControllerRotationPitchをtrue にしたら、Pitch(お辞儀イメージ)の回転が適用されるようになるので空を飛べるようになる
バグ
code:cpp
# Move()
FVector2D MovementVector = Value.Get<FVector2D>();
if (Controller != nullptr)
{
const FVector ForwardDirection = GetActorForwardVector();
const FVector RightDirection = GetActorRightVector();
float MoveSpeed = 10.0f;
AddActorLocalOffset(ForwardDirection * MovementVector.Y * MoveSpeed, true);
AddActorLocalOffset(RightDirection * MovementVector.X * MoveSpeed, true);
}
スタート開始時点ではWで前方に向かうが、マウスで右を向くと、Wで右に進む。
https://scrapbox.io/files/69e0dbf93caed06dfeef32ce.png
GetActorForwardVector()は、鳥が向いている方向。ゲーム開始時(北)なら、(1, 0, 0)が返る。右(東)を向くと、(0, 1, 0)を返す。AddActorLocalOffset()は鳥自身を基準とした相対的な座標で動かすので、Wを押したときに北を向いていたら前方に、右を向いていたら鳥を右向きに動かす処理になる。左を向いていたら左、後ろを向いていたら後ろに進む。
AddActorWorldOffset()を使って、世界を基準に動かせばいい。右を向いているとき(0, 1, 0)、世界から見て右に動かす(そのまま右に動く)。
Default Pawn の出現を設定する
World Setting > Game Mode で上書きすることでこの辺りを調整できる。Noneとしている場合はプロジェクトのデフォルト値が使われる。この値も、BP ClassでBP_BirdGameModeという要素を作って設定ができる。
Default Pawnを今回BP_Birdにすることで対応。
https://scrapbox.io/files/69e18f8e3caed06dfef188c4.png
この過程でPlace Actors Panel からスポーン位置などを調整しておくとよい
キャラを用意する
Echo を使う。
C++クラスの作成。Characterクラスを親として、BlankCharacter.csとした
そちらをベースにBP Class 作成。キャラクターをベースとすると、ある程度必要なコンポーネントは付与されているので(CharacterMeshや、Capsuleなど)、まずはそちらに要素を割り当てていく(Unityで言うSerializeFieldの部分と同じ)。
割り当てたら、正しいゲーム上の前方を向くように Character Mesh を回転。Capsule に合うようにキャラの位置も調整。重力などはデフォルトで適用されるようになっているのでそのまま進める。この時点で、ゲーム画面に配置すると表示されるようになっているはず。アニメーションを割り当てるとその挙動のまま配置できる
操作割当
IA, IMC 変数の用意。コンストラクタでの定義。各種移動関数の用意と、SetupPlayerInputComponentの設定。
BP側で割当。GameMode をこのキャラクタで始めるように設定。
SpringArm と Camera もつける。
カメラの向いた方向に、今度は Player を追従させないようにする
キャラクターを Controller に追従させる設定のbUseControllerRotationYaw = true;を切ればよい
Move処理の修正
code:cpp
const FVector ForwardDir = GetActorForwardVector();
const FVector RightDir = GetActorRightVector();
カメラだけを動くようにしたので、キャラは右を向いているが、画面としては正面を見ているケースがある。この状態でWを押すとキャラの前方に進む処理なので、右に進むことになる。なので、World基準での前(1,0,0)※ベクトルはUEではXが前方, Yが右を示すに進ませるとよい...と思ったが、それだとキャラがどんな方向を向いていて、カメラがどんな状況だったとしても、World の前に向かってしまう
やることとしては、Controller 自体の回転を取得し、回転行列として処理することでController にとっての正面ベクトルを取得すること
https://scrapbox.io/files/69c4a7459ea0767ac854cad4.jpeg
ベクトル|V|がある。これをθ方向に傾ける場合、|V|とR(θ)を回転行列で掛け合わせる。
code:cpp
const FRotator ControlRotation = GetControlRotation();
const FRotator YawRotation(0.f, ControlRotation.Yaw, 0.f);
const FVector Direction = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::X); // 回転行列
const FVector ForwardDir = Direction;
Controller の Rotation を取得して、その中の YawRotation (横向き回転) だけ取り出す(上下も回転を取ってしまうと、空を見上げてWを押したときに上に飛んで行ってしまう)。
FRotationMatrix(YawRotation)で、角度θ(Yaw)が入った3 * 3 の回転行列で計算した結果が入る。よって、角度が考慮されたベクトルの値が入っているので、その中のx(正面)だけGetUnitAxis()で取得している。
Groomの適用
Groom(グルーム)とは、髪の毛や動物の毛(ファー)を、リアルに描画、物理シミュレーションするための専用データ。旧式のデータとして髪の毛テクスチャを張り付ける、ヘアガードと呼ばれる手法があったが、それよりもデータを持たせやすくしたもの。
割当にはinclude だけでなく、モジュール自体も読み込む。
code:cpp
PublicDependencyModuleNames.AddRange(new string[] {
"Core", "CoreUObject", "Engine", "InputCore", "EnhancedInput",
"HairStrandsCore" // <- 追加
});
Ctrl Shift Bでビルドして、Binaries, Intermedimate, Savedのファイルを消す。その後Generate Visual Studio project Filesで再生成。Visual Studio でビルド。
キャラの容量を下げる
まず、プロジェクト自体の上限を調整
$ r.Streaming.PoolSize 2000
今回ノートPCの設定に合わせて、GTX 1650 の 4GB VRAM に合わせて、2GB とする
治らなければクオリティを下げる。
テクスチャ
テクスチャを開くと、Maximum Texture Sizeが0で設定されている。これは制限なしということなので、1024とか2048(2k)にして負荷を下げる。
右クリックでまとめてTexture Source Reduce Sizeで変更もできるが、元データ自体が変わるので、今回はAsset Actions > Edit Selections in Property Matrixで設定する。
https://scrapbox.io/files/69e1fbde3caed06dfef402ca.png
何か選択してからctrl A ですべて選択して、Maximum Texture Size を調整して保存。
https://scrapbox.io/files/69e1fd273caed06dfef40531.png
UE を閉じて再度開く。
Skeletal Mesh
LOD を調整(Level of Detail)して、常に少し荒い状態に設定すればよい
まず、Asset Details で、LODの数を4-5に調整。荒いレベルのものをいくつか作ってくれる(0が最高クオリティ)。minimum を 希望のクオリティに合わせればよい。
https://scrapbox.io/files/69e207533caed06dfef4260e.png
移動方向にキャラクターも回転させる
キャラが右を向いてて、Controller(カメラ)が真正面を向いていたら、Wで前進する。ただ、右を向いたまま走るので、移動方向にRotate したい。
code:cpp
#include "GameFramework/CharacterMovementComponent.h" // 進行方向に振り向く設定にするかどうかと、振り向き速度
GetCharacterMovement()->bOrientRotationToMovement = true;
GetCharacterMovement()->RotationRate = FRotator(0.f, 400.f, 0.f);
これでうまくいくはずだが、動かない。
code:cpp
# Move()
AddActorWorldOffset(ForwardDir * MoveVector.Y * MoveSpeed, true);
AddActorWorldOffset(RightDir * MoveVector.X * MoveSpeed, true);
# ↑をやめて、ACharacter の持つ専用の移動関数を使う
AddMovementInput(ForwardDir, MoveVector.Y);
AddMovementInput(RightDir, MoveVector.X);
AddActorWorldOffsetは、Managerを無視したワープ挙動の移動なので、MovementInputを使った形で移動させてやると動作する。またMoveSpeedをfloatで定義していたが、これもキャラクタークラスなら既に所持している。BP側からMax Walk Speedなどで調整しよう。
State別にアニメーションを用意する
Animation BluePrintを用意して、対象のSkeletonを選択。こちらでパラメータに応じたアニメの使い分けが作れる。
Idle <-> run を作る。必要なのは対象キャラクターが移動しているかどうか。ABP上で、Variableとして、BP_BlankCharacterを用意する。
キャラが動いているどうかの情報取得
https://scrapbox.io/files/69e236733caed06dfef4a2ed.png
ABPの変数として、Character, MovementComponent, GroundSpeedの3つを用意。
初期時、ABPを割り当てているOwnerからPawnを取ってそれをキャストし、Character変数に入れる。キャストできたらCharacterMovementComponentを取得できるのでそれもMovementComponent変数にいれる。移行、Update(Tick)で、そのMovememtComponent変数からVectorVelocityが取得できるので、それをGroundSpeed変数として入れ続ける。このGroundSpeed変数に応じてIdle / Run を切り替えればよい。
State Machineの作成
AnimGraph で、 State Machine で作成。Entryピンから線を伸ばしていって、Stateを追加していく。State にはそれぞれ名前に合ったモーションを用意してつけておく。遷移条件の中はで先ほど用意したGroundSpeedを使って条件を作成していく形になる。
ABPの親クラスで上記のBPで書いた、GroundSpeed等の取得処理を作る
ABPの右上に元としたParent Class が存在している。こちらを、自身で用意したC++クラスを使うようにする。今回、BlankAnimInstance.cppとする。GroundSpeedを取得するために必要な手順は、
ABP が割り当てられた親からPawnを取得する
BlankCharacter にキャストして、キャストできたら付属している CharacterMovementComponent が取得できる
CharacterMovementComponent から 移動ベクトルを取得して、それをVSizeXYでfloatとしている。
code:cpp
if (BlankCharacterMovement)
{
GroundSpeed = UKismetMathLibrary::VSizeXY(BlankCharacterMovement->Velocity);
if (GroundSpeed != 0)
UE_LOG(LogTemp, Log, TEXT("The vector value is: %s"), *BlankCharacterMovement->Velocity.ToString());
}
BlankCharacterMovement->Velocityから、秒間あたりの移動量が返る。(600, 0, 0)なら、X軸の正方向に秒間600進む速さ。ただこれはあくまでベクトルでの返り値なにで、VSizeXYでfloatに返されている。
code:cpp
# VSizeXYの中身(√(X^2 + Y^2) の、二点間の距離の計算)
## (600,0,0)なら、√360000 = 600 が返る
## 斜め移動 (391.573, 106.755,0) なら、 335092 + 24906 = √359998 = 約 600 が返るという感じ
return FMath::Sqrt(X*X + Y*Y);
Zを考慮しない理由は、垂直の動きだから。
ログを出して値を見ていくとわかりやすい
ジャンプ処理をEnhanced Input で作る
NIA_Jumpをboolで作る(jump中かそうでないかのbool)
IMCに割当。C++で登録。
Blueprint側で、C++で作った変数(JumpActionとした) に NIA_Jump を登録。
State Machineの管理
ジャンプ用の Animation と、State の遷移を作る
MovementComponent から IsFalling()という空中判定のデータが取得できるのでこれをC++側で使ってUpdateで見る
ABP側: Main States という要素を作る
Ground -> OutputPoseとしていた形式を、もっと子階層に分ける
MainStates -> OutputPose
MainStatesには、OnGround,InAir,Landの子階層を作って、それらをIsFalling等で分割する
OnGroundはIdle,Runなど
InAirは落ちているアニメーションなどをつけていく
MainStateをOutputでつないで、Main内部で小分け。OnGroundなどで、Cacheとしたポーズ遷移を呼び出す
https://scrapbox.io/files/69e489813caed06dfeff046b.png
https://scrapbox.io/files/69e489903caed06dfeff0486.png
Land -> OnGroundへと戻る際は、Automatic Rule Based on Sequence Player in Stateをtrueとした。これは、現在のアニメーション再生が終わったら自動的に次のステートに遷移させる機能。
着地アニメのカット
遷移条件を2種類付属させることで、着地後のアニメを任意のタイミングでスキップさせる
1つめは、アニメ再生が終わったら遷移させる
2つめは、
アニメの秒数が0.2秒経過
GroundSpeedが0よりも上
https://scrapbox.io/files/69e48f213caed06dfeff5a9d.png
ジャンプをきびきび動かす
Gravity Scale とジャンプの強さを変えて調整した。仮に、通常落下するときは元の重力加速度のまま落とすなどの場合は、関数側で別途工夫が必要となる。
人間から、鳥に操作を切り替える処理を実装してみる
どちらもControllerを持ってさえいれば、自在に動く。なので、そのControllerの受け渡し方。Plaer Controller をPossessしているほうが動かせるので、まずはそれを受け渡す。受け渡し方は、マウスを合わせて、近い距離で。マウスを合わせる処理については、Raycastのようなもので対応する。
Eキー対応
まずはIMC, NIA としてInteractキーを作って適用。ログを出すようにして確認済み。
BirdにCollisionを付与する
Line Trace を使う
マウスカーソルからRayを飛ばすのではなく、「カメラからカメラが向いている正面方向へRayを飛ばす」ことがLine Trace。Rayを飛ばして、その物体がBirdにキャストできたらControllerを渡すようにすればよい。
後でLine Trace を通じて鳥かどうかを判定するために、Collisionを付与する。ただ、すでにUCapsuleComponentがあるからこちらにCollison設定を付与する。
Birdに付属しているCollisionの設定を見直す
https://scrapbox.io/files/69e4d9a73caed06dfe0049ad.png
Collision Responsesで設定できる
Ignore: Rayが来ても無視する
レスポンスの種類
Overlap: Rayをすり抜けさせても良いが、通知したというイベントは発火する
Block: Rayを止めて、当たったというイベントを発火する
Trace Responses について
これらはTrace(光線)用のチャンネルとして用意されている。Visibility と、Cameraがある。Line Trace を使うということで、CameraのほうをBlockとして、検知させるのかと考えるかもしれないがそうではない。敵の視界判定や銃の弾用途等に使うならVisibilityが好ましい。Cameraはあくまで、ゲームのカメラが壁や床にめり込まないようにするための設定。なのでこれをBlockとすると、鳥などにSpringArmが干渉するようになる。
VisiblityをBlockにすることで、Eキーを押したときに出すRayを受け取らせるようにする
Eキーを押したとき、Rayを飛ばす
詳細はコードに書いた
Rayでやると鳥にエイムを合わせなければならないので、Sweep(円形)を使って取得するようにすれば近くで押すだけで良くなる
code:cpp
FHitResult HitResult;
FCollisionQueryParams CollisionParams;
CollisionParams.AddIgnoredActor(this);
bool bHit = GetWorld()->SweepSingleByChannel(
HitResult, Start, End,
FQuat::Identity, // 無回転
ECC_Visibility,
SphereShape, // 作成した球体を渡す
CollisionParams
);
↑ 最初に触れた物を取得する処理だが、これだと地面に触れたときに終了してしまうので配列で入れるようにする
code:cpp
for (const FOverlapResult& Hit : OverlapResults)
{
AActor* HitActor = Hit.GetActor();
HitActorがポインタの理由
UE5ではAActorはポインタで扱うルールがある(画面から消えたりすると、nullに設定してくれるようなシステムがあるので)。なので、そちらに従う
鳥側のマッピング調整
現在、BeginPlay()にIMCの登録処理を置いていたが、これをSetupPlayerInputComponentの中に入れる(Controllerが渡されたときに発火する関数なので)。BeginPlayだと渡されたときに発火しないので、バグになる可能性がある
鳥から人間にControllerを渡し返す
Character側のCollision設定も忘れないこと。
鳥が地面をすり抜ける挙動を調整する
CharacterクラスはMovementComponentが足元の自動判定でLandscapeで作った地面などを感知してくれている。Pawnをベースとしたクラスにはそちらがないため、自身で実装する必要がある。
LandscapeのObject Type はWorldStaticなので、こちらをBlockするような調整をする
https://scrapbox.io/files/69e593ca3caed06dfe029977.png
地面に沿ってゆっくり動く挙動にもしたい
Bird側は今、AddActorWorldOffsetで移動させているがこれをやめる(瞬間移動の挙動なので)。Pawn用のMovementComponentがあるのでそれを持たせて、そちらで移動させるようにする。
https://scrapbox.io/files/69e6aa5d3caed06dfe067069.png
既存のComponentとはセクションが区切られている。これはTransformなど空間的な情報を持っているかどうかの違い。MeshなどがUSceneComponentとして扱い、ロジックだけを担当するものをUActorComponentとして取り扱う。
走っている最中に切り替えると、走っているアニメのまま切り変わる
キャラクタークラスが、Controllerが外れた時の関数を持っているのでそちらをオーバーライドして書く形になる
MovementComponent->StopMovementImmediately();
ジャンプしながらControllerを外すと、物理演算が無効になり空中に浮遊したままになる挙動がある
同じく、GetCharacterMovement()->bRunPhysicsWithNoController = true;などで無効化できるのでそうする
近くにいたら、[E]という表示を出す
Unity なら Object に UI を持たせて管理できるが、UE5 の場合は以下の通り
User Interface > Widget Blueprint
User Widgetで、名前を決めて追加する(WBP_InteractPromptなど)
C++に持たせる
WBPの設定
UMG(Unreal Motion Graphics)を使う。
C++への定義
Blank.Build.csにUMGを追加。その後関連ファイルをExplorerから消してビルド。
code:cpp
PublicDependencyModuleNames.AddRange(new string[] {
"Core", "CoreUObject", "Engine", "InputCore", "EnhancedInput", "HairStrandsCore",
"UMG"
});
UWidgetComponent が使えるようになるので、他のものと同じように変数で定義。
テスト
https://scrapbox.io/files/69e71b003caed06dfe08ae2e.png
鳥の真ん中よりちょっと上に出したい。
https://scrapbox.io/files/69e71bc23caed06dfe08b07f.png
Widget Component をDesiredにする。またDetailsタブのslotで、paddingや中央揃えなどもできるのでそうしてやる。
https://scrapbox.io/files/69e71c873caed06dfe08b3a6.png
Bird 側に紐づけたComponent 側でも、Desired Size を有効にしておくとよい。
鳥に近づいたら、このUIが表示されるようにする
鳥側に、表示判定用のSphereComponentを付与する。人が侵入してきたときにUIを表示し、出ていったときに非表示にするような設計を考えていく。Overlap イベントで行う
Overlapイベントを作る
とりあえず写経的に書いてみる。デリゲートとして処理される
code:cpp
// protected に配置
UFUNCTION()
virtual void OnInteractAreaBeginOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult);
UFUNCTION()
virtual void OnInteractAreaEndOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex);
同じく、鳥から人に移るときに[E]を表示させよう
Character に Widget を持たせる
鳥側で用意したWBPでよい。クラス側には感知用のSphere, Widget用の変数を2種類作る
Character クラスはCapsule Componentが Root だが、その中に感知用のコンポーネントを入れていって構わない。Root の Capsule と移動用の Movement がうまくかみ合って Character クラスは出来上がっているので、Capsule が Root になることは必須である。
Character に Overlap イベントを持たせる
WidgetをBPで割り当てる。
関数定義、AddDynamic()で購読、関数の処理を書く。
? 検知しないときは、Collision の設定を疑う。鳥を感知させないなら、ECC_Pawn に Overlap させるようになっているか、BP側でも見直しておくこと。
鳥操作中の時は人間側のWidgetを出し、人間操作中の時は鳥側のWidgetを出す
こういったものは、どう設計していけばいいだろう。実装だけなら if だけでも完結するが、規模の大きなプログラムになったときに簡潔に書くためにはどう設計していくべきか。Unity であればマネージャークラスなどで管理していたが、UE ではどういったものがベストプラクティスになるだろうか
UE ではあまりマネージャークラスなどは用意しないらしい。今回は PlayerController がどんな Pawn を Possess しているかということを調べて場合分けしていく形になる。
鳥に真上上昇処理を作る
Bird だけに追加のアクションを指定する手順として好ましいのは、共通IMCで基本的な操作を定義し、Character / Bird の固有のIMCを作る形。
注意点
Mapping Context の設定の手前で、Subsystem->ClearAllMappings();としておく。Bird -> Character に移った場合にマッピング登録が重複してしまうので、リフレッシュしておく。
メダルに触れる処理を作る
まずは検知から。Overlap イベントだろう。
Collsion 設定について
Bird と Coin が反応しなかった。Coin は pawn の Overlap イベントを持っていたが、Bird の ObjectType設定が WorldDynamic だったので動作しなかった。Bird を Pawn にすると動作した。
それぞれに好ましい ObjectType 設定を考える。Character と Bird は Pawnでよく、コインなどのitemはWorldDynamicとすべき。さらにitemは、メッシュ側はNoCollisionとして、検知用のcomponent側だけで設定しておくと良い。
Collision についてもっと知る
Brd だけでも、Root, Capsule, Skeletal, InteractArea と 4種類あってかなり大変なので紐解く
Root, Capsuleは今回Capsuleを親として考える設計としているので、まず同じ設定になる
Root, Capsule
壁や床と物理的にぶつかる、ぶつからないという設定をする。基本的にはCollision Enabled Query and Physics, 物理干渉したい相手にBlockの設定をする。
SkeletalMesh
見た目。特別な理由がない限りNoCollision とすればいい。ヘッドショットとかあったら調整する必要があるが。
InteractArea
感知判定。Query Only(No Physics Collision)として、感知したい相手を Overlap, それ以外は Ignore とすれば良い。自身のObjectType はWorldDynamicにしておくとよい。大規模開発では、専用のObjectTypeを用意して配置しておくことが多い。
カメラが気になるなら、Camera を ignore にしておこう
実際のゲーム側で反映されないときは、レベル上からその Actor に対して何かしら設定をダッシュボード側で弄ってしまったケースが多い。配置件数が少ないのであれば、再配置してみるとよい。
音を鳴らす
この辺でmp3をインポート。Audio > MetaSounds に、sfx_CoinとしてMetaSoundを用意。
https://scrapbox.io/files/69ec27e6fc4464f697c9b6b3.png
Itemクラスに持たせる。
class USoundBaseがSoundCue, MetaSound の親クラスなのでこちらを.hで定義。.cppでは音を鳴らすために"Kismet/GameplayStatics.h"が必要。include してこちらを使って音を鳴らす。
UIの用意
集めたコインはどこで管理するのだろう。Unity だと manager を作って管理していたが、UE はゲームプレイフレームワークという概念(Actor とか、そういうの)のうちに含まれている。今回はコインを取るBlankCharacterでスコアをカウントする設計で考える。そうするとクラスが肥大化してくるので、実装できたタイミングでパーツを分けていくといい(Player, PlayerHeath, PlayerMovement等という感じでクラスに分けていく)。
コイン追加処理をBlankCharacterに持たせる。
UMG で UI を作る
データ管理、計算はC++側、見た目の配置などはBP側で完結させる設計にする
まずCharacter側でデリゲートを定義して、AddCoin()時にBroadcastする
キャッチするためのWidgetを用意。
Canvas Panel を置いて、その上に貼り付けていく。コイン用テキストボックスにはIsVariableを有効化しておいてBPから値を書き換えられるようにしておく。
https://scrapbox.io/files/69ec3687fc4464f697c9d184.png
右上をDesignerからGraphに変更して通知受け取り処理に切り替える。
Graph側
https://scrapbox.io/files/69eca24cfc4464f697cbc1c0.png
Event Construct: UI が画面生成されたときに一度だけ呼ばれるイベント
GetPlayerControllerで現在のPlayer情報を取得し、キャスト。
BlankCharacterからピンを伸ばして、、BindEventでC++で定義したデリゲートOnCoinCountChangedをバインド。この時のイベントを、UpdateCoinTextという名前で定義。
WBP_HUD 側で用意したテキストボックスのCOinTextに、 イベントの返り値をTextとしてセット。
OnCoinCountChanged.Broadcast(CoinCount);
BlankCharacter側で、HUDを作成し、ビューに加える
https://scrapbox.io/files/69eca3aafc4464f697cbc3f2.png
これで動く。
デリゲートについて詳しく
code:cpp
// #include "BlankCharacter.generated.h" 等の定義... DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnCoinCountChanged, int32, NewCoinCount);
UE の用意する、C++のマクロを呼び出す。
Blueprint空も触れて、複数の関数を同時に呼び出すという仕組みが重いので、UE側でこのマクロを使うことで裏側で必要なC++のクラス定義を自動生成してくれている。
このマクロの役割としては、FOnCoinCountChanged という名前のクラス(構造体)を定義するという役割を持つ。なので関数とかそういったわけではなく、クラス定義をどこでも呼べるようにマクロにしてくれているという感じ。だからこの後、前方宣言していなくても下記のような定義ができるようになっている
code:cpp
UPROPERTY(BlueprintAssignable, Category = "Events")
FOnCoinCountChanged OnCoinCountChanged;
Overlap との違い
今回: デリゲートをそのまま作っている
Overlap: UE の要したOverlapイベントを受け取るための関数を作っている
クリア処理を作ってみる
Unity だとこれも Manager が管理するが。コイン枚数は Player に持たせたが、クリアフラグやゲームルールまで持たせるのは非推奨。Player は 操作が離れたり死亡してDestroyされたときなど、フラグが消滅してしまう。UE ではGameModeを使う。
GameModeBaseをもとに、C++クラスを作成。BlankGameMode。
※ちなみにこれはデフォルト操作対象を球から鳥、人間に変えたりするあの設定のこと。
このクラスの Public でクリアチェック関数を用意して、コイン取得時に呼び出す。(Level に GameModeを設定する場所があり、コイン取得を行う関数ではその空間から本関数を取得してチェックする動き)
クリアできたら Widget を出す
Widgetを用意して、GameModeのBPを開いてWidgetを紐づける。
code:cpp
protected:
// BlueprintImplementableEvent: C++ で呼ぶが、「CLEAR」などUIを出す処理はBP側でするということ
UFUNCTION(BlueprintImplementableEvent, Category = "Rules")
void OnGameCleared();
こんな感じで定義しておくと、BP側のEventGraphの右クリックで呼び出せる。
https://scrapbox.io/files/69ecbc25fc4464f697cbe989.png
この関数が呼ばれたときの処理をBPで書いていくことになる
https://scrapbox.io/files/69ecbd70fc4464f697cbed65.png
呼ばれたときに、今回作ったウィジェットをviewport(画面)に出す。ウィジェットはGameModeに持たせなくてもOK.
カメラの設定
個別設定の場合
code:cpp
Camera = CreateDefaultSubobject<UCameraComponent>(TEXT("Camera"));
Camera->SetupAttachment(SpringArm);
Camera->bConstrainAspectRatio = true;
Camera->AspectRatio = 16.0f / 9.0f;
こんな感じでできる。ただし、CharacterとBirdに対して適用したいので、今回は違う手法で行う
全体設定の場合
手順軽く
自身でPlayerControllerクラスを用意
自身でPlayerCameraManagerクラスも用意
ここで、アスペクト比などを決定
自作したPlayerControllerクラスで、そのカメラマネージャーを使うように指定
あらかじめ作っていたBlankGameModeクラスに、自作したPlayerControllerクラスを使うようにBP等で指定
UI側をスケーリングに対応する
Scale Boxを親として配置。Scale to Fitの項目を設定。
Size Box を子要素として入れる。Width Override, Height Overrideに1920 * 1080 を設定
その子要素としてCanvas Panelを配置してUIを作っていく形になる。
BlankCharacter コードの分割
Unityだと、Player, PlayerMovementといったように細かなロジックを自作componentに切り分けて管理する。しかしUEではMove周りを分割など、そういったことはあまりしない。ただし現在コインのカウント処理がCharacterがある。これは分割できるので、実際に切り出してみよう
Actor Component を親としてC++クラスを用意。
https://scrapbox.io/files/69eeb737fc4464f697cfa21d.png
Characters フォルダが今回あるが、コンポーネントはコンポーネントとしてフォルダを作成しておくほうが一般的。なので今回、分割しておく。キャラがコインについての処理を持つという設計だが、将来的に敵がコインを落とすなど、そういったときも本Components を持つケースがありうるから。
UCoinComponentとした。
フォルダ構成とか作り間違えたので、Binaries, Intermedimate, Savedを消して作り直す(slnは消さなくてよい)。ueprojectからregenerate。slnからvsを開いてビルド。
間違えた。Uとかの接頭辞はUE側で勝手につけてくれるので、設定時にこちらで付与する必要は無い。消しとく。
疎結合パターンで考える
キャラがCoinComponentを持つ。コインが感知したとき、感知者がCoinComponentを持っているかチェック。持っていたら、そちらを使ってAddCoin()する(このようにすると、CoinComponentを持っているかどうかを見るようになるので楽になる)
ゲームクリア判定もデリゲートで行う
CoinComponentがGameModeを呼んで判定しているが、GameModeが勝手に聞き取って判定するようにする
GameMode 側で、開始時にPlayerCharacterを呼び、そのCoinComponentに購読した。
code:cpp
void ABlankGameMode::BeginPlay()
{
Super::BeginPlay();
if (ABlankCharacter* BlankCharacter = Cast<ABlankCharacter>(UGameplayStatics::GetPlayerCharacter(this, 0))) {
if (UCoinComponent* CoinComponent = BlankCharacter->FindComponentByClass<UCoinComponent>())
{
CoinComponent->OnCoinCountChanged.AddDynamic(this, &ABlankGameMode::HandleCoinCountChanged);
UE_LOGFMT(LogTemp, Warning, "BlankGameMode: successfully bound to CoinComponent");
}
}
}
// デリゲート用 Broadcast時に自動で呼ばれる関数
void ABlankGameMode::HandleCoinCountChanged(int32 NewCoinCount)
{
CheckWinCondition(NewCoinCount);
}
ゲーム開始時の挙動
GameModeの初期化
GameModeがPlaerControllerを生成
GameModeが、DefaultPawnClassに設定されているデータをベースに生成。Possessさせる。
その後、レベル上のBeginPlay()が呼ばれる
上記の動きになるので、GameModeのBeginPlay()が呼ばれる頃にはキャラからコンポーネントが取れるようになっている。
補足として、キャラがリスポーンするゲームなどの場合は、CoinComponentの参照が切れることとなる。Player側のBeginPlayで、再登録を促すような処理を書かなければならない。
タイトル画面を作って、シーンの切り替えを実現する
Unityではシーンと呼ぶが、UEにおいてはLevelsと呼ぶ。タイトル用のレベル、ゲーム本編用のレベルなどを用意。タイトルにはタイトル用のGameModeを用意して、どのGameModeでどのLevelsを動かすかという考え方。
レベルL_Titleの用意
Level を作って天球などを配置。
今後操作するなら、Project Setting などでDefault map を調整しておくとよい。
タイトル画面用のウィジェットの用意
C++クラスを用意する。UserWidgetを親として、TitleWidgetを用意
code:.h
protected:
virtual void NativeConstruct() override;
UFUNCTION()
void OnStartButtonClicked();
// UMG 上で、StartButton という名前で、Button型のものと紐づける
// 存在しなければBP側でコンパイルが通らない
UPROPERTY(meta = (BindWidget))
UButton* StartButton;
UPROPERTY(EditAnywhere, Category = "LevelTransition")
FName NextLevelName = FName("WorldMap");
protectedに配置する意図
TitleWidgetをベースに、WBP_TitleWidgetを作ることになるはず。その場合、WBP側は子クラスとして振る舞うことになる。その際に不都合が起こらないようにしている(BP側でNextLevelNameを書き換えたりと)
NativeConstruct
BeginPlay等と同じく、UIが画面生成時に1度だけ呼ばれる初期化関数。
code:.cpp
void UTitleWidget::NativeConstruct()
{
Super::NativeConstruct();
if (StartButton)
{
StartButton->OnClicked.AddDynamic(this, &UTitleWidget::OnStartButtonClicked);
}
}
void UTitleWidget::OnStartButtonClicked()
{
UGameplayStatics::OpenLevel(this, NextLevelName);
}
OnClicked.AddDynamic
UE側がUButtionで用意してくれているデリゲートを使っている。クリックしたら指定の関数が呼ばれるように設定。
画面でUIを作る。
UPROPERTY(meta = (BindWidget))としたなら、配置したButtonの名前をここで指定した変数名と一致させることを忘れない。
GameModeBaseを親として C++ クラスを用意
code:TitleGameMode.h
UCLASS()
class BLANK_API ATitleGameMode : public AGameModeBase
{
GENERATED_BODY()
protected:
UPROPERTY(EditAnywhere, Category = "UI")
TSubclassOf<class UTitleWidget> TitleWidgetClass;
public:
virtual void BeginPlay() override;
};
TSubclassOf<class UTitleWidget> TitleWidgetClass
TSubclassOf: 指定したクラス(または派生クラス)のUClassだけを格納できるテンプレートクラス。これからタイトルUIをCreateWidget()を使って作るときに使うが、その時の土台とするクラスは何にするかという情報を入れている
? あらかじめUTitleWidget* TitleWidget として持たせておくことはできないのか
現在はCreateWidgetでメモリ確保→AddViewPortで画面表示→レベル遷移で自動破棄の流れ。UnityではデフォルトでUIを配置してそちらを変数に持たせることが出来るがUEではできない。なので、必ずどこかのタイミングでC++やBPで生成する必要がある。
code:TitleGameMode.cpp
void ATitleGameMode::BeginPlay()
{
Super::BeginPlay();
APlayerController* Controller = GetWorld()->GetFirstPlayerController();
if (Controller && TitleWidgetClass)
{
UTitleWidget* TitleWidget = CreateWidget<UTitleWidget>(GetWorld(), TitleWidgetClass);
if (TitleWidget)
{
TitleWidget->AddToViewport();
// 入力モードをUI専用にし、マウスカーソルを表示させる
FInputModeUIOnly InputMode;
InputMode.SetWidgetToFocus(TitleWidget->TakeWidget());
Controller->SetInputMode(InputMode);
Controller->bShowMouseCursor = true;
}
TitleWidgetを問題なく作ることが出来たら、画面に表示 + 各種設定。
こちらをベースにBP_TitleGameModeの作成。
TSubclassOf<class UTitleWidget> TitleWidgetClass に、何を基としてウィジェットを作るかの割当を行っておくこと。先ほど作ったWBP_TitleWidgetを基とするように設定(CreateWidgetで、このUIができる)
https://scrapbox.io/files/69f00b2afc4464f697d364d6.png
L_TitleのWorld Settingsで、BP_TitleGameModeを使うように指定する。
遷移後、プレイヤーが動かない
TitleGameMode側でFInputModeUIOnly InputMode;として設定し、その設定が引き継がれている。BlankGameModeのBeginPlayで調整すればよい
タイトル画面のカメラ比率調整
BlankPlayerController.cppで PlayerCameraManagerClass を指定しているので、TitleGameModeでも、このコントローラを使って制御するように設定すればOK
ゲーム性を考える: 鳥時
鳥から中断したとき、元の位置に帰るようにする。鳥に初期位置を覚えさせて、コントローラを渡したときに元の位置に戻しておけばよい
code:cpp
FTransform DefaultSpawnTransform;
this->SetActorLocation(DefaultSpawnTransform.GetLocation(), false);
こんな感じ。FTransform には, 位置、回転, Scale の3つの情報が入っているので、Location 指定時にはGetLocationでFVectorの形にして渡さなければならない。
となると、[E]を押したとき、有無を言わさず戻してしまっても構わないかもしれない。
Bird に Controller を渡す対象のキャラクターを持たせる
これは、キャラからBirdに処理を渡すときにセットしてやればよい。今回は下記のように設計した
code:cpp
# Bird 側
void ABird::SetBlankCharacter(ABlankCharacter* Character)
{
BlankCharacter = Character;
}
# Character 側
if (ABird* HitBird = Cast<ABird>(HitActor))
{
HitBird->SetBlankCharacter(this);
このケースでも良いが、今後車や別の物体に移るとき、if がその数だけ増えることになるためメンテナンス性が下がる。よりきれいにするなら、Controller がその役割を担うとよい。
Controller に持たせてみよう
code:cpp
UPROPERTY()
TObjectPtr<APawn> OriginalPawn;
APawn* Pawnとしても良いが、UE5 では↑の実装が好ましい
code:bird.cpp
UPROPERTY(VisibleAnywhere)
UCapsuleComponent* Capsule;
UPROPERTY(VisibleAnywhere)
USkeletalMeshComponent* BirdMesh;
なので本当は、今ならこの辺もTObjectPtrで定義するほうがいい。
こんな感じで移せる
看板を読む、画面にテキストをぺぺぺ...と小刻みに出す機能を作ってみる
code:設計イメージ
・看板を作る
・Eキーを押すと、感知する
・変数として持たせていたテキストボックスから、文字が出てくる...
ぺぺぺ...と文字を出すのは後から。まず簡易的な形から進める。Actorを親としたC++を作成。Mesh 、インタラクトエリア、テキストボックスを持たせる。
看板のMeshが大きすぎるとき
Scaleを調整するのではなく、Static Mesh 自体を弄って小さく設定するのが良い。
https://scrapbox.io/files/69f54d888dc79f02a28559d3.png
Build Scaleを調整して、Apply Changesで対応。BlenderとUEのサイズ単位が異なるのでこういうケースがあるらしい。
インタラクトキーが表示 / 非表示となる処理
Overlapイベントに応じて設定していく。InteractAreaに、AddDynamicで定義したOverlap時に発生する関数を設定。
Eキーに役割を複数持たせる
現在キャラクターでEを押すと、Birdに憑依する。ただし、看板が近くにある場合は看板を読むという別の処理を走らせたい。つまりEキーに役割を複数持たせたい(今後宝箱を開けるとか、そういったものも)。Eキーを押したとき、Overlapしたものが看板ならA, 鳥ならBの処理...とすると、ループが都度増えることとなる。こちらを防ぐためにはインターフェースを用いるとよい。
C++クラスから、Unreal Interfaceを親として作成。今回弄るのは.h側だけ。publicに純粋仮想関数のガワだけ用意して、そちらは継承したクラス側で実装する。
設計イメージ
code:Interactable.h
public:
virtual void Interact(APawn* Interactor) = 0;
code:Signboard.h
class BLANK_API ASignboard : public AActor, public IInteractable
{
public:
virtual void Interact(APawn* Interactor) override; // インターフェース側で定義したもの
code:Bird.h
class BLANK_API ABird : public APawn, public IInteractable
{
public:
virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;
BlankCharacter側でEを押したときに、コリジョンに触れた対象からこのインターフェースを抜き取って処理する。
code:BlankCharacter.cpp
// void ABlankCharacter::ExecInteractive()
if (bHit)
{
for (const FOverlapResult& Hit : OverlapResults)
{
AActor* HitActor = Hit.GetActor();
// 対象に Interface があればそれを実行
if (IInteractable* InteractableTarget = Cast<IInteractable>(HitActor))
{
InteractableTarget->Interact(this);
return;
これで、鳥ならBirdで実装された処理が、看板ならSignboardで実装された処理が実行される。
インタラクトで画面にテキストを出す
メッセージUIは、Webアプリにおけるモーダルのような設計イメージ。開閉状態はSignboardではなくControllerに管理してもらう。
UI本体のクラスを用意
文字列を受け取って引数で渡されたテキストをUIにセットする関数を用意する
Controller
開閉状態をフラグで管理
UIをBeginPlayで生成し、その後 visible / hidden で切り替えるような処理の用意
TSubclassOf<UBlankDialogueWidget> DialogueWidgetClass;で定義したクラス設計書を持たせるためにBP Classとしてコントローラを作る(未作成だった)。TitleGameMode, BlankGameModeに割り当てていたコントローラをこのBP Classに変える
ABlankCharacterのインタラクトで、そのフラグに応じてEキーの処理分岐をする
Controllerに持たせた開閉状態で分岐させた
テキストを開いているときは、WASD / Jumpを動かさなくする
Moveなどの処理中にControllerで持たせたUIフラグをチェックさせれば解決できそうだが、これはベストプラクティスとは言えない。好ましい設計パターンであるInput Mode の切り替えで対応するか、用意されている関数で対応する
code:BlankPlayerController.cpp
void ABlankPlayerController::ShowDialogue(const FText& Message)
{
if (DialogueWidgetInstance && !bIsDialogueOpen)
{
DialogueWidgetInstance->SetDialogueText(Message);
DialogueWidgetInstance->SetVisibility(ESlateVisibility::Visible);
bIsDialogueOpen = true;
if (ACharacter* CastCharacter = Cast<ACharacter>(GetPawn()))
{
CastCharacter->GetCharacterMovement()->StopMovementImmediately();
FlushPressedKeys(); // 押しっぱなし判定などもリフレッシュ
}
SetIgnoreMoveInput(true); // テキスト表示中は移動操作を無効化
SetIgnoreMoveInputで無効化して、閉じるときに有効に戻せばよい
Jump等の関数について
こういった単発アクションに関しては、Controllerに持たせている開閉フラグを見るようにしてやればよい
code:cpp
void ABlankCharacter::TryJump()
{
if (ABlankPlayerController* PC = Cast<ABlankPlayerController>(GetController()))
{
if (PC->IsDialogueOpen())
{
return;
}
}
// Super::Jump(), ACharacter::Jump() でもよい
Jump();
}
こっちをEnhanced Input にバインドするとよい
複数のメッセージを格納できるようにする
FTextで持たせていたデータを、配列として持たせる。Controller に、現在何ページ目を呼んでいるかを記録する変数を持たせる(index)。Eキーを押したときに、配列の最後であるかどうかを判定させてページめくり or 閉じる という分岐を持たせる。
配列と要素の追加
code:cpp
UPROPERTY(EditAnywhere, BlueprintReadWrite, meta = (AllowPrivateAccess = "true"))
TArray<FText> MessageTexts;
MessageTexts0 = INVTEXT("1ページ目のテキストです。"); ビルドは通るがUEがクラッシュする。C++では空配列に対してインデックスで指定するとメモリ不正アクセスになる。
Add()を使って入れること、取り出すときは空配列チェックをすること。
code:cpp
# 格納時
MessageTexts.Add(INVTEXT("1ページ目のテキストです。"));
# 取出し時
// Messages.Num() > 0 でもよいので、空配列かどうかをチェックすることを忘れない
if (Messages.IsEmpty())
{
UE_LOG(LogTemp, Error, TEXT("ABlankPlayerController::ShowDialogue(): メッセージ配列が空です。"));
return;
}
DialogueWidgetInstance->SetDialogueText(Messages0); ページ送り
Controller に 現在のウィジェットのテキスト配列、index を持たせる
テキスト配列がindexを超えたらウィジェットを閉じる。そうでなければ次のテキストをセットするというような処理にすればよい。
タイピング演出
細かく文章が出てくる演出(プレゼンテーション)。文字列を操作して時間経過とともに文字が増えていくという形になるので、これはUBlankDialogueWidgetで完結させる。
しくみ
テキスト全文をあらかじめ持っておく
現在何文字目まで表示したかを、index として保持する
タイマーで、一定間隔で特定の関数を繰り返し呼ぶようにする
関数では文章の左端から、index の数だけ文字を切り出して TextBlock にセットする。文字列と index が一致したら終了。
作ってみる
code:.h
public:
void SetDialogueText(const FText& InText);
bool IsTyping() const { return bIsTyping; }
void SkipTyping();
protected:
// このC++をベースにBPを作ったとき、同名コンポーネントが配置されていない場合エラーにするという制約
UPROPERTY(meta = (BindWidget))
TObjectPtr<UTextBlock> MessageText;
private:
void UpdateTyping();
UPROPERTY(EditAnywhere, Category = "Dialogue")
float TypewriterSpeed = 0.05f; // 切り取るスピード
FTimerHandle TypewriterTimerHandle; // タイマーハンドル(後述)
FString FullString; // テキスト全文
int32 CurrentCharIndex = 0; // 現在切り取って表示させている文字数
bool bIsTyping = false; // 切り取り加工中かどうかのbool
code:cpp
void UBlankDialogueWidget::SetDialogueText(const FText& InText)
{
// BP 側で配置した TextBlock の有無チェック
if (!MessageText) return;
// タイマーを二重起動する処理を防ぐ処理
// TypewriterTimerHandle は、この先設定するタイマー処理のハンドル
// ゲームエンジンの時間管理者(TimerManager)を呼び出し、タイマー処理をリセットしている
// インタラクト処理を連打したときに二重起動する処理などを防ぐ。
GetWorld()->GetTimerManager().ClearTimer(TypewriterTimerHandle);
// 切り取り処理
// FText は 1文字ずつ切り出せないので、FString にして処理を行う(UEのUI開発の基本)
FullString = InText.ToString();
CurrentCharIndex = 0;
bIsTyping = true;
MessageText->SetText(FText::GetEmpty()); // 最初の空っぽ状態をセット
// 指定の関数を、指定の秒数ループで実行するという指示を行う
// このときに TypewriterTimerHandle にタイマーに関する情報が保持されるので、
// 止めたいときは ClearTimer(Handle) というように指定すればよい
GetWorld()->GetTimerManager().SetTimer(
TypewriterTimerHandle,
this,
&UBlankDialogueWidget::UpdateTyping, // この関数を
TypewriterSpeed, // この間隔で呼ぶ
true // ループ有効
);
}
void UBlankDialogueWidget::UpdateTyping()
{
CurrentCharIndex++;
// index 分だけ文字列から切り出して格納。FString で加工したデータを FText に戻して SetText()
FString CurrentString = FullString.Left(CurrentCharIndex);
MessageText->SetText(FText::FromString(CurrentString));
// すべて表示出来たら、SkipTyping (実質表示終了を表す関数)に移行
if (CurrentCharIndex >= FullString.Len())
{
SkipTyping();
}
}
void UBlankDialogueWidget::SkipTyping()
{
// タイマーを停止し、全文を一気に表示する。入力中の処理を止める
GetWorld()->GetTimerManager().ClearTimer(TypewriterTimerHandle);
MessageText->SetText(FText::FromString(FullString));
bIsTyping = false;
}
キャラとコントローラ側
code:cpp
# Character
void ABlankCharacter::ExecInteractive()
{
if (ABlankPlayerController* PC = Cast <ABlankPlayerController>(GetController()))
{
if (PC->IsDialogueOpen())
{
PC->ProceedDialogue();
return; // 無いと、ダイアログを閉じつつインタラクトが発生してループしてしまう
code:cpp
# Controller
void ABlankPlayerController::ProceedDialogue()
{
if (!bIsDialogueOpen || !DialogueWidgetInstance) return;
// 文字がぺぺぺと出ている最中のパターン
if (DialogueWidgetInstance->IsTyping())
{
DialogueWidgetInstance->SkipTyping();
return;
}
// 既に全部の文字が表示されているときパターン(次のページ or 閉じる)
CurrentDialogueIndex++;
if (CurrentDialogues.IsValidIndex(CurrentDialogueIndex))
{
}
else
{
CloseDialogue();
}
}
これで、文字を切り出している間にEを押したら全文表示、そうでなければ次ページ or ウィジェットを閉じるという処理が実現できる。
文章を改行できるようにする
code:Signboard.h
UPROPERTY(EditAnywhere, BlueprintReadWrite, meta = (AllowPrivateAccess = "true", MultiLine = "true"))
TArray<FText> MessageTexts;
MultiLine = "true"を付与すると、BP 上で複数行入力できるようなボックスになる。
C++で改行する場合は、改行コードを入れる。
code:cpp
MessageTexts.Add(INVTEXT(
"1ページ目のテキストです。\n改行は Shift + Enter でできます。\n1ページ目のテキスト。1ページ目のテキスト。"
));
https://scrapbox.io/files/69f9f1042dc79442f0d5f572.png
看板がカメラにぶつかるのを止める
看板のMesh の Collision の Camera を Ignore とした。
文章が開いているなら、Enter, 左クリックなどでも文字を進められるようにする場合
Eですべて処理を進めているが、テキスト送りなどだけは別のキーでも実行できるようにならないか(インタラクト自体は従来通り、Eだけ)。IMC の 動的切り替え、及び優先度機能を使って実現する。
実装
NIA用意。digital bool を受けとるようにする
IMCにE, Enter, 左クリックなどを配置して、上記で作ったInputActionを配置
Characterにマッピングを持たせる
UInputActionのポインタ変数と、紐づけるための関数を用意(Move()とかそういうの)
SetupPlayerInputComponentの中で、nullptr チェックして紐づける
Controller 側の調整
マッピングUInputMappingContextを変数として持たせる
ダイアログが開かれたときに、プレイヤーのマッピング優先度を調整する
Character, Controller にIMCを持たせる。
Character には ダイアログを進めるというIA(Value Type のみ定義されたもの)を持たせる。この IA が入力されたときどうするかという関数を用意して、BindAction で紐づける。ここでは「ダイアログを進行する」という処理を持たせる。
code:cpp
void ABlankCharacter::RequestProceedDialogue()
if (ABlankPlayerController* PC = Cast<ABlankPlayerController>(GetController()))
PC->ProceedDialogue();
Controller では、ダイアログ表示時のマッピングを管理する IMC を持たせる。
Signboard::Interact()実行時、コントローラのShowDialogue()が呼ばれる。なので、その ShowDialogue 時に所持している IMC を Subsystem に Priority を 10 として重ね掛けする
code:cpp
if (
UEnhancedInputLocalPlayerSubsystem* Subsystem =
ULocalPlayer::GetSubsystem<UEnhancedInputLocalPlayerSubsystem>(GetLocalPlayer())
)
{
if (DialogueMappingContext)
Subsystem->AddMappingContext(DialogueMappingContext, 10);
これで E キーが押されたときも Character の ExecInteract()ではなくRequestProceedDialogue()が発動するようになる。
CloseDialogue()時に、その重ね掛けを解除する
code:cpp
if (
UEnhancedInputLocalPlayerSubsystem* Subsystem =
ULocalPlayer::GetSubsystem<UEnhancedInputLocalPlayerSubsystem>(GetLocalPlayer())
)
{
if (DialogueMappingContext)
Subsystem->RemoveMappingContext(DialogueMappingContext);
マップを作ってみる
マテリアル集める
自作のマテリアルを用意
Materials で、 M_Landscapeとして作成。
Details から Fully Roughで、光沢をなくす(地面なので)
ブレンドレイヤーで割り当てる
Landscape mode で綿密な設定をするにはどうするか
閉鎖空間から始めて、広い世界に飛び出させる(ピカーみたいな演出もあるといいけど)
プレイヤーが画面外に落ちるのをケアする
World Settings > Kill Z で、この座標を下回るとDestroy処理が走るように設定できる。それで落ちた時にDestroy時の処理を設計するとよい
プレイヤーに行ってほしくない場所について
トンネルなどの用意
アセットを持ってきて、Static Mesh > Details > Collision Complexity から Use Complex as simple とする
テクスチャなども調整する
点滅する不具合
Build Scaleを1.0 -> 3.0 など引き延ばしてしまうと不備になる可能性がある。その場合はActor 側のscaleを調整して対応する。
Eキーを押せるsignboardもどきの物を作る
Signboard をベースに、メッシュなしで作る。ドアとかに割り当ててしゃべらせる。
状況に応じてテキストを変える
エラー解消
Blueprint Runtime Error: "Attempted to access missing property 'OnCoinCountChanged'. If this is a packaged/cooked build, are you attempting to use an editor-only property?". Node: Bind Event to On Coin Count Changed Graph: EventGraph Function: Execute Ubergraph WBP HUD Blueprint: WBP_HUD
WBP_HUD の Bind Event to On Coin Count Changed という BP のイベントが動いていない。
VS でしっかりビルドすると治った
草木の配置
Foliage Mode で対応する。緑の線のアセットが必要だが、素材に 水色(Static Mesh)しかない。その場合は自身でstatic mesh から foliage を生成することも出来るので、そうしてやる。
パフォーマンス調整
Project Setting > Rendering > Shadow Map Method をShadow Maps とする。Anti-aliasing も TAA にしておくとよい
Foliage の木にも当たり判定を付与する
Foliage としたものをblockallとする。マップの既存のものには反映されないかも。それだけ選択して、shift をおしっぱで塗るとそれだけ消せるので作り直しておくと良い
木をまっすぐ置く
https://scrapbox.io/files/69fd6eca4a47cec3dce5ad9a.png
https://scrapbox.io/files/69fd720b4a47cec3dce5b828.png
Align to Normalのチェックを外して、zoffsetを少し下げると、斜面でもまっすぐ立つ。
世界全体の明るさの調整
Post Process Volume で明度など調整できる。Place Actor から配置
プレイヤーが遠くに行くと消失する
UE には、World Bounds Checks という、特定の領域から外に出たものを、エラーとして弾いて強制削除するという機能が有効になっている。キャラが歩いた時、その境界を超えると失敗する。
下記を外す。
https://scrapbox.io/files/69fd791d4a47cec3dce5cdeb.png
取得したコインが、離れてもう一度近づくと再生成される
Destroy されたコインは、マップが再読み込みされると再度スポーンする。これは、コインに独自IDを持たせ、Destroy したときにWorld を管理しているactor に書き込む。そうすることで、コインのBeginPlay()時にそのIDを持っているかチェックして、即時にDestroyするように設計すればよい。今回は既存のままで。
状況に応じて、話す内容を変える
Enumで対応したが、もっといい設計がある。次の作品で。
プレイヤーの身体能力を高める
CharacterStatComponentを作って割り当て、そちらを呼び出して設計する。
暗転処理
他のウィジェットと同じく、UMG(Unreal Motion Graphics)で実現する。
UserWidgetを親としてクラス作成。
Controller で作ったウィジェットを呼び出す。
既存のゲームクリア処理と調整
コイン取得時にデリゲート。BlankGameMode のHandleCoinCountChanged で、 CheckWinCondition が走る。指定の条件通りだとOnGameCleared が走る。 BP側で、ウィジェットを表示するような処理。BP側の実行ピンだけ切っておく。
暗転アニメ
https://scrapbox.io/files/69fe8d035c3c134ed42712b6.png
https://scrapbox.io/files/69fe961a5c3c134ed427388a.png
こんな感じでRender Opacity で キーフレームを打って調整していく
リトライボタンに、リトライ機能を割り当てる
山作って、コインいっぱい作る
暗転してクリア
10枚とったらウィジェットが出る仕様を消す
アプリケーションとしてビルドする
.exeとして出勅する処理を、UE では Packaging と呼ぶ。
Project Settings > Project > Packaging
Build Configuration で Development から、 Shipping などにする(デバッグ用処理が消える)
Platforms > Windows > Package Project でパッケージング。
https://scrapbox.io/files/69fec6f45c3c134ed427a562.png
40分くらいかかって大変
白い二重丸を消す
None にする
https://scrapbox.io/files/69fed5fd5c3c134ed427c055.png
レベル遷移がしない
Cook の対象からそのレベルに漏れがある。C++ や BP内部で、FName でマップ指定して開いているときに発生する。
Packaging の List of maps to include in a packaged buildから調整ができるが、これはヒューマンエラーに繋がるので非推奨。レベル参照は、別途異なる設計で対応すべき。
code:cpp
UPROPERTY(EditAnywhere, Category = "LevelTransition", meta = (AllowedClasses = "World"))
TSoftObjectPtr<UWorld> NextLevel;
void UTitleWidget::OnStartButtonClicked()
{
if (!NextLevel.IsNull())
{
// ↓ ではなく
// UGameplayStatics::OpenLevel(this, NextLevelName);
// ソフト参照を使ってレベルを開く
// Packaging したとき、Cookerがアセットを検知できる)
UGameplayStatics::OpenLevelBySoftObjectPtr(this, NextLevel);
}
}
こんな感じ
Packaging したあと、まぶしすぎたりテクスチャがおかしい
Packaging 中のログを見る
Failed to compile Material for platform PCD3D_SM5, Default Material will be used in game.
Function MF_CharacterMaterialOverrides: (Node TextureSampleParameter2D) Param2D> Sampler requires VirtualTexture
Enable Virtual Texture を有効化
https://scrapbox.io/files/69fee8c15c3c134ed427e8e6.png
まぶしい
Post Process Volume の Exposure を調整する。
テクスチャ切れを起こす
これば別のProject に分けてがっつり直す練習をしたほうが良さそう...
ToDo
ジャンプをソウルシリーズみたいにきびきびさせる✅
鳥が地面をすり抜ける✅
近くにいたら、[E]というような表示を出して分かりやすくする✅
メダルに触れると音が出る処理✅
UI (今何枚かなど)✅
カウントするためのManagerをどう管理するか✅
簡易タイトル画面✅
簡易リザルト✅
テキストを出す機能を作ってみる✅
関数名とかにつけるUEのマクロをもっと明確に理解する✅
UFUNCTIONとかつけてない関数もあるけど、どう区別していくのかとか
マップ作る
タイトル, クリア
画面をもうちょっと良い感じにする
浮いてるものを乗り継いだりできるように
ゲーム性
メダルを集める
めちゃくちゃでかいのとかあったら面白い
時間内にコインをたくさん集めていくみたいな?
鳥に憑依すると、広いステージを操作できるがメダルは取れない
E を押すと、鳥と切り替えられる感じ
人間になると、メダルが取れる
鳥になって探そうという感じで
作成後
テクスチャ切れを直す
M_HP_Tree_Trunk Material /Game/HighPoly_Tree_Model/Materials/M_HP_Tree_Trunk.M_HP_Tree_Trunk was missing the usage flag bUsedWithInstancedStaticMeshes. If the material asset is not re-saved, it may not render correctly when run outside the editor. Fix
M_HP_Tree_Leaf Material /Game/HighPoly_Tree_Model/Materials/M_HP_Tree_Leaf.M_HP_Tree_Leaf was missing the usage flag bUsedWithInstancedStaticMeshes. If the material asset is not re-saved, it may not render correctly when run outside the editor. Fix
Map check complete: 0 Error(s), 2 Warning(s), took 5.02ms to complete.
https://scrapbox.io/files/69ffc3225c3c134ed4297b85.png
アセットをContent Brouser から消して、入れ直して、メッシュを割り当て直すのが手っ取り早かったりする
Map check complete: 0 Error(s), 0 Warning(s), took 3.913ms to complete.
M_HP_Tree_Trunk Material /Game/HighPoly_Tree_Model/Materials/M_HP_Tree_Trunk.M_HP_Tree_Trunk was missing the usage flag bUsedWithInstancedStaticMeshes. If the material asset is not re-saved, it may not render correctly when run outside the editor. Fix
M_HP_Tree_Leaf Material /Game/HighPoly_Tree_Model/Materials/M_HP_Tree_Leaf.M_HP_Tree_Leaf was missing the usage flag bUsedWithInstancedStaticMeshes. If the material asset is not re-saved, it may not render correctly when run outside the editor. Fix
キャラの参照が切れる挙動
import し直して、Project Setting も見直したがうまくいかず
https://scrapbox.io/files/6a16f14f65e24daf8cbc603b.png
https://scrapbox.io/files/6a16f15c65e24daf8cbc6049.png
マテリアルではなく、Texture が悪かった
↓はもう直してしまったが、Convert to Virtual Textureという項目がある。そちらで全てリプレイス
https://scrapbox.io/files/6a16f17265e24daf8cbc6069.png
ノイズ
↓画面右上にノイズが出ていて、ゲーム中もたまに出る
https://scrapbox.io/files/6a17a06265e24daf8cbd45c9.png
Nanite(ナナイト)を切る。切った後は Apply Changes を忘れないこと。