#アジェンダ
1.はじめに
2.ヒエラルキーの確認
3.コードの確認
a.アイドル状態 ⇄ ピストル状態
b.壁際
c.ピストル状態 × 壁際
4. おわりに
#1. はじめに
初めてのオリジナルゲームでシューティング系のTPSゲームを作成しました。プレイ動画はこんな感じ。
動画:「初めてのオリジナルTPSゲーム(プレイ動画)」
その実装方法を、2.ヒエラルキーの確認、3.コードの確認に分けて解説していきます。
誰かのヒントになると嬉しいです。至らぬ点・不明点があればご指摘いただけると幸いです。
#2.ヒエラルキーの確認
プレイヤーのヒエラルキービューはこんな感じ。
-PlayerAxis:
プレイヤー本体(Player_TPose。3Dモデル)とカメラ(MainCamera)の親オブジェクト。
プレイヤー本体の動きとカメラの動きを切り分けるため、両オブジェクトの親を設けた。
-Player_TPose:
プレイヤー本体(3Dモデル)。
-MainCamera:
カメラ。
-CardinalPointOfCamera:
カメラが向く方向を設定するための基点。
-rayPosition:
プレイヤーの接地判定を行うため、真下(ローカル座標系のY軸のマイナス方向)にrayを飛ばしている。
今回は特に触れないので無視してOK。
今回の記事では、
① PlayerAxisという親オブジェクトが、Player_TPoseとMainCameraの動きを切り分けるために設けられている。
②Player_TPoseがプレイヤー本体 = 3Dモデル。
③MainCameraがカメラ。
④CardinalPointOfCameraは、MainCameraが向く方向の基点。
ということを覚えておいてください。
※①については続編記事の「初めてのオリジナルTPSゲーム作成 ~カメラとプレイヤーの連動編~」で詳しく解説。
(一言解説しておくと、Player_TPoseの向きが変更された時にMainCameraの向きも合わせて変更されたら困るからです。)
※④について具体的にはMainCameraにアタッチされたScriptで下記のように実装しているからです。
MainCamera.transform.LookAt(CardinalPointOfCamera.transform.position);
また、子オブジェクトそれぞれのTransform(親オブジェクトPlayerAxisを基準としたローカル座標系)の初期値を載せておきます。
-Player_TPose
#3.コードの確認
今回はCameraの動き単体にスポットを当てて、そのうちの3つに分けて解説していきます。
念のため、アジェンダを再掲しておきます。
a.ピストル状態 ⇄ アイドル状態
b.壁際
c.ピストル状態 × 壁際
また、これから説明するコードが書かれたScriptは、MainCameraにアタッチされています。
では、早速それぞれ見ていきましょう。
###a.アイドル状態 ⇄ ピストル状態
実際の動きはこんな感じ。
動画:「初めてのオリジナルTPSゲーム(ピストル ⇄ アイドル)」
画面右下のSliderを上にスライドするとピストルを構えた「ピストル状態」、下にするとピストルを構えない「アイドル状態」になります。
ここでは、アイドル状態 → ピストル状態について説明します。逆も実装方法が全く一緒なので割愛します。
ポイントは2つです。
❶画面中央の赤丸(MainCameraが映し出す映像の中央)が向いている方向に、プレイヤーの体を向ける。
(具体的には、PlayerAxisをその方向に向けて、子オブジェクトPlayer_TposeのRotationを(0,0,0)にする)
❷MainCameraとCardinalPointOfCameraをピストル状態の位置に移動させる。
ソースコードの中身はこんな感じ。
void Update()
{
if (prsSlider.value == 1 && cbpScript.Crouch == false && cameraZoomin == true)
{
if (doOnceCurPlayerRotation)
{
cameraDoubleTapReset = false;
sliderFillImage.raycastTarget = false;
psbImage.raycastTarget = false;
pdImage.raycastTarget = false;
cbImage.raycastTarget = false;
independentCameraTrans = true;
dirWhenTrans =
(CardinalPointOfCamera.transform.position - transform.position).normalized;
raycastWhenTrans = Physics.Raycast(
transform.position, dirWhenTrans, out hitInfoWhenTrans, disWhenTrans, layerMask
);
if (raycastWhenTrans)
{
transform.parent = null;
PlayerAxis.transform.LookAt(
new Vector3(hitInfoWhenTrans.point.x, PlayerAxis.transform.position.y,
hitInfoWhenTrans.point.z), Vector3.up);
transform.parent = PlayerAxis.transform;
destination = CardinalPosPistol + ((CardinalPosPistol -
PlayerAxis.transform.InverseTransformPoint(
hitInfoWhenTrans.point)).normalized
* initialDisFromCardinalToCameraWhenPistol);
}
else if (!raycastWhenTrans)
{
transform.parent = null;
Vector3 endPoint = transform.position + (dirWhenTrans * disWhenTrans);
PlayerAxis.transform.LookAt(
new Vector3(endPoint.x, PlayerAxis.transform.position.y, endPoint.z),
Vector3.up);
transform.parent = PlayerAxis.transform;
destination = CardinalPosPistol + ((CardinalPosPistol -
PlayerAxis.transform.InverseTransformPoint(endPoint)).normalized
* initialDisFromCardinalToCameraWhenPistol);
}
Player.transform.localRotation = Quaternion.Euler(0f, 0f, 0f);
doOnceCurPlayerRotation = false;
}
else if (!doOnceCurPlayerRotation)
{
Vector3 curLocalPos = transform.localPosition;
curLocalPos = Vector3.Lerp(
curLocalPos, destination, Time.deltaTime * cameraMoveSpeed);
transform.localPosition = curLocalPos;
Vector3 curLocalPosCardinal = CardinalPointOfCamera.transform.localPosition;
Vector3 destinationCardinal = CardinalPosPistol;
curLocalPosCardinal = Vector3.Lerp(
curLocalPosCardinal, destinationCardinal, Time.deltaTime * cameraMoveSpeed);
CardinalPointOfCamera.transform.localPosition = curLocalPosCardinal;
transform.LookAt(CardinalPointOfCamera.transform.position);
//Camera位置を目標位置まできちんと移動させる
if ((transform.localPosition.y - destination.y) <= 0.001f &&
(transform.localPosition.y - destination.y) >= -0.001f)
{
transform.localPosition = destination;
CardinalPointOfCamera.transform.localPosition = destinationCardinal;
transform.LookAt(CardinalPointOfCamera.transform.position);
cameraDoubleTapReset = true;
cameraZoomin = false;
cameraZoomout = true;
cameraCrouchZoomin = true;
cameraCrouchZoomout = true;
doOnceCurPlayerRotation = true;
sliderFillImage.raycastTarget = true;
psbImage.raycastTarget = true;
pdImage.raycastTarget = true;
cbImage.raycastTarget = true;
independentCameraTrans = false;
}
}
}
}
上記コードはコードをそのまま貼り付けたので、今回説明する部分以外のところが含まれている(この実装部分のカメラの動きと、他の実装部分でのカメラの動きが同時に実行しない用の変数など)ので、
今回説明する部分だけに絞るとこんな感じ。
void Update()
{
//ピストル状態になる時(画面右下のSliderを上にスライドさせた時)
if (prsSlider.value == 1)
{
//このif内は一度だけ実行される
//MainCamera・CardinalPointOfCameraの移動開始前に、移動先情報を確定、
//また、PlayerAxisとPlayer_TPoseの回転(向く方向)を完了させておく。
if (doOnceCurPlayerRotation)
{
//MainCamera → CardinalPointOfCamera方向のベクトル
//MainCameraは常にCardinalPointOfCameraを向くようにしているので、画面中央からの法線ベクトル
dirWhenTrans =
(CardinalPointOfCamera.transform.position - transform.position).normalized;
//画面中央からrayを飛ばしている
//rayの衝突地点にMainCameraが向く
raycastWhenTrans = Physics.Raycast(
transform.position, dirWhenTrans, out hitInfoWhenTrans, disWhenTrans, layerMask
);
//rayが衝突した時
if (raycastWhenTrans)
{
//PlayerAxisを回転させる前にMainCameraと一旦親子関係を外す
//MainCameraも回転されたらカメラ映像が高速で移動して困るため
transform.parent = null;
//PlayerAxis回転
PlayerAxis.transform.LookAt(
new Vector3(hitInfoWhenTrans.point.x, PlayerAxis.transform.position.y,
hitInfoWhenTrans.point.z), Vector3.up);
//回転終了後、親子関係を戻す
transform.parent = PlayerAxis.transform;
//MainCameraの移動先を確定
//CardinalPosPistolはピストル状態時のCardinalPointOfCameraの位置・移動先
//rayの衝突地点 → CardinalPosPistol の延長線上がMainCameraの移動先
destination = CardinalPosPistol + ((CardinalPosPistol -
PlayerAxis.transform.InverseTransformPoint(
hitInfoWhenTrans.point)).normalized
* initialDisFromCardinalToCameraWhenPistol);
}
//rayが衝突しなかった時
//rayの衝突地点が、rayの最終地点になっているだけで、実装方法は上と全く一緒。説明割愛。
else if (!raycastWhenTrans)
{
transform.parent = null;
Vector3 endPoint = transform.position + (dirWhenTrans * disWhenTrans);
PlayerAxis.transform.LookAt(
new Vector3(endPoint.x, PlayerAxis.transform.position.y, endPoint.z),
Vector3.up);
transform.parent = PlayerAxis.transform;
destination = CardinalPosPistol + ((CardinalPosPistol -
PlayerAxis.transform.InverseTransformPoint(endPoint)).normalized
* initialDisFromCardinalToCameraWhenPistol);
}
Player.transform.localRotation = Quaternion.Euler(0f, 0f, 0f);
doOnceCurPlayerRotation = false;
}
//前準備が完了したら、MainCameraとCardinalPointOfCameraの移動開始
else if (!doOnceCurPlayerRotation)
{
//MainCameraの現ローカル座標(PlayerAxis基準の相対的位置)
Vector3 curLocalPos = transform.localPosition;
//Lerp関数でMainCameraを滑らかに移動
curLocalPos = Vector3.Lerp(
curLocalPos, destination, Time.deltaTime * cameraMoveSpeed);
//MainCameraの現ローカル座標の更新
transform.localPosition = curLocalPos;
//CardinalPointOfCameraもMianCameraと全く同じ実装方法
Vector3 curLocalPosCardinal = CardinalPointOfCamera.transform.localPosition;
Vector3 destinationCardinal = CardinalPosPistol;
curLocalPosCardinal = Vector3.Lerp(
curLocalPosCardinal, destinationCardinal, Time.deltaTime * cameraMoveSpeed);
CardinalPointOfCamera.transform.localPosition = curLocalPosCardinal;
//MainCameraは常にCardinalPointOfCameraを向く
transform.LookAt(CardinalPointOfCamera.transform.position);
//MainCameraとCardinalPointOfCameraを目標位置まできちんと移動させきる
if ((transform.localPosition.y - destination.y) <= 0.001f &&
(transform.localPosition.y - destination.y) >= -0.001f)
{
transform.localPosition = destination;
CardinalPointOfCamera.transform.localPosition = destinationCardinal;
transform.LookAt(CardinalPointOfCamera.transform.position);
}
}
}
}
###b.壁際
実際の動きはこんな感じ。
動画:「初めてのオリジナルTPSゲーム(壁際)」
ポイントは2つです。
①カメラが壁に遮られないように、プレイヤーから見て壁の手前に移動している
②壁際から離れた時、Cameraを元の軌道上(元の位置)に戻す
説明する部分のみのコードはこんな感じ。
このコード内に**「画面をドラッグするとMainCameraがCardinalPointOfCameraを基点にRotateAround関数で移動する」部分**が実装されていましたが、長すぎるので省略しました。
void Update()
{
//Pistolを構えていない時
if (prsSlider.value == 0)
{
//CardinalPointOfCamera → MainCamera方向のベクトル
directionFromCardinalToCamera =
(transform.position - CardinalPointOfCamera.transform.position).normalized;
//MainCameraの壁接触判定用のray
//CardinalPointOfCamera(MainCameraが常に向いている点)からMainCameraまでrayを飛ばす
viewobstacleRaycast = Physics.Raycast(CardinalPointOfCamera.transform.position,
directionFromCardinalToCamera, out viewobstacleHitInfo,
intialDisFromCardinalToCamera, layerMask);
//カメラが壁際にある時
if (viewobstacleRaycast)
{
//ray衝突がプレイヤー自身じゃないことを念のためチェック
//(layermaskでプレイヤーに衝突しないよう設定すればいいのでぶっちゃけ無駄です。。。)
if (!viewobstacleHitInfo.transform.CompareTag("Player"))
{
//壁際になった際に、壁際から離れた時に一度だけ実行するためのbool値をtrueに設定
//これで壁際から離れた際に一度だけ実行する準備完了
doOnceReturnToOriginalPos = true;
//MainCameraの現ローカル座標
Vector3 curLocalPos = transform.localPosition;
//MainCameraの移動先(最終地点)をrayの衝突地点に設定
Vector3 destination =
PlayerAxis.transform.InverseTransformPoint(viewobstacleHitInfo.point);
//Lerp関数でMainCameraを滑らかに移動
curLocalPos = Vector3.Lerp(
curLocalPos, destination, Time.deltaTime * cameraMoveSpeed);
transform.localPosition = curLocalPos;
//CardinalPointOfCameraもLerp関数で滑らかに移動
//CardinalPosWallは事前に設定している、壁際にMainCameraがある時用の位置
Vector3 curLocalPosCardinal = CardinalPointOfCamera.transform.localPosition;
Vector3 destinationCardianl = CardinalPosWall;
curLocalPosCardinal = Vector3.Lerp(
curLocalPosCardinal, destinationCardianl, Time.deltaTime * cameraMoveSpeed);
CardinalPointOfCamera.transform.localPosition = curLocalPosCardinal;
//MainCameraは常にCardinalPointOfCameraの方向を向く
transform.LookAt(CardinalPointOfCamera.transform.position);
}
}
//カメラが壁際にない時
else if (!viewobstacleRaycast ||
(viewobstacleRaycast && viewobstacleHitInfo.transform.CompareTag("Player")))
{
//「壁際にある」→「壁際にない」に条件変化した際に一度だけ実行
if (doOnceReturnToOriginalPos)
{
//CardinalPointOfCameraを元々の位置に戻す
Vector3 curLocalPosCardinal = CardinalPointOfCamera.transform.localPosition;
Vector3 destinationCardinal = CardinalPosOriginal;
curLocalPosCardinal = Vector3.Lerp(
curLocalPosCardinal, destinationCardinal, Time.deltaTime * cameraMoveSpeed);
CardinalPointOfCamera.transform.localPosition = curLocalPosCardinal;
//MainCameraを元々の軌道上(位置)に戻す
//intialDisFromCardinalToCameraで、CardinalPointOfCameraからの元々の距離に戻している
Vector3 curLocalPosCamera = transform.localPosition;
Vector3 destinationCamera = destinationCardinal + ((transform.localPosition -
CardinalPointOfCamera.transform.localPosition).normalized
* intialDisFromCardinalToCamera);
curLocalPosCamera = Vector3.Lerp(
curLocalPosCamera, destinationCamera, Time.deltaTime * cameraMoveSpeed);
transform.localPosition = curLocalPosCamera;
//MainCameraは常にCardinalPointOfCameraの方向を向く
transform.LookAt(CardinalPointOfCamera.transform.position);
//MainCameraとCardinalPointOfCameraを最終地点まで移動させきる
if ((curLocalPosCardinal.y - destinationCardinal.y) <= 0.001f &&
(curLocalPosCardinal.y - destinationCardinal.y) >= -0.001f)
{
CardinalPointOfCamera.transform.localPosition = destinationCardinal;
transform.localPosition = destinationCamera;
transform.LookAt(CardinalPointOfCamera.transform.position);
//一度だけ実行するためfalseにする
doOnceReturnToOriginalPos = false;
}
}
else if(!doOnceReturnToOriginalPos)
{
//ここに壁際にない時の、通常時の、MainCameraの動きを実装。
//長くなるので割愛。
}
}
}
}
###c.ピストル状態 × 壁際
最後に、「ピストル状態」かつ、「壁際」での実装部分を解説します。
実際の動きはこんな感じ。
動画:「初めてのオリジナルTPSゲーム(ピストル状態①)」
ポイントは1つで、
壁際にあるときでも、MainCameraが向く方向と弾が飛んでいく方向の整合性が取れている
(ピストルを構えたプレイヤーとMainCameraの相対的位置が変化しない)
です。
コードはこんな感じ。(余分なところは省略しています)
b.壁際で説明したコードとほぼ同じですが、違う点は、こちらではCardinalPointOfCameraを移動させない点です。
ピストル状態の時は、銃口とMainCameraの向く方向性が、壁際にあろうとも、常に同じ方向を向いていてほしいので、MainCameraが向く方向の基点になるCardinalPointOfCameraは動かしません。
その他は、b.壁際と同じ実装なので説明しません。
void Update()
{
//Pistolを構えている時
if (prsSlider.value == 1 && !independentCameraTrans)
{
Vector3 directionWhenPistol =
(transform.position - CardinalPointOfCamera.transform.position).normalized;
float disWhenPistol = (originalLocalCameraPosWhenPistol -
CardinalPointOfCamera.transform.localPosition).magnitude;
viewobstacleRaycastWhenPistol = Physics.Raycast(
CardinalPointOfCamera.transform.position, directionWhenPistol,
out viewobstacleHitInfoWhenPistol, disWhenPistol, layerMask);
//MainCameraが壁際にある時
if (viewobstacleRaycastWhenPistol)
{
if (!viewobstacleHitInfoWhenPistol.transform.CompareTag("Player"))
{
doOnceReturnToOriginalPos = true;
Vector3 curLocalPos = transform.localPosition;
Vector3 destination = PlayerAxis.transform.InverseTransformPoint(
viewobstacleHitInfoWhenPistol.point);
curLocalPos = Vector3.Lerp(curLocalPos, destination,
Time.deltaTime * cameraMoveSpeed);
transform.localPosition = curLocalPos;
transform.LookAt(CardinalPointOfCamera.transform.position);
}
}
//MainCameraが壁際にない時
else if (!viewobstacleRaycastWhenPistol || (viewobstacleRaycastWhenPistol &&
viewobstacleHitInfoWhenPistol.transform.CompareTag("Player")))
{
//「壁際にある」→「壁際にない」に条件変化した際に一度だけ実行
if (doOnceReturnToOriginalPos)
{
Vector3 curLocalPosCamera = transform.localPosition;
Vector3 destinationCamera = CardinalPointOfCamera.transform.localPosition +
((transform.localPosition -
CardinalPointOfCamera.transform.localPosition).normalized *
initialDisFromCardinalToCameraWhenPistol);
curLocalPosCamera = Vector3.Lerp(
curLocalPosCamera, destinationCamera, Time.deltaTime * cameraMoveSpeed);
transform.localPosition = curLocalPosCamera;
transform.LookAt(CardinalPointOfCamera.transform.position);
//MainCameraを最終地点まで移動させきる
if ((curLocalPosCamera.z - destinationCamera.z) <= 0.001f &&
(curLocalPosCamera.z - destinationCamera.z) >= -0.001f)
{
transform.localPosition = destinationCamera;
transform.LookAt(CardinalPointOfCamera.transform.position);
doOnceReturnToOriginalPos = false;
}
}
else if(!doOnceReturnToOriginalPos)
{
//ここに壁際にない時の、通常時の、MainCameraの動きを実装。
//長くなるので割愛。
}
}
}
}
#4. おわりに
今回は、自分の備忘録も兼ねて、実装方法の解説をしてみました。
私も初めてのオリジナルゲームで最適な実装方法をできているとは思っていません。
ただ、誰かのヒントに少しでもなれば幸いです。(私も色んな方の色んな記事を読みまくってなんとか作り上げることができたので、、、)
下記に連絡先を載せておきます。Resumeにポートフォリオを載せているので、是非みてください。
Twitter: なんじょー@AR勉強中(@12reoer21)
GitHub : 12oreo21
Resume : なんじょー@AR勉強中