😺

[Unity]歩く時に頭の揺れを表現する[FPS]

2024/04/17に公開

FPSといえば64の007ゴールデンアイ世代

おはこんばんにちは、ゆきおです。
FPSデビューは小学生、Nintendo64の007ゴールデンアイとパーフェクトダーク
からのF.E.A.RやHarf-Life、そしてカウンターストライクで育ってしまった元廃ゲーマーです。

今日はFPSゲームにリアリティを持たせる上で欠かせない、ヘッドボブについて書いていきます。

ヘッドボブとは

アメリカンな文化なのでジャパニーズには馴染みがないですが、
「海外の映画やドラマでありがちな、車の中で延々と頭フリフリしてるデフォルメされた野球選手、もしくはサングラスかけた陽気なヒマワリの人形」
って言ったら大体の人には通じると思っています。

あれボビングヘッドって言います。

頭の揺れとか、歩いてる時の視界の上下の揺れのことですね。

そのワードをヒントにだいぶ古いUnityのリポジトリか何かを見つけてなんとか実装に漕ぎつけたのでアウトプットしようと思い立ちました。

とはいえこれを実装したのはUnityを勉強したての頃で、改めてどうなってるのか分析したいと思います。

Enum,Delegate

// Enumerations
public enum PlayerMoveStatus { NotMoving, Crouching, Walking, Running, NotGrounded, Landing }
public enum CurveControlledBobCallbackType { Horizontal, Vertical }

// Delegates
public delegate void CurveControlledBobCallback();

まず歩いてるんだかしゃがんでるんだか走ってるんだかキャラクターの状態の定義と
視点の揺れの方向が縦横、つまりX軸とY軸
そしてデリゲートの定義です。あとで足音を再生するメソッドを登録します。

走ってるときは揺れを早く小刻みにしたい、しゃがんでる時はゆっくり大きくしたい。てな風に使います。

クラスに関しては優秀な助手(Claude3)に解説していただきます。

CurveControlledBobクラス

[System.Serializable]
public class CurveControlledBob
{
	[SerializeField]
	AnimationCurve _bobcurve = new AnimationCurve(new Keyframe(0f, 0f), new Keyframe(0.5f, 1f),
																	new Keyframe(1f, 0f), new Keyframe(1.5f, -1f),
																	new Keyframe(2f, 0f));

	// Inspector Assigned Bob Control Variables
	[SerializeField] float _horizontalMultiplier = 0.01f;
	[SerializeField] float _verticalMultiplier = 0.02f;
	[SerializeField] float _verticaltoHorizontalSpeedRatio = 2.0f;
	[SerializeField] float _baseInterval = 1.0f;

	// Internals
	private float _prevXPlayHead;
	private float _prevYPlayHead;
	private float _xPlayHead;
	private float _yPlayHead;
	private float _curveEndTime;
	private List<CurveControlledBobEvent> _events = new List<CurveControlledBobEvent>();

	public void Initialize()
	{
		// Record time length of bob curve
		_curveEndTime = _bobcurve[_bobcurve.length - 1].time;
		_xPlayHead = 0.0f;
		_yPlayHead = 0.0f;
		_prevXPlayHead = 0.0f;
		_prevYPlayHead = 0.0f;
	}

	public void RegisterEventCallback(float time, CurveControlledBobCallback function, CurveControlledBobCallbackType type)
	{
		CurveControlledBobEvent ccbeEvent = new CurveControlledBobEvent();
		ccbeEvent.Time = time;
		ccbeEvent.Function = function;
		ccbeEvent.Type = type;
		_events.Add(ccbeEvent);
		_events.Sort(
			delegate (CurveControlledBobEvent t1, CurveControlledBobEvent t2)
			{
				return (t1.Time.CompareTo(t2.Time));
			}
		);
	}

