UE で物理の力だけで泳ぐキャラクターを作る

第23回UE5ぷちコン|株式会社ヒストリア で以下の作品を提出しました。

www.youtube.com

このゲームは水中ダイビングゲームですが、そのジャンルによくある「海中の美しさ」であったり「海中をスイスイ泳ぎ回る気持ちよさ」みたいなものは一切ありません。

ただ、「泳ぐ」だけに焦点を当てたゲームになっています。
操作が極まってくると気持ちよく泳げる時もあります😵

キャラクターの動きの全体

画質も悪いので分かりにくいのですが、以下のようにキャラクターの周囲にあるボール群に対して、キャラクターの物理アセットが触れることでボールが移動しそれがまた戻ってきています。

周囲のボール群が、キャラクターの移動によって移動(実際には端の方から追加・削除)しているので、余計に動きが分かりにくいですね。。

youtu.be

  • キャラクターは Simulate Physics 状態である、かつ重力も極小さい
  • キャラクターは手足を関節 (肩関節、股関節) を軸にして回すことしかしていない
  • 手足には末端に大きめの物理アセットが付いている
    • 足であればダイバーのフィンを想像してもらうとわかりやすいです
  • キャラクターの周囲には物理的に特定座標に束縛されたボールが配置され(続けて)いる
  • ボールは物理的に押すことができる、また押した際にまた元の位置に戻る力が働く
  • キャラクターの物理アセットがボールを押すことでほぼ無重力状態のキャラクターはその反動で進む
  • 同時に、戻ってこようとするボールにも押されて前に進む

動きの全体としては上記のような感じでしょうか。

キャラクター

基本のポーズ

アニメーション画面 Control Rig 編集時

基本のポーズというか、今回のキャラクターはアニメーションBP は使っておらず、この 1 ポーズ 1 フレームだけのアニメーションアセットを使っています。

このポーズも、Quinn_Simple の Control Rig に対して

  • 両手をバンザイさせ
  • 身体を水平にし
  • 足のつま先を後ろに流すように

ということだけをしています。

ここであまり水泳っぽいポーズやアニメーションにこだわっていないのは、私の技術的な部分も大きいですが、それとは別に手足を関節 (肩関節、股関節) を軸にして回すことに関して、極力シンプルに動かすためという理由もあります。

物理アセット

最初に見てもらった動画を別の方向から見ると以下の左図のようになっています。
右の図は物理アセットの編集画面で同じ角度で見たものです。

PIE 実行時 物理アセット

ボールを押す(手の場合は「かく」方が適切かもしれませんが)ために手足には大きめの物理アセットを付けています。
本当はもっとシンプルな形状にしたかったのですが推進力を得るために大きくしています。

腰にも一箇所だけ板状の物理アセットを付けています。これは重力の小さいキャラクターはそのままだと浮き続けてしまうので、ボールに当たって浮かないようにするために付けています。

物理コントロール

このキャラクターはアニメーションBPを使っていないとしましたが、動きには全て Physics Control を利用しています。

後述の参考にも入れましたが、今回は Content Example のコードをベースにキャラクターを設定しています。

/Game/ExampleContent/PhysicsControl/Blueprints/BP_PhysicsControlCharacter.BP_PhysicsControlCharacter

WorldSpace/ParentSpaceControlData の数値は多少いじっていますが以下のような初期設定はそのまま利用しています。

WorldSpace は無効にし、ParentSpace を有効にします。全体の重力は小さい数値にしています。この数値は調整の結果なので計算された理由はありません。
この辺りの WorldSpaceParentSpace の違いは Content Example の BP_PhysicsControlWalker を見ると理解が深まります。

ParentSpace で駆動させるため、外側からの座標の維持ではなく内側からの筋肉で駆動するような形になります。

参考情報

物理アセットと物理コントロールでキャラクターを動かす

まず、物理アセットを見てもらいます。

上から 横から

メインは肩関節と股関節です。

  • 肩関節
    • 水平方向にのみ 80 度動かせる
    • 脇をとじるようなイメージです。クロールよりも背泳ぎのような動きに近いです。
  • 股関節
    • 垂直方向は下に 30 度動かせる
    • 水平方向は 30 度動かる
    • 基本はバタ足で足を水平状態から下に押し下げるイメージです

この物理アセットの設定を踏まえた上で、物理コントロールでの手足の動きを確認します。

足の動き

以下はキックをした時の処理の一部です。Name には pelvis_thigh_l (左股関節) か pelvis_thigh_r (右股関節) が入ります。

ポイントは Control DataLinear Strength, Angular Strength の値と、 Control TargetTarget Angular Velocity Z です。
この辺りも色々試しての値なので「これでOK」という値ではないのですが、要は以下のようなことです。

  • Linear Strength0 にして位置を維持する力を 0 にする
  • Angular Strength0 以上にして角度を維持する力は残す
  • Target Angular Velocity に値を入れて該当の関節の角度に力を与え続ける
    • Z 方向は物理アセットの設定次第です。今回は Z マイナス方向が足を下方に曲げる(身体としては前方へのキック)ことになります

ちなみに Target Orientation に指定の角度(例えば -30) のような値を入れて動かすことも可能ですが、今回は Target Velocity を使っています。

上へのバタ足も考え方は同じです。

  • Target Angular Velocity に Z プラス方向の力を与え続けて戻す
  • または、Target Orientation0 (つまり、元の位置) を入れて戻す

最後にある程度動かし終えたら、元の Control Data に戻すだけです。

手の動き

以下は手でかいた時の処理の一部です。 Name には clavicle_l_upperarm_l (左肩関節) か clavicle_r_upperarm_r (右肩関節) が入ります。

省略していますが、キックと同様に Control DataLinear StrentghAngular Strength の値を変えています。

手の場合は、Target Angular VelocityTarget Orientation を両方設定しています。
もちろん、どちらかだけでも手を動かすことはできます。この辺りも調整の結果として両方を指定しています。

動きの確認

以下は横から動きを見たものです。
上記は Control DataControl Target の設定のみを示しましたが、実際にはバタ足の上下の動き、手のかきの閉じと開きは単純な Delay で制御されています。
足を下げてから 1.5sec 後に足を上げる、さらに 1.5sec 後に物理コントロールを元に戻す、といった感じです。

youtu.be

周囲のボール群

ボール群とは書きましたが、このゲームでは水そのものを表現しています。というかボールプールですね。

例えば、バタ足をすると足の周辺に泡が立ちますが、これは周囲のボールが移動した時に Niagara の Effect を再生し、波が立ちそうな場所に泡を出すということをしています。

ボールの一つ

以下の通り単なる SpherePhysicsControl です。

前述の泡を表示する部分を除けばスクリプトは以下のように Begin Play の設定のみです。 (加えて、最後の Multiplier は調整用です)
2つ目の画像にある Set Control Target Position を見てもらって分かる通り、初期位置を維持するようにしているだけです。

このあたりのシンプルな World-Space Control は Content Example の BP_PhysicsControlBox を見ると分かりやすいです。

ボールは初期位置を維持しますが、Linear Strength を弱めているため、物理的な力を加えられるとある程度移動します。しかし、その力が弱まるとまた元の位置にも土楼とします。 よって、その役割としては以下のようなものがあります。

  • ほぼ無重力状態のキャラクターをボールに囲まれた場所に留める。ただし、キャラクターが間に入ればその分だけスペースが広がる
  • キャラクターの物理的な動きに柔らかく反発し、その力に応じてキャラクターを押し戻す
  • ボールが元の位置に戻ろうとする力で、キャラクターの物理アセットをさらに押す

