[第14回UE4ぷちコン]振り返り:もぐら叩き

概要

    • ゲーム部分の設計と実装を振り返る

    • やりたい事を列挙、必要な仕組みを逆算

    • プロトタイプでの確認は重要

設計

何をどうする遊びなのか

カーソルを動かして防御したり攻撃する。

    • 防御

        • 挙動に癖のあるカーソルを

        • 一定時間内に

        • 敵攻撃マーカーの位置まで動かす

    • 攻撃

        • 挙動に癖のあるカーソルを

        • 時機を見て

        • 防御面では不利になる位置に移動させる

カーソルに自機の腕、マーカーに敵の腕が追従するので

結果としてなんか殴り合いみたいになるだろう。

分解

何が必要か洗い出す。

    • 挙動に癖のあるカーソル

        • 慣性の影響を強くする

        • 物理挙動に即した挙動で、プレイヤーが予測及び制御可能であって欲しい

        • 簡易な物理計算でカーソル位置を更新する仕組みが必要

    • 一定時間内に所定の場所まで動かす

        • 時間と場所指定を作る仕組みが必要

        • 時間と場所指定をプレイヤーに伝える仕組みが必要

    • 攻撃時機

        • こちらの攻撃が有効な時と、そうでない時の切り替えが必要

        • 攻撃時機がプレイヤーにわかる仕組みが必要

実装

プロトタイプ設計→実装→本番設計→実装という流れで進めた。

↓はプロトタイプ。

脳内でいろんなものを補完しているが

ある程度楽しさがあったので続ける事にした。

設計の時間をそれなりに長くとったので、

見た目の進捗が出ない日が続いたが、手戻りは少なかった…と思う。

プロトタイプの重要さを感じる。

以下ではプロトタイプの反省を踏まえた

本番用の実装について記す。

カーソル移動

簡易な物理計算

慣性のついた挙動の為、簡易なVerlet法の計算をC++で実装した。

C++を使ったのは単に自分の慣れの為。

必要な物理パラメータを以下のような形でまとめて構造体にして、

Blueprint側から調整可能にした。

USTRUCT(BlueprintType)

struct FCursorMoveParam

{

GENERATED_BODY()

public:

UPROPERTY(EditAnywhere, BlueprintReadWrite)

float ForwardAcc = 2000.0f; // カーソル移動方向にレバー倒したときの加速


UPROPERTY(EditAnywhere, BlueprintReadWrite)

float ReverseAcc = 2000.0f; // カーソルと逆にレバー倒したときの加速


UPROPERTY(EditAnywhere, BlueprintReadWrite)

FVector2D CursorRange = FVector2D(1000.0f, 750.0f); // カーソルの移動範囲(cm)


UPROPERTY(EditAnywhere, BlueprintReadWrite)

FVector2D CursorOrigin = FVector2D(0.0f, 0.0f); // カーソルの原点。ニュートラル時ここに収束する


UPROPERTY(EditAnywhere, BlueprintReadWrite)

float Spring = 5.0f; // Originに戻る力


UPROPERTY(EditAnywhere, BlueprintReadWrite)

float SpringMaxExtendRate = 0.1f; // CursorRangeの何割でバネ力をカンストさせるか


UPROPERTY(EditAnywhere, BlueprintReadWrite)

float Dumper = 1.0f; // ダンパ係数


UPROPERTY(EditAnywhere, BlueprintReadWrite)

float Drag = 0.98f; // 空気抵抗


UPROPERTY(EditAnywhere, BlueprintReadWrite)

FVector2D Alignment; // 防御領域の矩形中心設定。0.5,0.5でカーソル中心

};

実際の物理計算は以下のような実装になった。

徐々に初期位置に戻す部分は減衰振動

void UCursorController::UpdateGuard(float DeltaTime, const FVector2D& Input, CursorParam& Cursor) const

