Last Updated 2023.10.17
Chinemachineの使い方や作例を日本語で解説したサイトも増えてきてますので、導入方法等の解説はここでは取り扱いません。いえ、本当に長くなってしまうので。
基礎的な用語や使い方は、公式や、各種検索を活用してみて下さい。
まだまだ初心者から抜け出せない自分が解説するほど滑稽なこともありませんしね……
本題に入る前に、なぜこんなことに手をつけているのか少しだけ触れてみたり。
UnityにはストⅣライクな格ゲーを作成できる、有名なアセットも存在しているのですが……とあるゲームで『軸』の概念に魅了された身として、自分が作ってみたいのは『3Dモデルを使った2D格ゲー』ではないのです。というか、そういうのがやりたいなら普通に本家やります。
自分が愛して止まず、その流れを汲み取りたいのは、埋もれていった数多のマイナー3D格ゲーなのです。そのためには、どうしても『3D格ゲーっぽいカメラワーク』が必要になってきます。
件のアセットにも、3D格ゲーライクなカメラワークがちゃんと搭載されているバージョンもあるのですがここでは割愛します。
かくして、自分のような初心者にも出来る方法を模索してみました。
簡単なスクリプトで得られる情報(オブジェクトの位置)から、三角関数を使って目的位置の距離や角度を計算する方法は、『紙面上』だと理解できるんですが、Unityで実際に動かす場合、最終的にカメラを移動させる座標を出さないといけません。
で、その部分が上手くいかなかったり、そもそもの前提として
Cinemachineのバーチャルカメラ機能を色々設定して使うと、カメラのTransformの値を直接上書きすることが出来なくなります。
いえ、出来るのかもしれませんが、少なくとも自分が試した範囲では無理でした。
ならバーチャルカメラを使わなきゃいいじゃん。という意見はごもっともなのですが、バーチャルカメラで使える機能自体は、とても便利で魅力的なんですよね。初心者ほどメリットが大きいというか。
結局このカメラワーク問題。ゲーム制作に着手し始めた頃から『だってわかんないし……』と逃げ続けてきたんですが、流石にそういう訳にもいかない段階にきてしまったので、10月に入ってから試行錯誤を繰り返しておりました。
▽ 数日かけて、出来上がったものがこちら
背景がアレなので望遠だと解りづらいんですが、近距離だとちゃんと解る程度には回転してくれてますね。
TargetGroupCameraの配置
複数のターゲットを画面内に収める動きは、Cinemacineに『CinemachineGroupComposer』という機能があるので、簡単な設定だけで実装できます。
ヒエラルキー欄 ⇒ Cinemachine ⇒ TargetGroupCamera を選択で
[CM vcam1]と[TargetGroup1]というオブジェクトが追加されます。
この[TargetGroup1]に1Pと2Pを設定する形ですね。
細かい設定方法は、公式や解説サイト等ありますので、ここでは割愛します。
実際のスクリプト
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Cinemachine;
public class CameraControl : MonoBehaviour
{
private GameObject Player1;
private GameObject Player2;
[SerializeField]
private float angle;
[SerializeField]
private float axisDifference;//軸ズレの値
private Vector3 centerPos;//中間点の格納庫
private Vector3 _1PViewportPos;//1Pのビューポート座標格納庫
//軸あわせ時のカメラの回転速度
[SerializeField]
private float clockwiseSpeed = 2.4f;//時計回り
[SerializeField]
private float cClockwiseSpeed = 2.4f;//反時計回り
void Start()
{
Player1 = GameObject.Find("1Pplayer");//プレイヤー情報を習得
Player2 = GameObject.Find("2Pplayer");//プレイヤー情報を習得
}
void FixedUpdate()
{
//1Pから見た2Pの角度
Vector3 targetDir = Player1.transform.position - Player2.transform.position;
angle = Vector3.Angle(targetDir, transform.forward);
//角度差を算出
axisDifference = (90 - angle);
//1Pと2P間の中間点を習得
centerPos = (Player1.transform.position + Player2.transform.position) / 2;
//1Pのビューポート座標を取得
_1PViewportPos = Camera.main.WorldToViewportPoint(Player1.transform.position);
}
void LateUpdate()
{
if (Time.timeScale == 0f) return;//停止中は何もしない
if (_1PViewportPos.x >= 0.49
&& _1PViewportPos.x <= 0.51)//スタック対策
{
transform.RotateAround(centerPos, Vector3.up, clockwiseSpeed);
return;
}
else if (_1PViewportPos.x <= 0.5)//1Pキャラが画面左側なら
{
if (axisDifference > 10)
{
transform.RotateAround(centerPos, Vector3.up, clockwiseSpeed);
}
else if (axisDifference < -10)
{
transform.RotateAround(centerPos, Vector3.up, -cClockwiseSpeed);
}
}
else
{
if (axisDifference < -10)
{
transform.RotateAround(centerPos, Vector3.up, clockwiseSpeed);
}
else if (axisDifference > 10)
{
transform.RotateAround(centerPos, Vector3.up, -cClockwiseSpeed);
}
}
}
}
上記スクリプトを vcam1に取り付けました。色々と拙いのは勘弁して下さい(´・ω・`)
FixedUpdate内で、1Pの正面から見た2Pの角度と角度差、プレイヤー間の中間点の座標を習得。
角度差計算の「90」は、1Pプレイヤーオブジェクトの初期設定分を差し引いてます。
LateUpdate内冒頭、何も書いてないとポーズ中にもカメラがグルグルと回転してしまうので、時間停止中(タイムスケール0)の時はリターンさせてます。
角度差が10以上になっていたら、中間点を軸に
『 transform.RotateAround(中心点, yを軸に, 指定の度数づつ) ;』
でカメラを回転させます。
この時、1Pキャラのビューポート座標からカメラの回転方向を分岐させ、画面右側に回った際の動きに対応させました。
このままだと、まだキャラがガクガクとしててスムーズじゃありません。
原因は更新タイミングとカメラの位置の差異で生まれているのですが、前述した問題があるので、カメラのTransformによる位置移動以外の方法で解決する必要があります。
Dampingの値を調整
ここで重要だったのが、CinemacineVirtualCameraコンポーネント、body内にあるDampingの値。
ここの値が大きいほどカメラの移動がゆっくりになるので、xとzの値を短く設定し直します。
これでカメラの移動時間を最小限に抑えれば、カメラが移動した後に角度変更するような手順になるので、カクカクの問題が改善される……というカラクリなんだと思います。
実際にゲームで使うにはこれで終わりではなく、今まで2D格闘みたいに平面で処理してた記述──オブジェクトの移動とか飛び道具の動き方など、移動システム全般の根本的な見直しが続出して、いろんなスクリプトの修正や組み直しをしたんですが、それはまた別のお話。
取り急ぎの備忘録なので、要点だけの記載となっておりますが、自分が『こう動いてほしい』と思ってる形にはなったと思います。TPS、FPS視点のカメラワークの作例はいろんなところにあるのですが、3D格ゲーで『一番ポピュラー』なこの動きが、検索してもまったく出てこなかったので、初心者の作例として上げておきます。
2P側に回った際の処理の件があるので、時間があるときにもう少し細かくリライト予定です。
ビューポート上の位置関係で判断する、とかでいけるのかしらん…?
⇒※10/14追記 ビューポート座標から裏回り時の挙動に対応できました。
※10/17追記
【覚え書き】
ポーズ中のリターン条件を、ポーズUIのSetActiveで判断しようとしたら、なんか赤エラーがでて原因も把握できなかったので(定義忘れとか綴りミスではなく)、タイムスケールを参照する方法で記載しました。
こっちの方が余計な追記や定義が不要で、記述もスッキリしてますし結果オーライですね。