	public Vector3 GetVectorOffset(float speed)
	{
		_xPlayHead += (speed * Time.deltaTime) / _baseInterval;
		_yPlayHead += ((speed * Time.deltaTime) / _baseInterval) * _verticaltoHorizontalSpeedRatio;

		if (_xPlayHead > _curveEndTime)
			_xPlayHead -= _curveEndTime;

		if (_yPlayHead > _curveEndTime)
			_yPlayHead -= _curveEndTime;

		// Process Events
		for (int i = 0; i < _events.Count; i++)
		{
			CurveControlledBobEvent ev = _events[i];
			if (ev != null)
			{
				if (ev.Type == CurveControlledBobCallbackType.Vertical)
				{
					if ((_prevYPlayHead < ev.Time && _yPlayHead >= ev.Time) ||
						(_prevYPlayHead > _yPlayHead && (ev.Time > _prevYPlayHead || ev.Time <= _yPlayHead)))
					{
						ev.Function();
					}
				}
				else
				{
					if ((_prevXPlayHead < ev.Time && _xPlayHead >= ev.Time) ||
						(_prevXPlayHead > _xPlayHead && (ev.Time > _prevXPlayHead || ev.Time <= _xPlayHead)))
					{
						ev.Function();
					}
				}
			}
		}

		float xPos = _bobcurve.Evaluate(_xPlayHead) * _horizontalMultiplier;
		float yPos = _bobcurve.Evaluate(_yPlayHead) * _verticalMultiplier;

		_prevXPlayHead = _xPlayHead;
		_prevYPlayHead = _yPlayHead;

		return new Vector3(xPos, yPos, 0f);
	}
}

CurveControlledBobクラス:

このクラスは、カメラの揺れ(Head Bob)を制御するための主要なコンポーネントです。
AnimationCurveである_bobcurveを使用して、歩行中のカメラの上下左右の動きを定義します。
_horizontalMultiplierと_verticalMultiplierは、カメラの揺れの大きさを調整するためのパラメータです。
_verticaltoHorizontalSpeedRatioは、上下と左右の動きの速度比を調整するためのパラメータです。
_baseIntervalは、カメラの揺れの間隔を制御するためのパラメータです。
_xPlayHeadと_yPlayHeadは、AnimationCurve上の現在の位置を表すための内部変数です。
_curveEndTimeは、AnimationCurveの終了時間を保持するための内部変数です。
_eventsは、特定のタイミングで呼び出されるコールバック関数を管理するためのリストです。

主要なメソッドは以下の通りです:
Initialize: 内部変数を初期化します。
RegisterEventCallback: コールバック関数を登録します。
GetVectorOffset: 現在の速度に基づいてカメラの位置のオフセットを計算します。

CurveControlledBobEventクラス

[System.Serializable]
public class CurveControlledBobEvent
{
	public float Time = 0.0f;
	public CurveControlledBobCallback Function = null;
	public CurveControlledBobCallbackType Type = CurveControlledBobCallbackType.Vertical;
}

CurveControlledBobEventクラス:

このクラスは、CurveControlledBobクラスのコールバック関数を管理するためのデータ構造です。
Timeは、コールバック関数が呼び出されるタイミングを指定します。
Functionは、呼び出されるコールバック関数自体を保持します。
Typeは、コールバック関数が上下方向(Vertical)または左右方向(Horizontal)のどちらに関連するかを指定します。

だそうです。
コードだけ見てもピンとこないと思うので、エディタの方を見てみるとなんとなく仕組みはわかるかもしれません。

スクリーンショット 2024-04-16 21.27.31.png

Starter AssetsのFPS Controllerを無理くり拡張しているのでコードはとっ散らかってますがインスペクターみるとイメージしやすいかと思います。

Head BobセクションのBobCurveの部分はコードで言うと以下の部分です。

[SerializeField]
AnimationCurve _bobcurve = new AnimationCurve(new Keyframe(0f, 0f), new Keyframe(0.5f, 1f),
																	new Keyframe(1f, 0f), new Keyframe(1.5f, -1f),
																	new Keyframe(2f, 0f));