{

// 移動量

FVector2D move = Cursor.Pos - Cursor.PosPrev;

Cursor.PosPrev = Cursor.Pos;


// 速度 = 移動量 / 前Frameの経過時間

FVector2D vel = move / this->DeltaTimePrev;


// 簡易空気抵抗

vel *= this->MoveParam.Drag;


// 加速度

FVector2D acc(FVector2D::ZeroVector);


// 入力ニュートラルなら、じょじょに初期位置に戻す

if (Input.Size() == 0.0f) {

// 原点からの伸び

FVector2D x = this->MoveParam.CursorOrigin - Cursor.Pos;

const float x_len = x.Size();


if (x_len > 0.0f) {

// バネが強くなりすぎないように、ある程度で伸びは足切り

const float x_max = this->MoveParam.CursorRange.X * this->MoveParam.SpringMaxExtendRate;

if (x_len > x_max) {

x *= x_max / x_len;

}

}


// 加速度*質量 = 力 = バネ定数 * 原点からの伸び - ダンパ係数 * 速度

// 質量は1.0kgとおいて省略

acc = this->MoveParam.Spring * x - this->MoveParam.Dumper * vel;


} else {

// 入力がある場合


// ブレーキを強める調整の為、入力と移動方向を見て加速度切り替え

acc.X = (move.X * Input.X >= 0) ? this->MoveParam.ForwardAcc : this->MoveParam.ReverseAcc;

acc.Y = (move.Y * Input.Y >= 0) ? this->MoveParam.ForwardAcc : this->MoveParam.ReverseAcc;


// アナログ入力量に応じて加速

acc *= Input;

}


// 境界を超えている場合、その方向の加速度を内側に戻す力で上書き

if (FMath::Abs(Cursor.Pos.X) > this->AlignedCursorRange.Max.X) {

acc.X = -FMath::Sign(Cursor.Pos.X) * this->MoveParam.ForwardAcc;

}

if (FMath::Abs(Cursor.Pos.Y) > this->AlignedCursorRange.Max.Y) {

acc.Y = -FMath::Sign(Cursor.Pos.Y) * this->MoveParam.ForwardAcc;

}


// 座標更新

Cursor.Pos += vel * DeltaTime + acc * DeltaTime * DeltaTime;

}

この計算を持つUCursorControllerはActor Component派生Classである。

これをPlayer Controllerにくっつけて使っている。

UIとの分離

カーソルの「現在位置」を「動かせる距離」で割れば、

位置情報をcmから0から1の割合に変換できる。

敵のパンチ座標を決める時も

この割合で位置を指定すればよい。

今回は、自分のわかりやすさの都合で

-1から+1の範囲になるように値を加工した。

UMGにはこの割合で情報を渡す。

座標のほかに、防御領域の縦横幅も割合で渡している。

1:1にすると、カーソルを中央に置くだけで全域を防御できることになる。

CursorAreaの子としてCursorがある。

渡された割合情報をCursorAreaの縦横の大きさにかけ合わせれば

Cursorの位置を計算できる。

割合で中継すると色々と利点がある。

    • 物理計算パラメータ変更が表示に影響しない

        • 逆に言えば、CursorAreaの中心位置や大きさの変更は物理挙動に影響しない

        • 物理パラメータは現実に即した値で書いてよいし、既知のパラメータセットを持ち込める

            • 今後こういう挙動を作る時に使いまわせる

    • 敵パンチ位置の決定計算にも影響しない

        • 最初から割合の値で座標決定すればよい

具体的なエピソードとして、

自機Skeletal Meshの腕位置をCursorに追従させる実装を行った際に

左右の腕を近づけると3D的にめり込む不具合が出たが、

UMG上でCursorArea位置と形状のみ調整するだけで

ゲームの難易度に影響させずに修正できた。

出題設定

いつ、どの位置にパンチが来るか設定する仕組みを考える。

要件整理

どういう出題をしたいのかを考える。

    • 左右交互にパンチ

    • 左右ランダムパンチ

    • 素早いが決まった位置に初見殺しパンチ

    • アドリブ対応可能な時間でランダムな位置にパンチ

    • 予告だけ前もっていっぱい表示されて焦るパンチ

どういうパラメータがあれば実現可能か整理する。

構造体定義

パンチ一発につき以下のようなパラメータを持たせることにした。

時刻関連は冗長なものもあるが、わかりやすさを重視した。