さも、これでうまくいくように書きましたが、実際には全然思ったようにいかず最初の投稿動画でお見せしたように、かなり不自由かつゆっくりとした動きにしかなりませんでした 😂
まあ、実際の私ぐらいの泳力だとあんなものなんですが。

ボール郡の管理

ボール郡は基本的にはキャラクターを中心に立方体のグリッドを形成しています。
それに加えてキャラクターの位置によって一定距離にボール郡を配置するように追加・削除を 1 秒ごとに繰り返しています。

Physics Control を使って泳ぐキャラクターを作ってみて

前回のUE5ぷちコンでは、MoverPhysical Animation を使ってゲームを作りました。

UE で外部からの物理の影響を (それなりに) 受けるキャラクターを作る - You are done!

その中で、次回は Phycis Control を使ったゲームを作りたいと思っていました。

今回もまずは、Physics Control を使ったキャラクターを地上で動かしていたのですが、中途半端なヒューマンフォールフラットみたいな動きにしかならず「どうしたものかなぁ」と悩んでいました。

ただ、前回ウォータースライダーのゲームを作り、必ずしも 2 足歩行するキャラクターでなくてもいいだろうということを学んでいたため、今回も水中でのキャラクターにすればおかしな動きをしても問題無いだろうと思い「泳ぐキャラクター」で行くことにしました。

泳ぐのは水を押していくこと

ここ半年ぐらいで娘と同じ水泳教室に通い始め、初心者向けのクロールや背泳ぎをしていました。
まだまだ上達しないのですが、コーチに教わる中でクロールにしろ背泳ぎにしろ、ただ手を回しているように見えても、実際には水中で「大きく水を押すか(そして進む)」を考えて身体を動かしていることが理解できてきました。

Physics Control を使ったキャラクターは Content Example もあるので簡単に作れるのですが、それを移動させるにはどうしようかというのも、その水泳がヒントになりました。
(流体とか学んでいればもっとスマートな方法があるとは思うのですが私では分からず。。)

水を押す代わりに、身体の周りにボールプールのように水を模したボールを配置し、それを押して進むことで移動できるようになりました。

そのボールも Physics Control で作成しましたが、ここに関しては Physics Constraint でも良かったのかもしれませんし、もっとシンプルな解決策があるような気もします。実際、かなり重くなるような気がするのでもっと軽い方法があればいいなと思います。

終わりに

今回の説明はかなりざっくりな上にとてもこれだけ見て再現できるものでは無いのが微妙ですね。。
一方で、面倒な箇所はあまり多くなく、物理アセットとその制約(どれぐらい、どの方向に動くか)を作り、Parent-Space の Physics Control をセットアップし、それを物理アセットで許可した方向に動かすようにするというだけです。

私の場合は Physics Control のキャッチアップからだったので時間がかかりましたが、この辺に慣れている人だと簡単に作れすぎて面白くないのかもしれません。(もっと面白い動きもさせられるのだろうなと思います)

これまでは「物理的に動くキャラクター」で遊ぶことを試してきましたが、もっと他にも使い道がないか試してみたいなと思います。

UE で外部からの物理の影響を (それなりに) 受けるキャラクターを作る

第22回UE5ぷちコン|株式会社ヒストリア で以下の作品を提出しました。

www.youtube.com

このゲームはシンプルなウォータースライダーですが、キャラクターがジャンプ台や噴水といった外部からくる物理の影響を受けるようになっています。
それに加えて、(ジャンプを除けば)現実でのウォータースライダーと同様にプレイヤーはキャラクターに対して身体の左右の角度を (Yaw 軸) 緩やかに変えることしかできません。
当然、ジャンプ中も方向転換等はできません。

とても操作に不自由があるゲームですし、物理に頼っているうえでの理不尽な挙動もあるのですが、なんとかゲームっぽくまとめることができたのではないかと思います。

キャラクターが物理の影響を受ける

今回のゲームにおいて、「キャラクターが物理の影響を受ける」には2つの観点があります。

  1. キャラクターが「外部からの力」を受けて、位置や回転が変わる
  2. キャラクターのメッシュが「外部からの力」を受けて、位置や回転が変わる

ここでの「外部からの力」は AddForceAddImpulse のようなものや、他の Simulate Physics なオブジェクトとの衝突、そして重力もそうですね。

粗いですが以下の画像でいえば、ジャンプ台になっている雲からの力を受けて「キャラクター」が物理的に上空に跳ね返され、それと同時に「キャラクターのメッシュ」も加速度や重力の影響を受けて手足がバタバタします。

以降ではそれぞれについてみていきます。

キャラクターが「外部からの力」を受ける

ここに関しては Mover plugin に 5.4 から試験的に実装された「Physics Based Character Movement」を使っているだけです。

Unreal Engine 5.4 Release Notes | Unreal Engine 5.5 Documentation | Epic Developer Community

Physics Based Character movement is a new Experimental feature we are building into the Mover plugin which provides for two-way physics-based interaction between the character and its environment.
- The system uses movement modes created in Mover and takes the output movement as a target motion for a physics representation of the character.
- The character imparts forces on the parts of the physics world it is interacting with, and physics forces and constraints can in turn affect the movement of the character.
- Multiplayer physics-based motion with rollback and re-simulation is supported using Networked Physics.

Mover plugin について把握したい方は以下の記事がとても参考になります。(私もこちらで知りました)

UE5 Character Mover 2.0とは - Let's Enjoy Unreal Engine

Quick Start

当然ながら UE 5.4 以上が必要です。

Plugin の有効化

まずは Mover と Mover Examples の plugin を有効化します。

Mover Examples が Engine/Plugins のフォルダにあるのが確認できます。
まずは、Maps フォルダにある L_PhysicsSimulatedCharacter を開いて遊んでみるのもいいかもしれません。

Example から PhysicsMannyPawn をコピー

続いて Pawns フォルダに PhysicsMannyPawn があるので、こちらを自身の Content の方に copy してきましょう。
名前がかぶると面倒なので、私は BP_PhysicsMannyPawn に変更しました。

以下のように BP_ThirdPersonCharacter と並べてみます。

まずここで面白いのが、先程の alwei さんの記事でも触れられていましたが、PhysicsMannyPawn のキャラクターは、Simulate Physics されたオブジェクトと同じように押すことができます。

以下の画像は BP_ThirdPersonCharacter で押していますが、もちろん PhysicsMannyPawn 同士で押し合うこともできます。

どうしてこうなるのか?

Mover のコードを追えていないので詳しいことはわかりませんが、まず PhysicsMannyPawnCapsuleSimulate Physics が有効化されています。

ちなみに BP_ThirdPersonCharacter の方でも CapsuleSimulate Physics を有効にしたとしても、以下のコードがあるためそもそも操作すらできなくなります。

// CharacterMovementComponent.cpp