キーフレームを生成してX軸、Y軸のそれぞれ指定したポイントに点を打ち、それを線で繋ぐとこのような波になりますね。カメラはこの動きをトレースします。

そして縦にどのくらい動くか、横にどのくらい動くかというのを指定しています。
これで緩やかな∞を描くように動くわけですね。

んで歩いている時と走っている時というのは揺れのスピードが変わりますので、それを変数で定義しています

_horizontalMultiplier(水平方向の乗数):
このパラメータは、カメラの水平方向の揺れの大きさを制御します。
AnimationCurveから計算された水平方向のオフセット値に、この乗数が掛けられます。
値が大きいほど、水平方向の揺れが大きくなります。

_verticalMultiplier(垂直方向の乗数):
このパラメータは、カメラの垂直方向の揺れの大きさを制御します。
AnimationCurveから計算された垂直方向のオフセット値に、この乗数が掛けられます。
値が大きいほど、垂直方向の揺れが大きくなります。

_verticaltoHorizontalSpeedRatio(垂直方向と水平方向の速度比):
このパラメータは、カメラの垂直方向と水平方向の揺れの速度比を制御します。
値が大きいほど、垂直方向の揺れが水平方向の揺れに対して相対的に速くなります。
これにより、歩行中のカメラの揺れの挙動を調整できます。

_baseInterval(基本間隔):
このパラメータは、カメラの揺れの間隔を制御します。
値が小さいほど、揺れの間隔が短くなり、揺れが速くなります。
値が大きいほど、揺れの間隔が長くなり、揺れがゆっくりになります。

おまけ:足音の再生

揺れが表現できたらついでに足音を追加しよう!てことで足音も実装しました。
先ほど作成したキーフレームが特定の位置に来た時に再生すると言う仕組みです。
足音はフリーアセットでいっぱいあるので、自分が作ったワールドに合う足音を拾ってきましょう。
微妙にトーンの違う足音が2種類用意されているので、交互に再生することでちゃんと歩いてる感が出せます。

ちなみにマップが広くて色んなシチュエーションがある場合は設置した地面をどうにかして判別してケース毎に土とか木の床とか雪の上とかを再生し分ければ良いのかなと思います。

void PlayFootStepSound()
{
	AudioSources[_audioToUse].Play();
	_audioToUse = (_audioToUse == 0) ? 1 : 0;
}

三項演算の部分は足音1と足音2を交互に再生するためのやつです

このメソッドをデリゲートに登録して

_headBob.RegisterEventCallback(1.5f, PlayFootStepSound, CurveControlledBobCallbackType.Vertical);

Verticalが1.5fの時にPlayFootStepSoundを実行する、つまりカーブの赤線部分です
スクリーンショット 2024-04-16 21.45.37.png
         ↑ここ

[SerializeField]
new AnimationCurve(new Keyframe(0f, 0f), 
                   new Keyframe(0.5f, 1f),
                   new Keyframe(1f, 0f),
                   new Keyframe(1.5f, -1f),←ココ!
                   new Keyframe(2f, 0f));

なんでここかというと、足音が鳴るのは足がついた時、つまり頭の揺れが下に来た時ですよね。それがこのポイントです。
意識して自分で歩いてみるとイメージしやすいです。
頭の上下と足の動きはほぼ連動していて、頭が上がるタイミングで足を地面に着けて歩いてみるとすごいキモいのがわかると思います。(部屋で一人の時にやってください

おわり

すんごいざっくりですがこんな感じでFPSあるある、視界の揺れを実装できました。
これをやらないと横滑りホバリング主人公になるのでイマイチリアリティに欠けるんですよね。

そして今となっては当たり前の対話型AIを使って解説してもらうと勉強になります…

ではでは、明日も頑張りましょう。

Discussion