#アジェンダ
1.はじめに
2.ヒエラルキーの確認
3.コードの確認
a.アイドル状態
b.ピストル状態
4. おわりに
#1. はじめに
この記事は、「初めてのオリジナルTPSゲーム作成 〜カメラ編〜」の続編になります。
上記記事をみていただいてからの方が、理解しやすいと思います。
今回は、カメラとプレイヤーの連動した動きの実装方法を、2.ヒエラルキーの確認、3.コードの確認に分けて解説していきます。
誰かのヒントになると嬉しいです。至らぬ点・不明点があればご指摘いただけると幸いです。
#2.ヒエラルキーの確認
詳しくは「初めてのオリジナルTPSゲーム作成 〜カメラ編〜」の【2.ヒエラルキーの確認】をみていただきたいです。
プレイヤーのヒエラルキービューの画像を一枚だけ載せておきます。
-PlayerAxis:
プレイヤー本体(Player_TPose。3Dモデル)とカメラ(MainCamera)の親オブジェクト。
プレイヤー本体の動きとカメラの動きを切り分けるため、両オブジェクトの親を設けた。
-Player_TPose:
プレイヤー本体(3Dモデル)。
-MainCamera:
カメラ。
説明は「カメラ編」に書いてあるのでほぼ丸投げしますが、一点だけ説明します。
それは、Player_TPose(プレイヤー本体)とMainCamera(カメラ)を親子関係にしていないため、プレイヤー本体の動きとカメラの動きを別々で動かせるということです。
動画で確認してみましょう。
動画:「初めてのオリジナルTPSゲーム(カメラ)」
プレイヤー本体が初期位置から左方向に向かって歩き始めましたが、カメラの向きは変わっていません。そして、プレイヤー本体を動かしながら自由自在にカメラも動かしています。
これが例えば、プレイヤーをカメラの親オブジェクトにすると、プレイヤーの動きとカメラの動きが常に同期されるため、FPSのカメラワークとなります。
#3.コードの確認
コードはアジェンダの再掲になりますが、
a.アイドル状態
b.ピストル状態
に分けて説明していきます。
###a. アイドル状態
アイドル状態は、ここではピストルを構えていない状態を指します。
2.ヒエラルキーの確認でみた動画にあるように、プレイヤー本体の動きとカメラの動きを別々で動せるように実装しています。
具体的には、以下のように切り分けされています。
❶画面左下のジョイスティック操作(赤枠):
カメラから見て、傾けた方向にプレイヤー本体が向く & PlayerAxis(親オブジェクト)が移動。
❷画面上半分のPanel(UI)のドラッグ(青枠):
プレイヤー本体を基点(厳密にはCardinalPointOfCameraを基点)にカメラの衛星回転。
(プレイヤー方向を常に向いて、プレイヤーから常に一定の距離を保ちながら移動。)
順番にコードを見ていきます。
❶画面左下のジョイスティック操作:
カメラから見て、傾けた方向にプレイヤー本体が向く & PlayerAxis(親オブジェクト)が移動する。
//Player_TPoseにアタッチされたスクリプトです。
void Update()
{
//JoystickとPlayerの動きを連動させるための変数を用意
stickCurrentPos = Stick.transform.localPosition;
stickDis = (stickCurrentPos - stickInitialPos).magnitude;
//uGUI>Joystick>Stickの現在座標から弧度(ラジアン)を算出
stickRadian = Mathf.Atan2(stickCurrentPos.y, stickCurrentPos.x);
//孤度(ラジアン)を角度(θ)に変換
stickAngle = stickRadian * Mathf.Rad2Deg;
//(450f - stickAngle)は、uGUI>Joystick>Stick と Player の動きを直感的に操作するための補正
//uGUI>Joystick>StickのθはX軸から反時計回り(XY平面)・PlayerのθはZ軸から時計回り(ZX平面)
stickRotation = Quaternion.AngleAxis((450f - stickAngle), Vector3.up);
//Player_TPoseの向く方向
//Pistol構えている時はPanelのDragで親オブジェクトPlayerAxisごとRotation
//(MainCameraも定位置にて追従)
if (PistolReady == true)
{
PlayerAxis.transform.rotation *= Quaternion.AngleAxis(
pdScript.panelDragVector.x * 0.2f * Time.deltaTime, Vector3.up);
}
//Pistol構えていない時はJoystick傾けた方向にPlayer_TPoseのみRotation
//今、説明しているのはこっち。
else if(PistolReady == false)
{
if(stickDis >= 0.1f)
{
curLocalRotation = Quaternion.Euler(transform.localRotation.x,
(stickRotation + Camera.transform.localEulerAngles.y),
transform.localRotation.z);
transform.localRotation = curLocalRotation;
}
}
//PlayerAxisの移動・Player_TPoseのAnimation
//Pistol構えていない時
if (PistolReady == false)
{
//Joystickを傾けた入力がない時
if (stickDis <= 10f)
{
NeutralIdleMove();
}
//ある程度傾けた時
else if (stickDis > 10f && stickDis < 60f)
{
WalkMove();
}
//目一杯傾けた時
else if ((stickDis >= 60f)
{
RunMove();
}
}
}
public void NeutralIdleMove()
{
if(stickDis <= 10f)
{
//PlayerAxisは移動しない
PlayerAxis.transform.Translate(0, 0, 0);
animator.SetBool("NeutralIdle", true); //Animator "NeutrallIdle" をtrue
animator.SetBool("Walk", false);
animator.SetBool("Run", false);
animator.SetBool("PistolIdle", false);
animator.SetBool("PistolWalk", false);
animator.SetBool("PistolLeftStrafe", false);
animator.SetBool("PistolWalkBackward", false);
animator.SetBool("PistolRightStrafe", false);
animator.SetBool("PistolRun", false);
animator.SetBool("PistolRunBackward", false);
}
}
public void WalkMove()
{
if(stickDis > 10f && stickDis < 60f)
{
//Player_TPoseが向いている方向に移動
//ポイントはthis.transform.forward(Player_TPoseのZ軸の正方向)
PlayerAxis.transform.position += this.transform.forward * stickDis
* 0.06f * translateForward * Time.deltaTime;
animator.SetBool("NeutralIdle", false);
animator.SetBool("Walk", true); //Animator "Walk" をtrue
animator.SetBool("Run", false);
animator.SetBool("PistolIdle", false);
animator.SetBool("PistolWalk", false);
animator.SetBool("PistolLeftStrafe", false);
animator.SetBool("PistolWalkBackward", false);
animator.SetBool("PistolRightStrafe", false);
animator.SetBool("PistolRun", false);
animator.SetBool("PistolRunBackward", false);
}
}
public void RunMove()
{
if (stickDis >= 60f)
{
//Player_TPoseが向いている方向に移動
//ポイントはthis.transform.forward(Player_TPoseのZ軸の正方向)
PlayerAxis.transform.position += this.transform.forward
* stickDis * 0.06f * translateForward * Time.deltaTime;
animator.SetBool("NeutralIdle", false);
animator.SetBool("Walk", false);
animator.SetBool("Run", true); //Animator "Run" をtrue
animator.SetBool("PistolIdle", false);
animator.SetBool("PistolWalk", false);
animator.SetBool("PistolLeftStrafe", false);
animator.SetBool("PistolWalkBackward", false);
animator.SetBool("PistolRightStrafe", false);
animator.SetBool("PistolRun", false);
animator.SetBool("PistolRunBackward", false);
}
}
**❷画面上半分のPanel(UI)のドラッグ: プレイヤー本体を基点(厳密にはCardinalPointOfCameraを基点)にカメラの衛星回転。 (プレイヤー方向を常に向いて、プレイヤーから常に一定の距離を保ちながら移動。)**
//uGUIのPanelにアタッチされたスクリプトです。
private Vector3 initialPos;
private Vector3 currentPos;
public Vector3 panelDragVector;
//ドラッグ開始位置の取得
public void OnBeginDrag(PointerEventData pointerEventData)
{
initialPos = pointerEventData.position;
cameraDrag = true;
}
//ドラッグした 距離と長さ = ベクトル の取得
public void OnDrag(PointerEventData pointerEventData)
{
currentPos = pointerEventData.position;
panelDragVector = currentPos - initialPos;
}
//ドラッグ終了したのでベクトルを0にする
public void OnEndDrag(PointerEventData pointerEventData)
{
panelDragVector = new Vector3(0, 0, 0);
cameraDrag = false;
}
//MainCameranにアタッチされたスクリプトです。
float cameralocalEulerAnglesX_DownMax = 300f;
float cameralocalEulerAnglesX_UpMax = 60f;
void Update()
{
//MainCameraはプレイヤーを基点(厳密にはCardinalPointOfCameraを基点)に、
//RotateAround()とLookAt()をするので、上下の動きの下限と上限を設定
//これは下限
if (transform.localEulerAngles.x <= 300f && transform.localEulerAngles.x > 180f)
{
transform.localEulerAngles = new Vector3(cameralocalEulerAnglesX_DownMax,
transform.localEulerAngles.y, transform.localEulerAngles.z);
if (pdScript.panelDragVector.y > 0)
{
pdScript.panelDragVector.y = 0f;
}
}
//こっちが上限
else if (transform.localEulerAngles.x >= 60f && transform.localEulerAngles.x < 180f)
{
transform.localEulerAngles = new Vector3(cameralocalEulerAnglesX_UpMax,
transform.localEulerAngles.y, transform.localEulerAngles.z);
if (pdScript.panelDragVector.y < 0)
{
pdScript.panelDragVector.y = 0f;
}
}
//ピストルを構えていない時 && Panelをドラッグしている時
//MainCameraの移動
if(prsSlider.value == 0 && pdScript.cameraDrag)
{
//RotateAround関数の第一引数で設定した中心点・第二引数で軸を設定した時に、
//中心点から見てその軸の負の方向にMainCameraがある時、
//同入力・逆回転になるので、入力にマイナスをかける
if (transform.localPosition.z < 0f && transform.localPosition.x < 0f)
{
//上下のPanelドラッグによる、カメラのYZ平面の移動
transform.RotateAround(CardinalPointOfCamera.transform.position,
CardinalPointOfCamera.transform.right,
pdScript.panelDragVector.y * -0.2f * Time.deltaTime);
//上下のPanelドラッグによる、カメラのXY平面の移動
transform.RotateAround(CardinalPointOfCamera.transform.position,
CardinalPointOfCamera.transform.forward,
pdScript.panelDragVector.y * 0.2f * Time.deltaTime);
}
else if (transform.localPosition.z >= 0f && transform.localPosition.x < 0f)
{
//上下のPanelドラッグによる、カメラのYZ平面の移動
transform.RotateAround(CardinalPointOfCamera.transform.position,
CardinalPointOfCamera.transform.right,
pdScript.panelDragVector.y * 0.2f * Time.deltaTime);
//上下のPanelドラッグによる、カメラのXY平面の移動
transform.RotateAround(CardinalPointOfCamera.transform.position,
CardinalPointOfCamera.transform.forward,
pdScript.panelDragVector.y * 0.2f * Time.deltaTime);
}
else if (transform.localPosition.z < 0f && transform.localPosition.x >= 0f)
{
//上下のPanelドラッグによる、カメラのYZ平面の移動
transform.RotateAround(CardinalPointOfCamera.transform.position,
CardinalPointOfCamera.transform.right,
pdScript.panelDragVector.y * -0.2f * Time.deltaTime);
//上下のPanelドラッグによる、カメラのXY平面の移動
transform.RotateAround(CardinalPointOfCamera.transform.position,
CardinalPointOfCamera.transform.forward,
pdScript.panelDragVector.y * -0.2f * Time.deltaTime);
}
else if (transform.localPosition.z >= 0f && transform.localPosition.x >= 0f)
{
//上下のPanelドラッグによる、カメラのYZ平面の移動
transform.RotateAround(CardinalPointOfCamera.transform.position,
CardinalPointOfCamera.transform.right,
pdScript.panelDragVector.y * 0.2f * Time.deltaTime);
//上下のPanelドラッグによる、カメラのXY平面の移動
transform.RotateAround(CardinalPointOfCamera.transform.position,
CardinalPointOfCamera.transform.forward,
pdScript.panelDragVector.y * -0.2f * Time.deltaTime);
}
//CameraのRotationは常にPlayer方向を向く
transform.LookAt(CardinalPointOfCamera.transform.position);
//左右のPanelドラッグによる、カメラのZX平面の移動
transform.RotateAround(CardinalPointOfCamera.transform.position,
CardinalPointOfCamera.transform.up,
pdScript.panelDragVector.x * 0.4f * Time.deltaTime);
}
}
###b. ピストル状態
実際の動きはこんな感じ。
動画:「初めてのオリジナルTPSゲーム(ピストル状態①)」
動画:「初めてのオリジナルTPSゲーム(ピストル状態②)」
こちらは、a. アイドル状態とは違い、プレイヤー本体の動きとカメラの動きが常に連動しています。
ポイントは2つで、
❶左右のプレイヤー本体の回転に対して、カメラが常に固定されている。
❷プレイヤー本体が上下に体を向ける動きに合わせて、カメラも移動する。
です。
❶左右のプレイヤー本体の回転に対して、カメラが常に固定されている。
こちらは、左右の回転の際に、両オブジェクト(プレイヤー本体とカメラ)の親オブジェクトであるPlayerAxisごと回転させています。
また。a.アイドル状態とは違い、ジョイスティックを後ろに傾けた際に、プレイヤー本体(Player_TPose)を振り向かせるのではなく、Animationでバック走させています。
void Update()
{
//JoystickとPlayerの動きを連動させるための変数を用意
stickCurrentPos = Stick.transform.localPosition;
stickDis = (stickCurrentPos - stickInitialPos).magnitude;
//uGUI>Joystick>Stickの現在座標から弧度(ラジアン)を算出
stickRadian = Mathf.Atan2(stickCurrentPos.y, stickCurrentPos.x);
//孤度(ラジアン)を角度(θ)に変換
stickAngle = stickRadian * Mathf.Rad2Deg;
//(450f - stickAngle)は、uGUI>Joystick>Stick と Player の動きを直感的に操作するための補正
//uGUI>Joystick>StickのθはX軸から反時計回り(XY平面)・PlayerのθはZ軸から時計回り(ZX平面)
stickRotation = Quaternion.AngleAxis((450f - stickAngle), Vector3.up);
//Player_TPoseの向く方向
//Pistol構えている時はPanelのDragで親オブジェクトPlayerAxisごとRotation
//MainCameraも定位置にて追従
//今、説明しているのはこっち。
if (PistolReady == true)
{
PlayerAxis.transform.rotation *= Quaternion.AngleAxis(
pdScript.panelDragVector.x * 0.2f * Time.deltaTime, Vector3.up);
}
//Pistol構えていない時はJoystick傾けた方向にPlayer_TPoseのみRotation
else if(PistolReady == false)
{
if(stickDis >= 0.1f)
{
curLocalRotation = Quaternion.Euler(transform.localRotation.x,
(stickRotation + Camera.transform.localEulerAngles.y),
transform.localRotation.z);
transform.localRotation = curLocalRotation;
}
}
//PlayerAxisの移動・Player_TPoseのAnimation
//Pistol構えていない時
if (PistolReady == true)
{
//Joystickを傾けた入力がない時
if (stickDis <= 10f)
{
PistolIdleMove();
}
//ある程度傾けた時
else if (stickDis > 10f && stickDis < 60f)
{
PistolWalkMove();
}
//目一杯傾けた時
else if ((stickDis >= 60f)
{
PistolRunMove();
}
}
}
public void PistolIdleMove()
{
if (stickDis <= 10f)
{
PlayerAxis.transform.Translate(0, 0, 0);
animator.SetBool("NeutralIdle", false);
animator.SetBool("Walk", false);
animator.SetBool("Run", false);
animator.SetBool("PistolIdle", true); //Animator "PistolIdle" をtrue
animator.SetBool("PistolWalk", false);
animator.SetBool("PistolLeftStrafe", false);
animator.SetBool("PistolWalkBackward", false);
animator.SetBool("PistolRightStrafe", false);
animator.SetBool("PistolRun", false);
animator.SetBool("PistolRunBackward", false);
}
}
//ジョイスティックを傾けた方向で、プレイヤー本体(Player_TPose) のAnimationを変更
public void PistolWalkMove()
{
if (stickDis > 10f && stickDis < 60f)
{
PlayerAxis.transform.Translate(new Vector3(
translateX, Stick.transform.localPosition.z, translateZ)
/ stickDis * 2.5f * Time.deltaTime);
if (stickAngle >= 45f && stickAngle < 135f) //Joystickを前方向に傾けているとき
{
animator.SetBool("NeutralIdle", false);
animator.SetBool("Walk", false);
animator.SetBool("Run", false);
animator.SetBool("PistolIdle", false);
animator.SetBool("PistolWalk", true); //Animator "PistolWalk" をtrue
animator.SetBool("PistolLeftStrafe", false);
animator.SetBool("PistolWalkBackward", false);
animator.SetBool("PistolRightStrafe", false);
animator.SetBool("PistolRun", false);
animator.SetBool("PistolRunBackward", false);
}
else if ((stickAngle >= 135f && stickAngle <= 180f) ||
(stickAngle < -135f && stickAngle > -180f)) //Joystickを左方向に傾けているとき
{
animator.SetBool("NeutralIdle", false);
animator.SetBool("Walk", false);
animator.SetBool("Run", false);
animator.SetBool("PistolIdle", false);
animator.SetBool("PistolWalk", false);
animator.SetBool("PistolLeftStrafe", true);//Animator "PistolLeftStrafe" をtrue
animator.SetBool("PistolWalkBackward", false);
animator.SetBool("PistolRightStrafe", false);
animator.SetBool("PistolRun", false);
animator.SetBool("PistolRunBackward", false);
}
else if (stickAngle >= -135f && stickAngle < -45f) //Joystickを後方向に傾けているとき
{
animator.SetBool("NeutralIdle", false);
animator.SetBool("Walk", false);
animator.SetBool("Run", false);
animator.SetBool("PistolIdle", false);
animator.SetBool("PistolWalk", false);
animator.SetBool("PistolLeftStrafe", false);
animator.SetBool("PistolWalkBackward", true);//Animator "PistolWalkBackward" をtrue
animator.SetBool("PistolRightStrafe", false);
animator.SetBool("PistolRun", false);
animator.SetBool("PistolRunBackward", false);
}
else if (stickAngle >= -45f && stickAngle < 45f) //Joystickを右方向に傾けているとき
{
animator.SetBool("NeutralIdle", false);
animator.SetBool("Walk", false);
animator.SetBool("Run", false);
animator.SetBool("PistolIdle", false);
animator.SetBool("PistolWalk", false);
animator.SetBool("PistolLeftStrafe", false);
animator.SetBool("PistolWalkBackward", false);
animator.SetBool("PistolRightStrafe", true);//Animator "PistolRightStrafe" をtrue
animator.SetBool("PistolRun", false);
animator.SetBool("PistolRunBackward", false);
}
}
}
public void PistolWalkMove()
{
//PistolWalkMove()と同じ実装なので説明割愛。
}
**❷プレイヤー本体が上下に体を向ける動きに合わせて、カメラも移動する。** こちらは、Panel(uGUI)の上下ドラッグに合わせて、**プレイヤー本体のボーンの一つであるSpine(腰のあたり)の角度**と**カメラの位置・角度**を調整しています。
//Player_TPoseにアタッチされたスクリプトです。
//CameraのlocalRotationとSpineのRotationを、Pistol時に連動
private void OnAnimatorIK(int layerIndex)
{
//ピストルを構えている時
if (prsSlider.value == 1)
{
if (Camera.transform.localEulerAngles.x >= 0f
&& Camera.transform.localEulerAngles.x <= 180f)
{
//カメラの回転に連動してSpineも回転
//微調整でかけたり引いたりしています
//ここの微調整めちゃ重要なので、各自の環境に合わせて調整してみてください
spineRotX = Camera.transform.localEulerAngles.x * 0.85f - 8.7f;
spineRotY = Camera.transform.localEulerAngles.x * 0.15f - 6f;
spineRotZ = Camera.transform.localEulerAngles.x * 0.61f - 6.13f;
}
else if (Camera.transform.localEulerAngles.x > 180f
&& Camera.transform.localEulerAngles.x <= 360f)
{
//同様に微調整
spineRotX = (Camera.transform.localEulerAngles.x - 360f) * 0.75f - 8.7f;
spineRotY = (Camera.transform.localEulerAngles.x -360f) * -0.5f - 6f;
spineRotZ = (Camera.transform.localEulerAngles.x - 360f) * 0.54f - 6.13f;
}
animator.SetBoneLocalRotation(
HumanBodyBones.Spine, Quaternion.Euler(spineRotX, spineRotY, spineRotZ));
if (pdScript.cameraDrag)
{
animator.SetBoneLocalRotation(
HumanBodyBones.Spine, Quaternion.Euler(spineRotX, spineRotY, spineRotZ));
}
}
else if (prsSlider.value == 0)
{
spineRotX = -3.63f;
spineRotY = -6.35f;
spineRotZ = -2.6f;
}
}
//MainCameraにアタッチされたスクリプトです。
void Update()
{
//Pistolを構えていて、PanelをDragしている時
if (prsSlider.value == 1 && pdScript.cameraDrag)
{
//上下のPanelドラッグによる、カメラのXY平面の移動
//今回MainCameraは上下の動きのみ
transform.RotateAround(CardinalPointOfCamera.transform.position,
CardinalPointOfCamera.transform.right,
pdScript.panelDragVector.y * -0.3f * Time.deltaTime);
//CameraのRotationは常にPlayer方向を向く
transform.LookAt(CardinalPointOfCamera.transform.position);
}
}
#4. おわりに
自分の備忘録も兼ねて、実装方法の解説をしてきましたが、カメラ編から含めてこれで完了です。
カメラ編でも最後に書きましたが、私も初めてのオリジナルゲームで最適な実装方法をできているとは思っていません。
ただ、誰かのヒントに少しでもなれば幸いです。(私も色んな方の色んな記事を読みまくってなんとか作り上げることができたので、、、)
最後まで読んでいただきありがとうございました。
下記に連絡先を載せておきます。Resumeにポートフォリオを載せているので、是非みてください。
Twitter: なんじょー@AR勉強中(@12reoer21)
GitHub : 12oreo21
Resume : なんじょー@AR勉強中