この記事はUnity Advent Calendar 2022 その3 24日目の記事です。
qiita.com
今回は、ダークソウルのようなカメラワークをCinemachineで実装してみます。
記事内ではゲームパッドで操作することを前提としています。
ちょっと不具合があったり、機能を省いていますがご了承ください。
ダークソウルのようなカメラワークとは
ダークソウルとは3人称視点のアクションRPGです。
一般的な3人称視点のゲームと同様、カメラはプレイヤーの移動に合わせて追従し、
プレイヤーを中心に回転します。
ゲームパッドでは左スティックで移動、右スティックで視点移動をします。
今回はダークソウル3をベースとした移動とカメラワークをUnityで実装します。
具体的には
・左スティックでの移動
・右スティックでの視点移動
・ジャンプ機能(これはSEKIROから)
・R3(右スティック押し込み)でのロックオン
の4つを実装します。
開発環境
Unity 2021.3.12f1
Cinemachine ver2.8.9
Input System ver1.4.4
プロジェクトはURPですがBuilt-inでも問題ありません。
必須ではありませんが、ステージ用アセットとしてこちらも使用します。
プロジェクトの設定
パッケージのインストール
UPM(Unity Package Manager)からCinemachineとInput Systemをインストールします
Input Systemをインストールすると、このようなダイアログを表示されるのでYesを押し再起動します。
再起動後、ProjectSettings/Player/OtherSettings/Configration/ActiveInputValidationが
Both、またはInputSystemPackage(New)になっていることを確認します。
なっていない(Input Managerのまま)場合は変更してください。
シーンの作成
インポートしたステージアセットに含まれている、SpeedTutor Test Scene.sceneをそのまま使います。
元のシーンを残しておきたい方は別名で保存するといいですね。
このシーン内に含まれているCameraは、TagがUntaggedになっているので、
TagをMainCameraに変更します。変更しないと後々エラーがでます。
また、ステージがそのままだとやや狭いので、DemoScene DecorationのScaleを全て2にしておきます。
上記のステージアセットを使わない場合は、Create Sceneでシーンを作成します。
Playerが動けるようPlaneだけ置いておきます。障害物などは必要ありません。
プレイヤーの作成
まずはプレイヤーを作成します。
Create/3D Object/CapsuleからCapsuleを作成。名前をPlayerにし、Rigidbodyをアタッチします。
RigidbodyはConstraintsのFreezeRotationにチェックしておきます。
追加でオブジェクトを作成
Playerの方向を取得するオブジェクトと、接地判定用オブジェクトを作成します。
Playerの方向を取得するため、Playerの子オブジェクトとして空のGameObjectを作成。
名前はOrientationにします。位置や角度はそのまま。
同じように設置判定をするGameObjectを作成。
名前はGroundCheckにします。
このGroundCheckはPlayerの足元に配置します。
移動制御Scriptの作成
以下の、PlayerMove.csをPlayerにアタッチします。
PlayerTrn、OrientationTrn、GroundCheckTrnにそれぞれオブジェクトを紐づけ、
WhatIsGroundはDefaultを設定します
Input Systemで入力を受け取りたいのでその対応をします。
PlayerにPlayer Inputコンポーネントをアタッチ。
Player Input内のCreate Actionsボタンを押して、Input Action Assetを作成。
作成後、Input Action Assetの設定をしていきます。
このままでも基本的な移動や視点移動はできますが、ジャンプとロックオン機能を実装したいため、新しいActionを追加します。
PlayerマップのActionsにJumpを追加、Action TypeはButtonを設定。
BindingのPathはGamepad/Button Southを設定、Use in control schemeのGamepadにチェックを入れます。
同じ手順でLockon Actionを追加、BindingはGamepad/Right Stick Pressを設定。
最後にウインドウ上部のSaveAssetで保存します。
保存したら、このウインドウは閉じても大丈夫です。
ここまでできたらゲームを実行してみましょう。
ゲームパッドの入力で移動とジャンプができると思います。
左スティックで移動、下のボタン(PS配置なら×ボタン)でジャンプします。
カメラの作成
次にCameraを動かせるようにします。
シーン内にCinemachine/Virtual Camera*1でCM vcam1を作成
Inspectorビューから設定していきます。
FollowにPlayerを紐づけます。
Bodyの設定
BodyはCameraの移動を制御する項目です。
BodyをFraming Transposerに設定
これはCameraがターゲットに対して(スクリーン空間上で)一定の距離を保ちながら追跡するアルゴリズムです。
パラメータはこのままにします。
Aimの設定
AimはCameraの回転を制御する項目です。
AimをPOVに設定
これはCameraをユーザーの入力に合わせて回転させるアルゴリズムです。
こちらのパラメータもそのままにします。
初期設定では旧Input Managerによる入力を使用してしまうので、Input Systemを使うよう変更します
CinemachineVirtualCameraコンポーネントの上部にある、Add Input Providerボタンを押します。
ボタンを押すと、CM vcam1にPlayer Inputコンポーネントが自動的にアタッチされます。
これはCinemachineでInput Systemの入力を受け取り処理できるようにするコンポーネントです。
XY Axisに最初からActionが紐づけられています。
ここには先ほど作ったInput Action AssetのActionではなく、デフォルトで用意されているActionが使われていますが、機能的には同じなのでこのままでも問題ありません。
この状態でゲームを実行してみましょう。
右スティックの入力に合わせてカメラが回転します。
これでキャラクターの移動とカメラの視点移動を実装することができました。
ここから先はおまけです。
Cinemachineの詳細設定
Cameraの更新設定(カクつきの対策)
Playerの移動をFixed Updateで行っている時限定
このまま動かすと、Playerが移動するたびCameraがガクガクします。
Playerの移動はFixed Updateで行っているのでCameraの更新タイミングを変更すると解決します。
Main CameraにアタッチされているCinemachineBrainのUpdate MethodをFixed Updateに変更。
(CinemachineBrainはVirtualCameraをシーンに作成すると自動でアタッチされます)
Cameraが地形を貫通しないようにする
カメラが壁や地面を貫通しないようにします。
CinemachineVirtualCameraコンポーネントのExtensions/Add ExtensionからCinemachineCollierを選択。
CinemachineColliderコンポーネントがアタッチされるので、StrategyをPullCameraForwardに設定。
これはカメラがどうやって視線を確保するかを決める設定です。
これでカメラが地形を貫通しないようになりました
カメラが少しめり込んで裏世界が見えてしまう場合、CinemachineColliderのCameraRadiusを大きくすると改善します。
今回は省略してますが、敵やプレイヤーのColliderは無視してほしいので、レイヤーを分け壁や地面とだけ衝突するよう設定しておくのがいいですね。
よりダークソウルっぽいカメラ設定
カメラのパラメータはほとんど初期状態なので、よりダークソウルに近い設定をしてみます。
カメラの中心点
ダークソウルのプレイ動画を見ると、画面の中心はプレイヤーキャラクターの顔~首周辺です。
今の設定だとキャラクターのお腹あたりが中心点になってしまうのでここを変えます。
Playerの子供に空のGameObjectを作成、名前をGazeにします。
Gazeはキャラクターの頭当たりに配置します。
カメラの中心になるよう、CinemachineVirtualCameraコンポーネントのFollowに設定します。
距離と角度制限
カメラの距離は結構近めなためCameraDistanceは4。
角度については、上方向に対し下方向はそこそこ制限があるのでこんな感じ。
(ちなみにPlayerMove.csのバグで、カメラが真上付近になるとプレイヤーの移動に支障がでちゃいます)
また、入力の反転はInvertで切り替えます。
これだけでダークソウルにちょっと近づきましたね。
追従速度など細かいところまでやりだすとキリがないのでこの辺りまで。
カメラのデーモン対策
カメラのデーモンとは
カメラのデーモンはダークソウルシリーズでよく用いられる単語で、
・狭い場所や壁際でカメラが激しく動く
・障害物によりカメラの動きが制限される、または勝手に動く
・敵にカメラを遮られて何も見えなくなる
など、カメラワークによってゲームプレイが阻害される問題全般を指します。
SEKIROでは井戸底の孤影衆が有名で、場所がかなり狭く壁際に押し込まれると何も見えなくなることもあり、忍法クソカメラの術と呼ばれたりします。
以下Youtubeからの引用です(特に0:10~0:15あたり)
youtu.be
大型の敵に関しては、見上げるような視点で地形の把握ができなくなったり、
敵のモーションが見えにくい問題が発生します。
そのため、ロックオンを使わず戦うのが推奨されることもあります。
youtu.be
対策方法
対策方法として、モンスターハンターが参考になるので取り上げます。
モンスターハンターもカメラの問題に悩まされてきましたが*2、ワールドから大きく改善されました。
一部はSEKIRO以降のフロム作品でもたびたび見られます
オブジェクトを透過(ディザ抜き)する
カメラとプレイヤーの間に挟まれたオブジェクトを透過させる手法。
透過とは書きましたが、実際にはディザ抜きのケースが多いようです。
プレイヤーが壁と密着しないようにする
プレイヤーの移動可能範囲と壁の間にカメラ用スペースを確保し、プレイヤーが壁と密着できないようにする。
プラスして透過オブジェクトを間に挟む手法が、モンスターハンターワールドでは広く使われています。
図にするとこんな感じ↓
密着できる場所もあるにはありますが、ほとんど気にならないレベル。
まあダークソウルではわざと壁に接触し、装備やコーデを眺めて楽しむといった遊び方があったりしますが。
モンスターと重なった時プレイヤーを透過する
アイスボーンから追加された機能で、プレイヤーがモンスターと重なった時にプレイヤーのシルエットを表示する手法です。
ライズではアウトライン表示になりました。
今回はモンスターハンターから3つの手法を取り上げました。
これらを使うことで快適(当社比)なカメラワークを実現できると思います。
試しに、プレイヤーと壁の間にオブジェクトを挟んでみるとこんな感じ。
透過処理はシルエットで代用。
Playerと接触するがCinemachineCollierは無視するレイヤーのColliderを配置すれば簡単に実装できます。
アウトラインはアセットを使いました。
ダークソウルでは、カメラが荒ぶる時ロックオンを強制的に解除するという措置が取られたりもします。
ゲームごとにさまざまな工夫が凝らされているので、いろいろ観察してみるのもいいですね。
ロックオン機能の実装(仮)
おまけで、ダークソウルシリーズで使われるロックオン機能を実装してみます。
手順としては、ロックオン時にVirtualCameraを切り替えて対応します。
※プログラム的な問題点が多いです。ご了承ください。
ロックオン時のカメラを作成
ロックオン時にのみ使われるカメラを作成します。
新しくVirtual Cameraを作成。
Priorityは0、FollowにはGaze、BodyはFraming Transposerにします。
Bodyの値は最初に作ったCM vcam1と同じにしておいたほうがよさそう。
ダークソウルではターゲットとプレイヤーが重ならないように、画面中央よりやや上側にターゲットがくるようになっています。
これを再現するためDamping、Screen Y、Soft Zoneの大きさをそれぞれ調整します。
敵の作成
ロックオン対象となる敵を作成します。まずは判定用のEnemyレイヤーを定義。
Playerと同じ要領でCapsuleを作成し名前をEnemyに変更、LayerはEnemyに設定。
場所は適当な所でOK。
※プログラムのバグで、ロックオン対象との位置取りによっては挙動がおかしくなるかも
ロックオンカーソルを作成
ロックオンしてることがわかるUIを作成します。
Canvasとその子にImageを作成。
カーソル用に適当な画像を用意して使います。
最後に、このImageオブジェクトは必ず非アクティブにしてください。
カメラを制御するScritpの作成
以下の、PlayerCamera.csをPlayerにアタッチします。
FreeLookCameraには通常時のVirtualCameraを、LockonCameraには先ほど作ったロックオン用VirtualCameraを紐づけます。
ロックオンを行うScriptの作成
以下の、PlayerLockon.csをPlayerにアタッチします。
PlayerCameraには先ほどのPlayerCameraコンポーネントを、OriginTrnにはPlayerを、TargetLayerNameにはEnemyを、LockonCursorには先ほど作ったカーソルをそれぞれ紐づけます。
VirtualCameraの切り替え速度
デフォルトだと切り替え速度が遅いので、MainCameraにあるCinemachineBrainコンポーネントのDefaultBlendの値を0.5に変更します。
これでとりあえず完成。
カメラの正面方向にEnemyがいる状態で右スティックを押すとロックオン状態になり、再度押すとロックオンが解除されます。
正直あやしい挙動があったり、ダークソウルより動きが硬い気もしますがまあここまで。
ロックオン対象がいない場合はプレイヤーの向いている方向にカメラをリセットする機能が必要ですが今回はないです。
ロックオンカメラにCinemachineColliderをつける場合
割と致命的な問題として、ロックオン用カメラにCinemachineColliderがある状態で障害物をターゲットとの間に挟むとカメラが荒ぶる問題があります。
そのため、カメラとターゲットとの間にある障害物は、いい感じに衝突判定を無効化するなど工夫が必要です。
良い解決方法が思い浮かばなかったので対応していませんが、CinemachineColliderのIgnoreTagやTransparentLayersなどがヒントになりそう。
まとめ
今回はCinemachineを使ってダークソウルっぽいカメラワーク+移動処理を実装しました。
移動、視点移動、ジャンプ、ロックオン機能にCinemachineの調整でダークソウルっぽい見た目に仕上がったと思います。
3人称視点のゲームであればいろいろ応用できると思うのでぜひ遊び倒してください。
問題点
解決できていない問題点が複数あります
時間がなかったのでそのままです。ごめんなさい。
・ジャンプ中に壁と接触すると浮遊する
・カメラが真上付近にあるとプレイヤーが正常に回転できなくなる
・ロックオン中に右スティックでターゲットを切り替える機能がない
・ロックオン対象がいない時にカメラをプレイヤーの向いている方向にリセットする機能がない
・ロックオン中、ターゲットとの距離が離れた時に解除する機能がない
・ロックオンする敵との位置関係によってはプレイヤーが正常に移動できなくなる
・ロックオン用カメラにCinemachineColliderがあると障害物を挟んだ時にカメラが荒ぶる
・ロックオン時に前回のロックオン時の角度を引き継いで一瞬揺れることがある。
改善点などがありましたらコメントなど気軽にお願いします!
まさかり大歓迎です!
参考資料
キャラクターコントローラー関係
移動に関する処理は↓の動画を参考にしました
youtu.be
Cinemachine関係
www.youtube.com
www.youtube.com
nekojara.city
ロックオン機能関係
最初はTargetGroupでやろうとしたがロックオン系には不向きみたいなのでやめました
forum.unity.com
forum.unity.com
www.youtube.com
youtu.be
ロックオン機能のScriptは↓2つの記事を参考にしました
ChatGPTの力も借りています
gomafrontier.com
qiita.com
その他
nekojara.city
raspberly.hateblo.jp
raspberly.hateblo.jp
raspberly.hateblo.jp
raspberly.hateblo.jp
以上です。