これを複数積み重ねると、連続パンチになる。

パンチのスイングにかかる時間と、パンチの回数を主な入力にして

上記構造体の配列を作る関数を作成した。

後は、配列内の要素の時刻パラメータを追いかけながら

パンチマーカーを置いたりロボを動かしたりすればよい。

場所と時刻の表示

単純なマーカーWidgetを作成して対応。

設置場所は、上述のCursorAreaの子にする。

1秒の尺で出現から収束までのAnimationを設定しておき、

Widgetの設置時に「マーカー設置から着弾までの時間」の逆数をPlay Rateにすれば

任意時間で再生できる。

余談:Sequencerで出題設定しようとして失敗

Sequencerでマーカーを置いたりパンチするタイミングを指定すれば

視覚的に敵の動きが設定できて良いかなと思って試してみた。

Event Trackを複数用意して、

間合いの前後や左右パンチのタイミングを設定していけば

敵にやらせたい行動が視覚的に設定できるかなと思ったが、

以下の躓きで諦めた。

そもそも、

「予告だけ前もっていっぱい表示されて焦るパンチ」をやる為に

パンチする時刻を前もって知りたいが、

SequencerのEvent時刻を簡単に集計する仕組みが無いので

手間と見合わないというのもある。

攻撃時機

「敵の攻撃をひととおり耐えて殴り返す」

という楽しみを再現したい。

だが、単純にパンチを実装すると

敵が近接する瞬間に殴って追い返すプレイや、

敵のパンチとパンチの合間を縫って殴るプレイが最適になってしまう。

もちろんそれはそれできっと楽しいが、

それは上級なプレイとして据え置いて、まずは

「一通り耐えてから殴る」

の実現を目指した。

「敵が殴り終わってから攻撃して下さい」

という事をどのように説明すればよいか。

敵も防御姿勢を取ればよいかな、と思い、

敵も腕を上げて防御する動きを作ってみた。

だが、その腕で殴る必要ができた時に明らかに隙ができるし、

そこを殴っても効果が無いというのは納得いかない。

敵も防御の腕を動かせばよいか…と考えたが

攻撃する動きと防御する動きを同時に再生して整合を取る自信が無かったので、

単純にバリアを張ってしまう事にした。

これで、敵の行動ルーチンが簡単になった。

近づく→殴る×n→バリア解除(攻撃チャンス)→下がってバリア貼る→近づく→……

実験で作った防御姿勢はバリア貼る時のポーズとして流用した。

参考にしたもの

    • 剣神ドラゴンクエストのボスバトル

        • 防ぐ時と殴る時が明確に分かれているが、RTAを見ると結構攻撃を挟む余地などがあって懐の深さを感じる

    • がんばれゴエモンシリーズのインパクト戦

        • 自分もビーム撃ったり高速移動したり百裂パンチしたい

反省

    • 最終的な成果の礎にはなっているか、ムダ作業が多少発生している

        • 十分に設計をしてから手を動かしたかったが、

        • 「こっから先は動かしてみねぇとわからねぃ!」

        • みたいな謎の職人が脳内に時折出没してしまう。

            • 適切なタイミングで出没させられるようになりたい。

    • 「プレイヤー側がどうすれば攻撃できるか」の説明が弱い

        • 腕を動かしまくればそのうち気づけるだろうが、UI的にもう少しヒントがあるべき

    • バリア発生と解除の説明が弱い

        • 今はバリア発動演出で一旦エフェクトが消えているが、色を薄くして貼りっぱなしにするほうがいいかも

    • 仕組み作りで力尽きて、あまり攻撃パターンを作れていない

        • すまねぇ…すまねぇ…

        • 一番やりたかった「予告だけ前もっていっぱい表示されて焦るパンチ」はやれて良かった…

    • 現状は小さくまとめ過ぎた感がある

        • 「速度乗ってる状態で防御したらパリィ」とかもやりたかった…

            • でも、それやるなら敵も相応に色々してきて欲しい気もするし…

        • 「サクっと作ってサクっと応募」という趣旨的にはこれぐらいでいいのかも?

以上