Unityで3D空間内をTPSっぽくWASD移動するキャラをつくる
UnityでTPSっぽいゲームの原型をつくるスクリプトの話です。
操作プレイヤーとカメラの連動とか、1から組もうとすると私自身つまずくことがあるのでメモ。
- (準備)とりあえずプロジェクトをつくる
- (準備)キャラをつくる
- (準備)地面をつくる
- プレイヤーをWASD移動させる
- カメラをプレイヤーに追従させる
- カメラをマウスで回るようにする
- プレイヤーが移動方向を瞬時に向くようにする
- プレイヤーが移動方向へ振り向くようにする
- プレイヤーがカメラの向きに応じて移動方向を変えるようにする
- カメラの注視点を補正する
- 最終的にこんなコードに
(準備)とりあえずプロジェクトをつくる
できました。
(準備)キャラをつくる
どんなキャラでもよいのですが、最低限、向いている方向がわからないと困るので、
適当にキャラっぽい何かをつくります。
まず空のゲームオブジェクトPlayer(Position(0,0,0), Scale(1,1,1))をつくり、
その子オブジェクトとして、以下の4つのCubeをつくります。
名前 | PositionX | PositionY | PositionZ | ScaleX | ScaleY | ScaleZ |
---|---|---|---|---|---|---|
Body | 0 | 0 | 0 | 1 | 1 | 1 |
RightEye | -0.25 | 0.25 | -0.5 | 0.1 | 0.1 | 0.1 |
LeftEye | 0.25 | 0.25 | -0.5 | 0.1 | 0.1 | 0.1 |
Mouth | 0 | -0.25 | -0.5 | 0.5 | 0.1 | 0.1 |
そして、Projectウィンドウでマテリアルをつくり、
名前をFaceとして、Bodyと区別の付く色に変更。
RightEye, LeftEye, Mouthに適用します。
こんな感じ。
…意外とかわいいかも。
以降は一塊のPlayerオブジェクトとして扱うので、Hierarchy上で畳んでおきます。
(準備)地面をつくる
Plane(Position(0,0,0), Scale(1,1,1))を生成します。
地面用のマテリアルをつくり、名前をGroundとして、Planeに適用します。
キャラが地面に埋まる状態となるので、PlayerのY座標を0.5にします。
こんな感じ。
ここからスクリプトを追加して書いていきます。
プレイヤーをWASD移動させる
Player.csスクリプトを追加します。
WASD入力から移動方向ベクトル(velocity)をつくり、
それを足してtransform.position値を変化させる処理を書きます。
ポイントは移動方向ベクトルを一旦つくること。
これは拡張する際に色々使います。
// Player.cs using UnityEngine; // プレイヤー public class Player : MonoBehaviour { [SerializeField] private Vector3 velocity; // 移動方向 [SerializeField] private float moveSpeed = 5.0f; // 移動速度 void Update () { // WASD入力から、XZ平面(水平な地面)を移動する方向(velocity)を得ます velocity = Vector3.zero; if(Input.GetKey(KeyCode.W)) velocity.z += 1; if(Input.GetKey(KeyCode.A)) velocity.x -= 1; if(Input.GetKey(KeyCode.S)) velocity.z -= 1; if(Input.GetKey(KeyCode.D)) velocity.x += 1; // 速度ベクトルの長さを1秒でmoveSpeedだけ進むように調整します velocity = velocity.normalized * moveSpeed * Time.deltaTime; // いずれかの方向に移動している場合 if(velocity.magnitude > 0) { // プレイヤーの位置(transform.position)の更新 // 移動方向ベクトル(velocity)を足し込みます transform.position += velocity; } } }
できたらPlayerオブジェクトにアタッチして、実行してみる。
こんな感じ。
こいつ…動くぞ…!
カメラをプレイヤーに追従させる
プレイヤーが動いたので、それに追従させてカメラを動かします。
PlayerFollowCamera.csスクリプトを追加します。
見下ろす回転と水平方向の回転を分けておきます。
// PlayerFollowCamera.cs using UnityEngine; // プレイヤー追従カメラ public class PlayerFollowCamera : MonoBehaviour { [SerializeField] private Transform player; // 注視対象プレイヤー [SerializeField] private float distance = 15.0f; // 注視対象プレイヤーからカメラを離す距離 [SerializeField] private Quaternion vRotation; // カメラの垂直回転(見下ろし回転) [SerializeField] public Quaternion hRotation; // カメラの水平回転 void Start () { // 回転の初期化 vRotation = Quaternion.Euler(30, 0, 0); // 垂直回転(X軸を軸とする回転)は、30度見下ろす回転 hRotation = Quaternion.identity; // 水平回転(Y軸を軸とする回転)は、無回転 transform.rotation = hRotation * vRotation; // 最終的なカメラの回転は、垂直回転してから水平回転する合成回転 // 位置の初期化 // player位置から距離distanceだけ手前に引いた位置を設定します transform.position = player.position - transform.rotation * Vector3.forward * distance; } void LateUpdate () { // カメラの位置(transform.position)の更新 // player位置から距離distanceだけ手前に引いた位置を設定します transform.position = player.position - transform.rotation * Vector3.forward * distance; } }
できたらMain Cameraオブジェクトにアタッチして、
Playerオブジェクトを参照させて、実行してみる。
こんな感じ。
カメラを完全追従させているので、キャラの動きというよりは地面の存在によって動きがわかります。
カメラをマウスで回るようにする
PlayerFollowCamera.csスクリプトに回転速度のパラメータと、マウス入力で回転する処理を追加します。
[SerializeField] private float turnSpeed = 10.0f; // 回転速度
void LateUpdate () { // 水平回転の更新 if(Input.GetMouseButton(0)) hRotation *= Quaternion.Euler(0, Input.GetAxis("Mouse X") * turnSpeed, 0); // カメラの回転(transform.rotation)の更新 // 方法1 : 垂直回転してから水平回転する合成回転とします transform.rotation = hRotation * vRotation; // カメラの位置(transform.position)の更新 // player位置から距離distanceだけ手前に引いた位置を設定します transform.position = player.position - transform.rotation * Vector3.forward * distance; }
無条件に動くと面倒だったので、ボタン押下中のみ(つまりドラッグ)動作にしてみました。
で、動かしてみる
ぐるぐるぐーる
プレイヤーが移動方向を瞬時に向くようにする
Player.csスクリプトのUpdate()の後半に、
向きの制御を追加します。
// いずれかの方向に移動している場合 if(velocity.magnitude > 0) { // プレイヤーの回転(transform.rotation)の更新 // 無回転状態のプレイヤーのZ+方向(後頭部)を、移動の反対方向(-velocity)に回す回転とします transform.rotation = Quaternion.LookRotation(-velocity); // プレイヤーの位置(transform.position)の更新 // 移動方向ベクトル(velocity)を足し込みます transform.position += velocity; }
Quaternion.LookRotationは、無回転状態のVector3.forward(Z+方向)を、第1引数のベクトルの向きに回します。 このキャラは無回転状態のときZ+方向に後頭部があるので、後頭部を向かせたいベクトルを引数に入れています。
進む方向に向いたぞ!
なんて前向きなやつなんだ!
プレイヤーが移動方向へ振り向くようにする
今書いたプレイヤーの瞬時振り向きを、ゆっくり振り向くようにしてみます。
Player.csスクリプトに振り向きの適用速度のパラメータを入れて、transform.rotationの式を書き換えます。
[SerializeField] private float applySpeed = 0.2f; // 回転の適用速度
// いずれかの方向に移動している場合 if(velocity.magnitude > 0) { // プレイヤーの回転(transform.rotation)の更新 // 無回転状態のプレイヤーのZ+方向(後頭部)を、移動の反対方向(-velocity)に回す回転に段々近づけます transform.rotation = Quaternion.Slerp(transform.rotation, Quaternion.LookRotation(-velocity), applySpeed); }
Quaternion.Slerp(球面線形補間)を使って補間してます。
applySpeedは0~1の間で指定します。0だと回転せず、1は瞬時に向くようになります。
くるっ。くるっ。
プレイヤーがカメラの向きに応じて移動方向を変えるようにする
Wを押したとき、本当はプレイヤーには「カメラが向いている奥行き方向」に進んで欲しいわけです。
そこで、カメラの水平回転(Y軸回転)を参照して、移動方向ベクトルを回してあげます。
Player.csスクリプトに参照用のパラメータを入れて、向きと移動の式を両方書き換えます。
[SerializeField] private PlayerFollowCamera refCamera; // カメラの水平回転を参照する用
// いずれかの方向に移動している場合 if(velocity.magnitude > 0) { // プレイヤーの回転(transform.rotation)の更新 // 無回転状態のプレイヤーのZ+方向(後頭部)を、 // カメラの水平回転(refCamera.hRotation)で回した移動の反対方向(-velocity)に回す回転に段々近づけます transform.rotation = Quaternion.Slerp(transform.rotation, Quaternion.LookRotation(refCamera.hRotation * -velocity), applySpeed); // プレイヤーの位置(transform.position)の更新 // カメラの水平回転(refCamera.hRotation)で回した移動方向(velocity)を足し込みます transform.position += refCamera.hRotation * velocity; }
スクリプトを変更した後、Main Cameraオブジェクトを参照させて、実行。
自由に動けそうな感じになってきた!
カメラの注視点を補正する
カメラの向いている方向に動けるになると一気にTPSぽくなったのですが、 同時にプレイヤーが画面の上の方に居すぎじゃない?という感覚が急に湧いてきたので、 カメラの注視点をプレイヤーの上空にずらしてみました。
// カメラの位置(transform.position)の更新 // player位置から距離distanceだけ手前に引いた位置を設定します(位置補正版) transform.position = player.position + new Vector3(0, 3, 0) - transform.rotation * Vector3.forward * distance;
実際には、今回は固定30度にしている見下ろし角度と合わせて調整すると良いと思います。
アクションゲームの場合、ジャンプや高所飛び降り、崖登り等に合わせて変化させるとそれらしくなりそうな予感。
最終的にこんなコードに
なりました。大体40行くらいずつですね。
// Player.cs using UnityEngine; // プレイヤー public class Player : MonoBehaviour { [SerializeField] private Vector3 velocity; // 移動方向 [SerializeField] private float moveSpeed = 5.0f; // 移動速度 [SerializeField] private float applySpeed = 0.2f; // 振り向きの適用速度 [SerializeField] private PlayerFollowCamera refCamera; // カメラの水平回転を参照する用 void Update () { // WASD入力から、XZ平面(水平な地面)を移動する方向(velocity)を得ます velocity = Vector3.zero; if(Input.GetKey(KeyCode.W)) velocity.z += 1; if(Input.GetKey(KeyCode.A)) velocity.x -= 1; if(Input.GetKey(KeyCode.S)) velocity.z -= 1; if(Input.GetKey(KeyCode.D)) velocity.x += 1; // 速度ベクトルの長さを1秒でmoveSpeedだけ進むように調整します velocity = velocity.normalized * moveSpeed * Time.deltaTime; // いずれかの方向に移動している場合 if(velocity.magnitude > 0) { // プレイヤーの回転(transform.rotation)の更新 // 無回転状態のプレイヤーのZ+方向(後頭部)を、 // カメラの水平回転(refCamera.hRotation)で回した移動の反対方向(-velocity)に回す回転に段々近づけます transform.rotation = Quaternion.Slerp(transform.rotation, Quaternion.LookRotation(refCamera.hRotation * -velocity), applySpeed); // プレイヤーの位置(transform.position)の更新 // カメラの水平回転(refCamera.hRotation)で回した移動方向(velocity)を足し込みます transform.position += refCamera.hRotation * velocity; } } }
// PlayerFollowCamera.cs using UnityEngine; // プレイヤー追従カメラ public class PlayerFollowCamera : MonoBehaviour { [SerializeField] private float turnSpeed = 10.0f; // 回転速度 [SerializeField] private Transform player; // 注視対象プレイヤー [SerializeField] private float distance = 15.0f; // 注視対象プレイヤーからカメラを離す距離 [SerializeField] private Quaternion vRotation; // カメラの垂直回転(見下ろし回転) [SerializeField] public Quaternion hRotation; // カメラの水平回転 void Start () { // 回転の初期化 vRotation = Quaternion.Euler(30, 0, 0); // 垂直回転(X軸を軸とする回転)は、30度見下ろす回転 hRotation = Quaternion.identity; // 水平回転(Y軸を軸とする回転)は、無回転 transform.rotation = hRotation * vRotation; // 最終的なカメラの回転は、垂直回転してから水平回転する合成回転 // 位置の初期化 // player位置から距離distanceだけ手前に引いた位置を設定します transform.position = player.position - transform.rotation * Vector3.forward * distance; } void LateUpdate () { // 水平回転の更新 if(Input.GetMouseButton(0)) hRotation *= Quaternion.Euler(0, Input.GetAxis("Mouse X") * turnSpeed, 0); // カメラの回転(transform.rotation)の更新 // 方法1 : 垂直回転してから水平回転する合成回転とします transform.rotation = hRotation * vRotation; // カメラの位置(transform.position)の更新 // player位置から距離distanceだけ手前に引いた位置を設定します(位置補正版) transform.position = player.position + new Vector3(0, 3, 0) - transform.rotation * Vector3.forward * distance; } }