void UCharacterMovementComponent::TickComponent(
{
    // 略
      
    // See if we fell out of the world.
    const bool bIsSimulatingPhysics = UpdatedComponent->IsSimulatingPhysics();
   
  // We don't update if simulating physics (eg ragdolls).
    if (bIsSimulatingPhysics)
    {

詳細も気になるところですが、まずは「これで物理に反応できるキャラクターが手に入った」ということです。

以下は、Physics Constraint を使ったバネを使ったデモです。
ThirdPersonCharacter の方は、ジャンプして着地してもそのままバネの上に立つだけですが、PhysicsMannyPawn の方はバネの跳ね返りの力でそのまま跳ね上がっています。キーを推して能動的にジャンプはしていません。

ThirdPersonCharacter
PhysicsMannyPawn

このバネは以下のチュートリアルで作成したものです。この人のチュートリアルは物理挙動好きな私にはどれも楽しいです😇
UE4 Jump Pad - using Physics Constraints in Unreal Engine 4 Tutorial How To - YouTube

キャラクターのメッシュが「外部からの力」を受ける

上記で貼った Gif 動画で分かる通り、現状ではキャラクターは跳ね上がった際も設定されたジャンプ (fall loop) アニメーションが再生されています。これを物理の影響を受けているようなポーズにしたいです。

Physical Animation

こちらも Physical Animation を使っているだけです。

Quick Start

Physical Animation Component の追加

ここまでで作成してきた Mover で動く BP_PhysicsMannyPawnPhysical Animation Component を追加します。

Physics Asset の付いた Skeletal Mesh に入れ替える

続いて、Skeletal Mesh の入れ替えを行います。というのも、ここまで使ってきた BP_PhysicsMannyPawn がデフォルトで利用する Mover Example の Skeletal Mesh は Physics Asset が適用されていません。

よって、Physics Asset が付いたものに入れ替えます。(Mover Example の Skeletal Mesh をコピーして、Physics Asset をつけても構いません)
私は ThirdPersonCharacter が利用している SKM_Manny に入れ替えました。

Skeletal Mesh の Collision を調整

上述のドキュメント通りで大丈夫です。

ドキュメントにもありますが、Capsule の Object Type が Pawn であるため、最低限 Pawn は除外する必要があります。

BP に設定を書く

ドキュメントとの大きな違いは Profile を使わず、Physical Animation Data を直接 Make して使っています。
また、基準となる Bone を pelvis にしています。

Physics Asset を見ると分かりますが、pelvis 以下ということは実質全てです。

また、Set All Bodies Below Simulate PhysicsInclude Self は無効にしています。これを有効にしてしまうと pelvis もぐにゃぐにゃになって動けなくなります。

Physics Blend Weight のみ変数化していますが、今回は 0.7 に設定しています。これを 1.0 に近づけるほどラグドールっぽくなります。

後はこれを呼び出すだけです。今回は Begin Play で呼び出してしまいます。

さて、これでラグドールっぽいけど動くキャラクターが手に入りました。

最終確認

ここまででウォータースライダーゲームのジャンプ台の動きが再現できました。(ウォータースライダーはキャラクターのポーズが寝そべっていまので姿勢は違いますが)

ちなみに、ウォータースライダーゲームの能動的なジャンプは、Physical Animation を無効化しています。

終わりに

Physical Animation はダメージリアクション等で使いやすい機能ですが、キャラクターを動かす時に使うと違和感のある動きになってしまいます。

今回はウォータースライダーにいるキャラクターでほぼ動きが無いこともあり、違和感なく利用することができました。

加えて、Mover の Physics Based Character Movement を使うことで、キャラクターがより簡単に物理の影響を受けることができるようになり、 Physical Animation が生かされるようになりました。

余談ですが、最初は Physics Control を使って実現しようとしていたのですが、なかなかうまくいかず断念しました。次は Physics Control も使ってなにかできればと思っています。

Roblox でのデータ永続化を学ぶ

Roblox でゲームを作るにあたり、適当にチュートリアルをこなした後に「まずはデータの永続化だろう」ということでそこから押さえていくことにします。

基本

公式の中級チュートリアルにある以下のドキュメントが基本になると思います。

create.roblox.com

ほぼ同じ内容になるかもしれませんが一つ一つ見ていきたいと思います。

Enabling Studio Access

デフォルトでは Studio から data stores にはアクセスできないので有効化します。

Creating a Data Store

Data stores は DataStoreService で管理されるようです。

DataStoreService を確認するにあたり、 Data stores のページが分かりやすいので見ていきます。

Data Stores

DataStoreService lets you store data that needs to persist between sessions, such as items in a player's inventory or skill points.
Data stores are consistent per experience, so any place in an experience can access and change the same data, including places on different servers.

per experience と書かれている通りで、この永続化は experience 単位での保存であり、 experience 内の複数の place からアクセス可能のようです。
experience は以下の最初のチュートリアルにあるように一つのバーチャル空間(ゲーム)と考えておけばいいと思います。
Creating Your First Experience | 文書 - Roblox クリエーターハブ

適切ではないかもしれません、UE で言い換えれば、一つの UE のプロジェクトが experience、プロジェクト内のレベルが各 place に相当します。

Enabling Studio Access

先ほど有効化したように、ゲーム設定のセキュリティから有効化しました

Accessing a Data Store

導入は以下のように GetService で行い、名前で個別の Data store にアクセスできます。

local DataStoreService = game:GetService("DataStoreService")
local experienceStore = DataStoreService:GetDataStore("PlayerExperience")`

The server can only access data stores through Scripts. Attempting client-side access in a LocalScript causes an error.

NOTE にある通り、サーバーからのみアクセスできます

Scopes

以下のような形式で scope を付与でき、GetDataStore の第二引数で絞り込みができます

ただ、NOTE にある通り、この方法は現在は Listing and Prefixes の方法に置き換えることが推奨されています。
ただ、既存のコードで見かけることがあると思うので知識としておさえておくと良さそうです。

For new experiences, it is not recommended to use the legacy scopes feature. Instead, use listing and prefixes to organize keys in your data store. If you have an existing experience that uses scopes, you can continue using them.

Managing a Data Store

Setting Data

A data store is essentially a dictionary, similar to a Lua table. A unique key indexes each value in the data store, such as a player's unique Player.UserId or a named string for a game promo.

NOTE にある通りで、data store は辞書データであり、プレイヤーの id やゲーム内で使う文字列をキーとして使います。

local success, errorMessage = pcall(function()
    experienceStore:SetAsync("User_1234", 50)
end)
if not success then
    print(errorMessage)
end

SetAsync に key / value を与えることで簡単に保存ができます。
以下の NOTE にある通り、SetAsync はネットワーク呼び出しであるので失敗の可能性があり、よって pcall を使ってエラーハンドリングを行います。

Functions like SetAsync() that access a data store's contents are network calls that may occasionally fail. As shown above, it's recommended that these calls be wrapped in pcall() to catch and handle errors.

Reading Data
local success, currentExperience = pcall(function()
    return experienceStore:GetAsync("User_1234")
end)
if success then
    print(currentExperience)
end

GetAsync に key を与えることで value が簡単に取得できます。

The values you retrieve using GetAsync() sometimes can be out of sync with the backend due to the caching behavior. To opt out of caching, see Disabling caching.

上記の NOTE にある通りで、 cache の仕組みで同期された値を取得できない場合があり、場合によっては cache 自体を無効化することもできます。
cache に関しては後ほど詳細が出てきます。

pcall でエラーハンドリングすることに関しては SetAsync と同じようです。

Incrementing Data
local success, newExperience = pcall(function()
    return experienceStore:IncrementAsync("Player_1234", 1)
end)
if success then
    print(newExperience)
end

数値データに関しては、キーに対して変更値を与えることで直接値を更新できるようです。

Updating Data
local function makeNameUpper(currentName)
    local nameUpper = string.upper(currentName)
    return nameUpper
end

local success, updatedName = pcall(function()
    return nicknameStore:UpdateAsync("User_1234", makeNameUpper)
end)
if success then
    print("Uppercase Name:", updatedName)
end

UpdateAsync はデータの更新ですが、SetAsync と違い、key に対して新しい value を渡すのではなく、現在値を更新する callback function を渡します。
通常は更新後の値を return しますが、nil を返すと更新処理がキャンセルされます。

The callback function passed into UpdateAsync() is not permitted to yield, so it cannot contain any yielding function like task.wait().

また、上記の NOTE にある通りで、 task.wait() を含むような yield 処理が入る関数にすることはできません。

Set vs. Update

この辺は一般的なデータ保存であることですが、Set の場合は、GetAsync で取得したデータが必ずしも最新のデータかは分からず(他のサーバーで更新がされるなど)SetAsync するときには意図せず直前の更新を無視した更新になってしまう可能性があります。

一方で Update の場合は、更新時に直前のデータを読み出すのでより安全にデータの更新ができます。

まあ、この辺はデータの性質(例えば確実に一人のユーザーからしか更新されない or いろんなユーザー・サーバーから更新される)を考慮して選択すればいいでしょう。

Removing Data
local success, removedValue = pcall(function()
    return nicknameStore:RemoveAsync("User_1234")
end)
if success then
    print(removedValue)
end

削除は key を指定して行います。

Ordered Data Stores

Data store は通常ソートされていませんが、一部の用途でソートされたデータ必要になるため、OrderedDataStore が用意されています。

用意する方法は以下のように取得方法を変えるだけです。

-- 通常の Data store
local characterAgeStore = DataStoreService:GetDataStore("CharacterAges")
-- Ordered Data Store
local characterAgeStore = DataStoreService:GetOrderedDataStore("CharacterAges")

これだけで取得される型が OrderedDataStore に変更されています。

OrderedDataStore | 文書 - Roblox クリエーターハブ

OrderedDataStore の仕様は上記にあります。

A OrderedDataStore is essentially a GlobalDataStore with the exception that stored values must be positive integers.
It exposes a method GetSortedAsync() which allows inspection of the entries in sorted order using a DataStorePages object.

ドキュメントを見ると、まず Data store は GlobalDataStore であり、それに対して **positive integersのみを value にとるという制限がOrderedDataStoreにはあるようです。 また、GetSortedAsyncというメソッドが用意されており、DataStorePages` というオブジェクトを使ってソートされた結果を表現するようです。

Ordered data stores do not support versioning and metadata, so DataStoreKeyInfo is always nil for keys in an OrderedDataStore. If you need versioning and metadata support, use a DataStore.

また、versioning や metadata もサポートされず、 DataStoreKeyInfonil になるようです。

Ordered data stores do not support the optional userIds parameter for SetAsync() or IncrementAsync().

加えて、SetAsync, IncrementAsync で利用できる userIds パラメータも利用できないとあります。

認識していませんでしたが、これらのメソッドには userIds というパラメータが設定でき、おそらくその処理を実行したユーザーの記録に使われるのだと思います。

OrderedDataStore | 文書 - Roblox クリエーターハブ

こう見ると、GetDataStore 時にスコープを指定できたりと細かい設定はありそうですね。

何でソートされるか?

value に integers しか設定できないという仕様からも、おそらく value でのソートが行われるのだと思います。 leaderboard が用途にあげられているので、まあ名前でソートしても意味がないので当然といえば当然かもしれません。

Metadata

まず、Service-defined, User-defined の 2 種類があり前者は DB にもありそうな update time や creation time です。
後者はユーザーが指定できるもので、ちょうど先程 SetAsync の引数としても出てきていた DataStoreSetOptions を通して設定するようです。

SetAsync の場合
local experienceStore = DataStoreService:GetDataStore("PlayerExperience")

local setOptions = Instance.new("DataStoreSetOptions")
setOptions:SetMetadata({["ExperienceElement"] = "Fire"})

local success, errorMessage = pcall(function()
    experienceStore:SetAsync("User_1234", 50, {1234}, setOptions)
end)
if not success then
    print(e

そのままではありますが、DataStoreSetOptionsインスタンスを作成し、指定したいメタデータを用意したら、それを SetAsync の第四引数に設定します。

ここでは先程紹介した userIds も第三引数に指定されていることが分かります。

Table of UserIds, highly recommended to assist with content copyright and intellectual property tracking/removal.

となっているので、 userIds は基本的には設定した方がいいんでしょうか。

GetAsync(), IncrementAsync(), and RemoveAsync() の場合
local experienceStore = DataStoreService:GetDataStore("PlayerExperience")

local success, currentExperience, keyInfo = pcall(function()
    return experienceStore:GetAsync("User_1234")
end)
if success then
    print(currentExperience)
    print(keyInfo.Version)
    print(keyInfo.CreatedTime)
    print(keyInfo.UpdatedTime)
    print(keyInfo:GetUserIds())
    print(keyInfo:GetMetadata())
end

これまでは 実行結果としては単一の結果 (success は pcall の結果であり除外) しか受け取っていなかったのに対して、 keyInfo を追加で受け取ることができます。

GetAsync の仕様を確認すると、戻り値は Tuple になっており、今回の DataStoreKeyInfo が取得できることが分かります。

DataStoreKeyInfo に metadata だけでなく userIds や version の情報も格納されていることが分かります。

UpdateAsync の場合
local nicknameStore = DataStoreService:GetDataStore("Nicknames")

local function makeNameUpper(currentName, keyInfo)
    local nameUpper = string.upper(currentName)
    local userIDs = keyInfo:GetUserIds()
    local metadata = keyInfo:GetMetadata()
    return nameUpper, userIDs, metadata
end

local success, updatedName, keyInfo = pcall(function()
    return nicknameStore:UpdateAsync("User_1234", makeNameUpper)
end)
if success then
    print(updatedName)
    print(keyInfo.Version)
    print(keyInfo.CreatedTime)
    print(keyInfo.UpdatedTime)
    print(keyInfo:GetUserIds())
    print(keyInfo:GetMetadata())
end

特筆すべき点はありませんが、同様に戻り値に keyInfo を取得できています。

その他

まず NOTE として

You must always update metadata definitions with a value, even if there are no changes to the current value, otherwise, you lose the current value. This applies to SetAsync(), IncrementAsync(), and UpdateAsync().

とあります。つまり、metadata に変更がなくても常に更新しないと現在値が失われるということです。まあ、wrapper 関数を用意して常に値をいれるようにすべきですね。

また、ユーザー定義のメタデータには以下の制限があります。

  • Key length: up to 50 characters.
  • Value length: up to 250 characters.
  • No limit for the total number of key-value pairs but the total size cannot exceed 300 characters.

Versioning

更新系のメソッドを利用した際に priodically にバージョン付けられたバックアップを作成してくれるようです。
UTC hour 単位となっており、その単位時間内で最初の書き込みにはバックアップが作成され、その時間内の2度目以降の書き込みは新しいバックアップではなく上書きになるようです。
まあ、一定時間ごとの最終データが記録されていると考えておけば良さそうです。

また、保持期限は 30 日で最新データに関してだけは無期限に保持されるとのことです。

Snapshots

Snapshot Data Stores - Data Stores | 文書 - Roblox クリエーターハブ

上記の API を通して、experience 内の全データのスナップショットを取得できます。これは、前述の Versioning のデータと同様なのですが、versioning が上書きになる単位 UTC 時間内であっても、新しくバックアップが作成されます。

experience に大きな更新をかけるときなどに、データが壊れて戻せなくなることを防ぐために直前に snapshots を取得するのが良さそうです。

Listing and Prefixes

ListDataStoresAsync(), ListKeysAsync() を使って、それぞれ Data store 、data store 内のキーの一覧を取得できます。

AllScopes Property

GetDataStore の部分で scope に関する言及がありました。そこで、scope で絞り込むよりも prefix で一覧することが推奨されていました。

ドキュメントだけだとパッと理解できなかったのでコードを書いて確認します。

local DataStoreService = game:GetService("DataStoreService")

local options = Instance.new("DataStoreOptions")
options.AllScopes = true

local dataStoreName = "DataStoreSample"

-- normal
local dataStore = DataStoreService:GetDataStore(dataStoreName)
-- scoped
local dataStoreScopedHouse = DataStoreService:GetDataStore(dataStoreName, "house")
-- all scopes
local dataStoreAllScopes = DataStoreService:GetDataStore(dataStoreName, "", options)

local DataStoreSample = {}

function DataStoreSample.GetData(key)
    local success, data = pcall(function()
        return dataStore:GetAsync(key)
    end)
    
    if success then
        return data
    end
end

function DataStoreSample.SetData(key, data)
    local success, data = pcall(function()
        return dataStore:SetAsync(key, data)
    end)

    if success then
        print("success")
    end
end

function DataStoreSample.GetDataFromHouse(key)
    local success, data = pcall(function()
        return dataStoreScopedHouse:GetAsync(key)
    end)
    
    if success then
        return data
    end
end

function DataStoreSample.SetDataToHouse(key, data)
    local success, data = pcall(function()
        return dataStoreScopedHouse:SetAsync(key, data)
    end)

    if success then
        print("success")
    end
end

function DataStoreSample.GetDataFromAll(key, scope)
    local success, data = pcall(function()
        return dataStoreAllScopes:GetAsync(`{scope}/{key}`)
    end)
    
    if success then
        return data
    end
end

function DataStoreSample.SetDataToAll(key, data)
    local success, data = pcall(function()
        return dataStoreAllScopes:SetAsync(key, data)
    end)

    if success then
        print("success")
    end
end

function DataStoreSample.ListData()
    local listSuccess, pages = pcall(function()
        return dataStoreAllScopes:ListKeysAsync()
    end)

    local results = {}
    if listSuccess then
        local items = pages:GetCurrentPage()
        if #items > 0 then
            for i, item in ipairs(items) do
                table.insert(results, item.KeyName) -- https://create.roblox.com/docs/ja-jp/reference/engine/classes/DataStoreKey
            end
        end
    end
    return results
end

function DataStoreSample.ListDataWithPrefix(prefix)
    local listSuccess, pages = pcall(function()
        return dataStoreAllScopes:ListKeysAsync(prefix)
    end)

    local results = {}
    if listSuccess then
        local items = pages:GetCurrentPage()
        if #items > 0 then
            for i, item in ipairs(items) do
                table.insert(results, item.KeyName)
            end
        end
    end
    return results
end

return DataStoreSample

名前は同じで、3 つの Data store を取得しています。

local options = Instance.new("DataStoreOptions")
options.AllScopes = true

local dataStoreName = "DataStoreSample"

-- normal
local dataStore = DataStoreService:GetDataStore(dataStoreName)
-- scoped
local dataStoreScopedHouse = DataStoreService:GetDataStore(dataStoreName, "house")
-- all scopes
local dataStoreAllScopes = DataStoreService:GetDataStore(dataStoreName, "", options)

When you use this property, the second parameter of GetDataStore() must be an empty string ("").

とあるように、AllScopes の場合は必ず第二引数は "" にします。

これを使って SetAsync, GetAsync そして ListKeysAsync を使うメソッドを定義しています。

これを使う側は以下の ServerScript です。

local Players = game:GetService("Players")
local ServerScriptService = game:GetService("ServerScriptService")
local DataStoreSample = require(ServerScriptService.DataStoreSample)

Players.PlayerAdded:Connect(function(player)
    local userId = "dummy_user_id"

    DataStoreSample.SetData(userId, "Test in Global") -- key is "global/dummy_user_id"
    DataStoreSample.SetDataToHouse(userId, "Test in House") -- key is "house/dummy_user_id"
    DataStoreSample.SetData(`house/{userId}`, "Test in Global 2") -- key is "global/house/dummy_user_id"
    DataStoreSample.SetDataToAll(`house/{userId}_2`, "Test in House 2") -- key is "house/dummy_user_id_2"

    local data = DataStoreSample.GetData(userId)
    local dataInHouse = DataStoreSample.GetDataFromHouse(userId)
    local dataInGlobalFromAll = DataStoreSample.GetDataFromAll(userId, "global")
    local dataInHouseFromAll = DataStoreSample.GetDataFromAll(userId, "house")

    -- それぞれの先頭のデータが取得できている
    print(`data is "{data}"`) --Output is "Test in Global"
    print(`dataInHouse is "{dataInHouse}"`) --Output is "Test in House"
    print(`dataInGlobalFromAll is "{dataInGlobalFromAll}"`) --Output is "Test in Global"
    print(`dataInHouseFromAll is "{dataInHouseFromAll}"`) --Output is "Test in House"

    print(DataStoreSample.ListData())
    -- {
    --    [1] = "global/house/dummy_user_id",
    --    [2] = "house/dummy_user_id",
    --    [3] = "house/dummy_user_id_2",
    --    [4] = "global/dummy_user_id"
    --  }
    print(DataStoreSample.ListDataWithPrefix("hou"))
    -- {
    --    [1] = "house/dummy_user_id",
    --    [2] = "house/dummy_user_id_2"
    -- }
    print(DataStoreSample.ListDataWithPrefix("glo"))
    -- {
    --    [1] = "global/house/dummy_user_id",
    --    [2] = "global/dummy_user_id"
    -- }
end)

確認できた通りで、AllScopes の場合 は SetAsync は scope を直接 house/ のように prefix を付けるだけで指定できます。
そして、同様に GetAsync も prefix を付けることで取り出すことができます。

また、ListKeysAsync に関しても期待通り prefix で絞り込むことができています。

ちなみに、AllScopes の場合は GetAsync の引数は必ず scope_name/key_name の形式で渡さないとエラーになります。

GetAsync - GlobalDataStore | 文書 - Roblox クリエーターハブ

Error Codes

これまでも pcall を使ってアクセスすることが言及されていますが、細かくエラーコードが網羅されていてこれは素晴らしいです。

Limits

Server Limits, Data Limits, Throughput Limits と制限が設けられています。

これらは experience 単位の Data store になります。制限に到達した場合は、4 つのキューに振り分けられるようです。
それは、set, ordered set, get, ordered get の 4 つになります。

案外 Data Limits にある文字列の長さ制限は気をつけなければいけないかもしれません。50 文字ですね。

Caching

データのキャッシュ機能がついています。GetAsync は 4 秒のキャッシュ時間があるようです。Data store はそもそもサーバーでしか実行されないと思いますが、サーバーからさらに backend に対してのリクエストのキャッシュということでしょうか。

キャッシュは更新系のメソッドで適用されるようですが、GetAsync でも取得時にキャッシュにデータがなければ作成されるようです。

また、GetVersionAsync, ListVersionsAsync, ListKeysAsync, ListDataStoresAsync は常に最新のデータを取得するようです。

Disabling Caching

GetAsyncDataStoreGetOptionsUseCache を無効化することでキャッシュを使わずにデータを取得できます。

が、制限もあるので用法用量を守ってということですね。

Best Practices

いくつかポイントがあるようですが、

  • Data store はできるだけ少ない数にし、Data store 間に関連を持たせる
    • DB のテーブルと似ていると言及されていますが、どれぐらいの数が適切かまでは書かれていません
  • 保存できるオブジェクトサイズは 4MB までであり、関連データは一つのオブジェクトとして保存すると良さそうです
    • 関連データに分けすぎると、フェッチの回数が多くなってしまうのでそれを避けられます
  • key の prefix を使ってデータを organize する

この辺を見ていると、状況は違いますが Firestore の設計に多少似ている部分もありそうです。

いったんまとめ

さて、データーストアの「概要」ページをなぞっていったわけですが、結構なボリュームでした。

ただ、おかげでだいたいのことは把握できましたし、簡単なサンプルコードも試せました。

自分には比較的馴染みがある Firestore の設計も参考にできそうなことも分かって良かったです。

「UEFN(Unreal Editor For Fortnite)でゲームづくりを始めよう!」で UEFN に入門した

wgn-obs.shop-pro.jp

読んだ書籍としては上記ですが、前半の内容はゲームメーカーズの方でも同じような内容のものが見れそうです。(書籍で利用するアセットもこのサイトから取得します)

gamemakers.jp

動機

前回のUE5ぷちコンで作ったゲームをどれぐらい UEFN に移植できるのかが興味があって、触ってみることにしました。
ずっと積まれていた本を消化したかったのもあります 😅

移植しようとしたゲームは以下のようなウォータースライダーのゲームです。

youtu.be

書籍を読んでみて

アセットのパスが変わっているものがある

書籍で利用する(サイトの方も同じですが)UEFN のアセットは「All > Fortnite > Devices」にあるとなっていますが、現在のバージョンでは、Devices ではなく GameplaySystem, Physics などのフォルダに分散しています。

検索すればいいのですが、とにかくフォルダも中のアセットも数が多いので大変そうです。

このようなリファレンスも作ってくださっているので、うまく利用したいです。
フォートナイト クリエイティブとUEFNで使える仕掛け一覧|ゲームメーカーズ

スケルタルメッシュのマテリアルがすぐに有効にできなかった

書籍やサイトの通りに進めると、デフォルトマテリアルがメッシュに設定されてしまいます。 エラーメッセージから bUsedWithSkeletalMesh=True が無いということがわかりました。結果的には、マテリアルで以下のオプションにチェックを入れるだけだったのですが、「自分のインポート方法が悪いのかなと」いうところばかり疑っていたので、少し時間を要しました。

レベルを変更して、Fortnite 側に反映されるタイミングがよくわからない

UE5 でいうところの PIE のようなものが、Fortnite 側での実行になるのですが、一度終える場合には Fortnite 側で「ゲームを終了」だけして、UEFN とのセッションは継続しておき、UEFN 側でレベルを変更したら「ゲームを開始する」としていたのですが、たまに反映されないことがありました。

その場合は「変更をプッシュする」や「更新」などをするのですが、そうするとセッションが一度切断されるので、結構な時間がかかります。

そんなものかなとも思っているのですが、開発体験としてはあまりよくないので、もしかすると私の知らないいい方法があるのではと思っています。

初めての Verse プログラムを変更して実行する | Unreal Editor for Fortnite ドキュメンテーション | Epic Developer Community

その後、上記のドキュメントを見ていて、「エディタで加えたすべての変更」の反映には「変更をプッシュ」しかなさそうだなと理解できました。

移植できそうだったか

コースを作るスプラインに関して

UE5 側では Spline コンポーネントを持った Blueprint でコースを作っていたのですが、UEFN 側ではそれがぱっと見つからりませんでした。ここに関してはあまり深く調査していないので、方法はあるのかもしれません。

とりあえずランドスケープのスプラインで作ってみました。

一方で、アセットの移行なんかは、UE5 の Migrate で直接放り込めたのでとても楽ですね。

アセットを Unreal Engine から移行する | Unreal Editor for Fortnite ドキュメンテーション | Epic Developer Community

プレイヤーをコースで滑らせられるか

UE5 側ではプレイヤーの Movement の DecelerationFriction を操作して滑り続けるようにしていたのですが、UEFN 側ではプレイヤーキャラクターに対して同じことをする方法がぱっと見つかりませんでした。

一方で、スライダーに対して物理マテリアルの Ice を適用すると、プレイヤーが氷の上を滑るような感じにできることがわかりました。

ここまでで・・・・

入門から、多少の移植作業をしてみましたが、ここからは UEFN の仕掛けや Verse でのスクリティングを学ばないと「何ができるのか?」を調べることもままならないなと感じ一旦ストップです 😵

UEFN 入門してみて

久しぶりに Fortnite も開いてみて「うわ、なんだこれ Roblox みたいじゃん」とトップ画面を見て驚きました。

UEFN は UE5 と似てはいますし取っつきやすいですが、これはこれである種の別物として向き合わないといけないなとも思いました。

UE5 でポカポンぽいゲームを作ってみる ~プロトタイピング~

ポカポン

epoch.jp

私も子供の頃の淡い記憶でしかないのですが、小学生の頃からファミコンスーファミはあったものの、まだこういうファミリーゲームも家にはありました。

今回はこの感じを目標に作ってみたいと思います。「プロトタイピング」編としていますが、これで終わるかもしれません。

記事投稿時点の状態

youtu.be

コードは以下です。 UE 5.3.2 で作成しています。追加のプラグイン等はありません。

Release Blog post 20240604 · dany1468/Pokapon_UE · GitHub

作成過程

  1. Modeling Tool でキャラクターの Static Mesh を作る
  2. BP で Static Mesh を合わせてキャラクターの動きを作る
  3. レベルに配置する
  4. キーボードで操作できるようにする

以上のような過程で作っていきました。

1. Modeling Tool でキャラクターの Static Mesh を作る

UE5 で Skeletal Mesh Editing Tool を使って、物理挙動で動くだけのキャラクターを作る - You are done!

以前のブログでも Modeling Tool で簡単なキャラクターを作りましたが今回も以下のように単純なものです。

上記のコードでは CharacterCreationMap という専用のレベルで作成しています。

正面 右側面

腕の Pivot

Physics Constraint をこの後使うため、腕の Pivot だけは回転軸あたりに設定しています。

2. BP で Static Mesh を合わせてキャラクターの動きを作る

コードでいうところの BP_PokaponPawn です。名前の通り Pawn をベースにしています。

コンポーネントも、各腕に対してのハンマー、盾以外はフラットに並んでいます。

PC_Physics Constraint Component で、各パーツを繋いでいます。PC_Body_Outer だけは、Body に対して外部の設置した箇所に接続するため、接続コンポーネントが片方空いています。

細かい部分はコードをみてもらった方がいいのですが、左右の手の Physics Constaraint だけ見ておきます。それぞれ、角度の対して制約をかけています。

PC_RightHand_Body PC_LeftHand_Body

とはいえ、最初の動画を見てもらって分かる通り、数値はかなり雑に設定してあるものです。

最後に Event Graph ですが、ハンマーと盾をそれぞれ押し下げる動きを作っているだけです。

3. レベルに配置する

Github のコードでいうところの PokaponTestMap が対象になります。

Outliner Viewport

BP_Camera があるのが分かります。これは Camera Component を持つだけの BP です。
Auto Possess Player を Player 0 にしてあります。このカメラがこのレベルでのプレイヤー扱いです。

BP_PokaponPawn, BP_PokaponPawn2 はそれぞれに Player1, Player2 のタグを付けてあります。

4. キーボードで操作できるようにする

現状 PIE でスタートしても何も動かせないので動くようにします。

Player Controller を作成します。PC_Pokapon という名前にしてあります。

Begin Play
Inputs

ゲーム開始時に Player1, 2 を Tag から検索して参照を保存し、Input があった時にはそれぞれに対してイベントを呼び出しています。

当然ながら Enhanced Input のファイルも作成してありますが、特に工夫はないので割愛します。Github の方には Inputs フォルダがあるので、そこを見てください。

まとめ

Phyisics でゲームを考える時に、どうしても現実にあるものをベースに考えてしまうのですが、レトロゲームはとてもルールもよく考えられていて素晴らしいなと改めて思うのでした。

UE の GAS Companion と Blueprint Attributes プラグインを試す

UE の GameplayAbilitySystem (GAS) はとても便利です。

一方で、私はまだまだ理解も浅いです。Lyra や古代の谷などのサンプルの他に、以下のドキュメントが参照先として紹介されるのを見ることがありますが、いつも見ては忘れ、見ては忘れという感じです。

以前は C++ がなければ使えないような機能があり、その当時から GAS Companion を使っていたのですが、同じ作者が Blueprint Attributes というプラグインを公開しているのを知り、今回はそれらを連携させてどういうことができるか試してみました。

Plugins

以下にマーケットプレイスのリンクと Description に加えて、公式のドキュメントとサンプルプロジェクトを一覧します。
いずれもとても丁寧に作られ、更新されていますし Discord での質問も受け付けているようです。

どちらも、GAS で C++ が必要な部分を Blurprint のみで実装可能にしてくれるものではありますが、それに加えて前述の GASDocumentaion で推奨されるようなパターンを考慮した形になっているようです。

今回試すこと

GAS Companion だけでも GAS を使う分には十分なのですが、Blueprint Attributes を使うことで、GAS の Attribute Set を Blueprint で作成することができます。今回はその点を中心に試していきます。

GAS Companion だけだと、https://gascompanion.github.io/api/Abilities/Attributes/GSCAttributeSet/ にある基本的な属性しか利用できず、追加するためには C++ での実装が必要になります。 とはいえ、Attributes Wizard にあるようにウィザードまで用意されているので、「やれば」的な感じではあるのですが。

実際に試す

とりあえず必要なものを揃える

BP_GASStudyCharacter : Thirdperson をコピーしたもので、 GSCModularCharacter に Parent を変更してあるだけです。

以下は、公式のサンプルプロジェクトに入っている Utility のコードなのですが、便利なのでとりあえずコピーします。(一番下の Home ボタンのは私が足したので、不要だと思います)

今回は PlayerController (PC_GASStudy) に貼りました。

まずは GAS Companion だけで Attributes を設定する

BP_GASStudyCharacter の方に設定するだけです。 GSCAttributeSet は GAS Companion からデフォルトで提供されています。

この状態で PIE で Enter を押してデバッグ情報を表示すると以下のようになり、 GAS Companion が提供する AttributeSet が設定できていることが分かります。

UI を付ける

Working with UI

デフォルトの AttributeSet であれば、こちらもデフォルトで提供されている Widget を利用できます。今回は WB_HUD を使いました。

上記を PC_GASStudy に設定すると期待通り表示を確認できます。

自分で UI を作る

先ほどのデフォルトの Widget の親クラスになる GASUWHud を継承して作成できます。

https://gascompanion.github.io/api/UI/GSCUWHud/

今回は WBP_CustomGSCUWHud と名前を付けました。これを先ほどの WB_HUD の代わりに入れても当然何も表示されません。

Widget の内部を作成

https://gascompanion.github.io/working-with-ui/#designing-your-own-widget

ドキュメントの通りですが、デフォルトで提供されている AttributeSet はすでに C++ 側で Text なども準備されており、それをレイアウトするだけになります。今回は Progress Bar は無視して Text 系だけ同じ名前で設定します。

これだけで以下のように UI が表示されました。現状 Max も Current も 0 なので 0 / 0 のような表示になります。

デフォルトの Attribute Set に値を入れる

https://gascompanion.github.io/quick-start/#initialization-with-data-table

ドキュメントの通りですが、Data Table を以下の構造を使って作成します。

DT_Study_Attributes という名前にしました。内容は以下です。ここも Row Name は決まっています。

先ほどの BP_GASStudyCharacter に設定した箇所の下に Data Table を設定できるので設定します。

設定 表示

UI をカスタムし、自分で値を取得し表示する

さて、自動で値が設定されるのは便利なのですが、ここから Attribute を追加したいので、まずは手動で表示してみます。

https://gascompanion.github.io/api/Components/GSCCoreComponent/

そのためには上記の Component を追加する必要があります。

先程の Widget に一列追加し、手動表示の列とします。

tree view

Widget 側の Construct で以下のように初期値を表示してみます。(以下は簡略化のために Health だけです)

お気づきの通りで、 Health など、デフォルトで用意される Attribute に関しては専用の取得関数も用意されています。

以下のように最終列に手動での表示ができました。

UI をさらにカスタムして使う

先ほどは GSCUWHud を継承して使いましたが、見てきた通りでデフォルトで提供される Attribute の Text 等がすでに入っていました。

今回はそれも入っていない GSCUserWidget を使います。

https://gascompanion.github.io/api/UI/GSCUserWidget/

WBP_CustomGSCUserWidget という名前にしました。とりあえず、先程の WBP_CustomGSCUWHud の UI の内容だけコピペしておきます。

次に、スクリプトもコピーしようとしますが、Get Owning Core Component が存在しないエラーになります。

Set Owner Actor を使う

https://gascompanion.github.io/api/UI/GSCUserWidget/#setowneractor

つまり、まだ WidgetGSCCoreComponent を持つ Owner Actor を知らないということです。

とりあえず、Widget のコンストラクタで設定してみます。

うまくいきました。

さて、ここまでで Blueprint Attributes で新しい Attribute を追加した時に表示できそうな準備ができました。(デバッグ表示だけでも良かったのですが。)

Attribute を Blueprint Attributes で作る

コンテンツブラウザから簡単に作れます。 GBA_Study_Attributes という名前にしました。

Armor / MaxArmor を作ります。

Blueprint Attributes には Gameplay Clamped Attribute Data が追加されており、これを利用することで簡単に最大値・最小値に clamp してくれる Attribute を作れます。

さて、この新しい Attribute Set もこれまでと同様に BP_GASStudyCharacter に設定します。

まずはデバッグで確認しましょう。OKですね!

追加した Attribute を UI に表示する

WBP_CustomGSCUserWidget の方で表示してみます。

UI に関しては単にグリッドの最終列に追加しました。

さて、表示ですが Health 等と違って専用の取得ノードは用意されていません。よって以下の Get Current Attribute Value を利用します。

以下のように表示することができました!(Armor の Base 値を 60 にしてしまったので、先程と値が変わっています。)

試してみて

今回はそれぞれのプラグインについてではなく、簡単にそれぞれがうまく噛み合うか確認しました。

GAS Companion と Blueprint Attributes は作者が同じということもあり、ある程度の棲み分けも考えて作られていると思いますが、どちらか単独でも十分に機能するように作られているように思います。

記述できる場所が違う( C++ か BP か、だけでなく)けれど同じことはできるような感じです。一方でこれは GAS を UE のデフォルトだけで書くのも同じ話ではあるので、このプラグイン達を便利に感じるかどうかはチームによると思います。

個人的には、サンプルも通して GAS の使い道を考える上で勉強になることが多かったです。それほど高額なプラグインでもないので検討に値するのではないかと思います。

UE5 で Skeletal Mesh Editing Tool を使って、物理挙動で動くだけのキャラクターを作る

第21回UE5ぷちコン|株式会社ヒストリア で以下の作品を提出しました。

第21回UE5ぷちコン 提出動画「Hand Push Sumo」 - YouTube

このゲームでは、Box や Sphere だけで構成された簡素な物体がプレイヤーキャラクターとなっています。

ただボタンを押したらキャラクターが腕を伸ばして相手を押すという、いわばレトロな対戦パンチゲームのような雰囲気を出すには、これぐらいがちょうどいいだろうという意図での選択でした。

時間が無くてプロトタイプで作ったキャラクターで進めるしかなかったというのもあります 🤣

このキャラクターはアニメーションは一切使っておらず、 Simulate Physics を有効にした状態で、ただ空中に浮いた腕を前に押し出す(そして自動で戻る)ということだけを行っています。

本記事では、このキャラクターを UE5 のみ で作成した過程を簡単にご紹介します。(本記事はコードがほとんど無いのでサンプルプロジェクトはありません)

空のステージを用意する

これは単に作業がしやすいからです。

既存のレベルを使うにしても、原点で作業した方が数値の設定がわかりやすいので良いとおもいます。

Modeling Tool でキャラクターの Static Mesh を作る

まずはキャラクターのメッシュを作ります。Modeling Mode に切り替えましょう。

1. Box 配置

何も無いので、まずは「Body」になる Box を配置します。

座標は原点にしておきます。

2. Body を Triangle Edit で調整

Mesh メニューにある、Triangle Edit で Body を調整していきます。

一つの面は三角が2つあるので、それらを選択し移動させて形状を変えていきます。今回は以下のように横長の形にしています。

また、今回は X がキャラクターの前面として作っているため、上記でも長いのは Y 方向です。

5. Sphere を設置し、縮める

これは頭部になります。今回は 0.6 倍しています。Pivot が下にあるので位置を最初に決めることができます。

初期サイズ 0.6倍後

6. 腕を作る

こちらも Box で作ります。Body 同様に形状を変えて、 最後に Pivot を変更します。

a. 形状変更 b. Pivot変更
c. 複製して両側に d. 少し Body から離す

Pivot 変更 (Edit Pivot) は XForm メニューにあります。

7. Merge して一つのメッシュにする

ここまでで作成したすべてのメッシュを選択した状態で XForm メニューの Merge でまとめます。

Pivot が真ん中になっていると思うので、足元 (原点で作業しているなら原点) に変えておきます。

そうすると、Outliner の表示が Combined の一つになっており、またそのメッシュ本体も以下のようにレベルがある場所と同じところの _GENERATED フォルダ内に保存されているはずです。

Contents Browser の Thumbnail Edit Mode でサムネイルの見た目を変えておくと便利です。
また、ここではこの Static Mesh の名前を SM_Study と名称を変更しています。

Skeletal Mesh Editing Tool でスケルタルメッシュを作る

1. プラグイン有効化

UE5.3.2 の時点ではまだデフォルトで有効では無いの有効化します。

  1. スケルタルメッシュに変換

Contents Browers で先程の Skeletal Mesh のコンテクストメニューから変換を実行します。

今回は Create New です。

そうすると、Skeleton と Skeletal Mesh が作成されます。

  1. Skeletal Mesh を開いて Root を確認

Pivot の変更がされていれば Root が以下のように足元になっているはずです。

  1. Skeletal Mesh の編集

引き続き Skeletal Mesh の画面です。
左のメニューに Skeleton があるはずです。もしなければ以下の上部の Editing Tools が有効になっているか確認してください。

さっそく Edit Skeleton してきます。

  1. Bone をつけていく

Edit Skeleton した状態で右ペインの Skeleton Tree にいくと、New Bone ができるようになっています。ボタンからでも Root のコンテクストメニューでも大丈夫です。(Edit Skeleton しないと New Bone が出ません)

最初は慣れないかもしれませんが、New Bone すると新しい joint が作成されるので、それを選択した状態で移動させます。

今回は最終的に以下のようにしました。

upper_arm_, lower_arm_ に関しては、Left 側だけを作って Mirror して複製しています。

Left 側を作り選択 Mirror を実行
複製される

Mirror に関してはデフォルトでは X 軸で Mirror されるようになっているので Y に変更しています。

6. Skin を付けていく

Skin に関しては 左クリックで塗るCtrl + 左クリックで消す だけここでは使います。

では、左の Skin メニューから、一度 Bind Skin し、そのまま Edit Weights していきましょう。

作成直後はこんな感じです。

今回は以下のようにしています。

Root spine_01 neck head
upper_arm_l lower_arm_l upper_arm_r lower_arm_r

上段は、Bind Skin したままです。
下段の arm 系は自身の arm 以外の weight を 0 に塗り直しています。それだけです。

私も Skin Weights は意図なくやっているので、参考にはならないと思います 😵

7. 確認する

Skeleton の画面に移動して、upper_arm_l などを動かして見ると、Skin Wieght の状況が確認できると思います。

ここまでで、Skeletal Mesh Editing Tool を使う部分は終わりになります。

Physics Asset を付ける

Physics Asset での目標は、「腕だけ前後に振る動きができる」というものです。

1. 生成し大きさを調整する

Contents Browser の Skeletal Mesh のコンテクストメニューから作成できます。

初期の生成メニューでは Capsule から box にだけ変えます。

生成直後は以下のような感じです。

まずは head だけ Sphere で作り直します。おそらく head と同じサイズになります。

以下のように大きさをメッシュに合わせます。neck 部分は少し spine_01 (body) を小さくして head の下に差し込んでいます。

spine_01 は足元まで延ばしておきます。Simulate Physics を ON にした時に倒れないようにするためです。

2. Constraint を調整する

upper_arm_l(r) に関するもの以外の Constraint を以下のように角度もロックします。

続いて upper_arm_l(r) に関する Constraint の設定です。以下に右側の例だけ示します。

対象 設定 状態

ただ、Constraint の方向によっては Angular Limits の設定だけをしても方向が異なる場合があります。
その場合は、回転ツールで Constraint 自体を回転させたり、Swing2 以外にしてみるなどしてみてください。

目的としては、腕の動きを下方向の X 方向にだけ限定したい というだけです。

Blurprint で動かす

1. 基本的な設定

作成した Skeletal Mesh を使って Pawn をベースにした BP を作成します。

Components Viewport

当然ながら SkeletalMesh の Simulate Physics は ON にしておきます。

2. Event Graph

腕を前に押し出すだけです。

3. Level Blueprint から呼び出す

先ほどの Pawn に Posses してもいいのですが、今回は雑に Level Blueprint から呼び出します。

レベルに配して参照を直接取得して以下のようにします。

4. 動作確認する

今回は以下のような動きになることを確認しています。

youtu.be

まとめ

Skeletal Mesh Editing Tool を使うことで、これまで Blender 等でする必要があった作業が(どの程度かはわかりませんが)代用できるようになりました。

まだ Experimental で発展途上だとは思いますが、可能性を感じる機能だと思います。

参考記事