うら干物書き

ゲームを作っています。

【Unity】オブジェクトについてくるカメラを作ったよ

 今回はカメラの挙動を作ってみました。
 移動制御するオブジェクトを常に追いかけて映し続けるカメラが欲しいのです。
 さーて、作りましょうか。

f:id:urahimono:20171009093401p:plain


この記事にはUnity2017.1.2f1を使用しています。

StandardAssetsのカメラじゃダメなの?

 スクリプトを作る前に、そもそもStandardAssetsの中にもCameraの処理はあります。
 それを利用するのはダメなのでしょうか。
 以前、StandardAssetsのCameraにどんなものがあるかは調べました。

www.urablog.xyz

 んー……、ちょっと違うんだよなー。
 映すターゲットと一定の距離を取り続けつつ追いかけるカメラを作りたいのです。
 MultipurposeCameraRigが似ているのですが、これはカメラ自身を回転させて焦点を変更し、わざわざオブジェクトの背後に回り込もうとする機能があります。
 それは要らないのです。

 やっぱり自分で作りましょうか。

また操作するオブジェクトを作るの?

 カメラの挙動を作る前に、移動操作ができるオブジェクトを作り必要があります。
 以前作ったスクリプトを参考に、パパっと作ってしまいましょう。
 
www.urablog.xyz

ObjectController.cs

using UnityEngine;

[RequireComponent( typeof( Rigidbody ) )]
public class ObjectController : MonoBehaviour
{
    [SerializeField]
    private float   m_moveSpeed     = 0.0f;
    [SerializeField]
    private float   m_turnSpeed     = 0.0f;
    [SerializeField]
    private float   m_jumpForce     = 0.0f;

    private Rigidbody   m_rigidbody     = null;


    private void Awake()
    {
        m_rigidbody = GetComponent<Rigidbody>();
    }

    private void Update()
    {
        ControlObject();
    }

    private void ControlObject()
    {
        Vector3 moveDir     = Vector3.zero;

        Vector3 forwardDir  = Vector3.forward;
        Vector3 rightDir    = Vector3.right;

        if( Input.GetKey( KeyCode.UpArrow ) )
        {
            moveDir += forwardDir;
        }
        if( Input.GetKey( KeyCode.DownArrow ) )
        {
            moveDir -= forwardDir;
        }
        if( Input.GetKey( KeyCode.RightArrow ) )
        {
            moveDir += rightDir;
        }
        if( Input.GetKey( KeyCode.LeftArrow ) )
        {
            moveDir -= rightDir;
        }

        if( moveDir.sqrMagnitude > Mathf.Epsilon )
        {
            moveDir = moveDir.normalized;
            Turn( moveDir );
            Move( moveDir );
        }

        if( Input.GetKeyDown( KeyCode.Space ) )
        {
            Jump();
        }
    }

    private void Move( Vector3 i_forward )
    {
        Vector3 delta       = i_forward * m_moveSpeed * Time.deltaTime;
        Vector3 targetPos   = transform.position + delta;
        m_rigidbody.MovePosition( targetPos );
    }

    private void Turn( Vector3 i_forward )
    {
        Quaternion  toRot   = Quaternion.LookRotation( i_forward );
        Quaternion  fromRot = transform.rotation;

        float delta             = m_turnSpeed * Time.deltaTime;
        Quaternion targetRot    = Quaternion.RotateTowards( fromRot, toRot, delta );

        m_rigidbody.MoveRotation( targetRot );
    }

    private void Jump()
    {
        // えっ、このままじゃ空中でもジャンプできちゃうって!?
        // 仕様だよ!

        m_rigidbody.velocity    = Vector3.zero;
        
        Vector3 jumpVec         = Vector3.up * m_jumpForce;
        m_rigidbody.AddForce( jumpVec, ForceMode.VelocityChange );
    }

} // class ObjectController

f:id:urahimono:20171009092252p:plain
f:id:urahimono:20171009092303g:plain

 移動、ジャンプとアクションゲームに必要な最低限の機能ができましたー。

StandardAssetsのカメラを参考にしないの?

 さて、カメラの挙動を作っていきましょう。
 オブジェクトの構成はStandardAssetsのCameraにあるMultipurposeCameraRigを参考にしてみましょう。

 RigPivotCameraの三段構成にしましょう。
 Rigはターゲットとなるオブジェクトとの位置の調整に使用して、
 Pivotはターゲットとカメラの距離の調整用に使用します。
 Cameraにカメラを設定し、x軸回転を用いてオブジェクトに焦点を合わせます。

f:id:urahimono:20171009092355p:plain

 以前、MultipurposeCameraRigを使って、この構成だとカメラの制御がしやすかったので、自分で作る際も参考にさせてもらいましょう。
 ではスクリプトを作りましょう。

CameraController.cs

using UnityEngine;

public class CameraController : MonoBehaviour
{
    [SerializeField]
    private Transform   m_target    = null;
    [SerializeField]
    private float       m_speed     = 0.0f;

    public  Transform Target
    {
        get { return m_target; }
    }

    private Transform   m_cameraTransform   = null;
    private Transform   m_pivot = null;

    private void Awake()
    {
        Camera camera   = GetComponentInChildren<Camera>();
        Debug.AssertFormat( camera != null, "カメラが無ぇよ!" );
        if( camera == null )
        {
            return;
        }

        m_cameraTransform   = camera.transform;
        m_pivot             = m_cameraTransform.parent;
    }

    private void LateUpdate()
    {
        UpdateCamera();
    }

    private void UpdateCamera()
    {
        if( Target == null )
        {
            return;
        }

        Vector3 targetPos   = Target.position;

        float deltaSpeed    = m_speed * Time.deltaTime;
        transform.position  = Vector3.MoveTowards( transform.position, targetPos, deltaSpeed );
    }
    
} // class CameraController

f:id:urahimono:20171009092409p:plain
f:id:urahimono:20171009092421g:plain

 ルートのオブジェクトであるRigには、ターゲットの位置に移動させます。
 子オブジェクトであるPivotにターゲットから離したい分の距離を設定すれば、いい感じに映ります。

 カメラの移動速度よりターゲットの方が速い場合は上記のようにカメラが少しずつ離れていく演出が出来ます。
 カメラの方が早ければ、常に画面中央にターゲットを捉えられます。

f:id:urahimono:20171009092507g:plain

カメラの角度は自分で設定するの?

 今回はMultipurposeCameraRigのようにカメラを回転して焦点を合わせる処理は行いません。
 初期状態カメラの角度を維持しつつ、位置の移動によって焦点を合わせます。
 ということは、エディタ上で現在設定しているターゲットに焦点が合うような回転角度を指定しておく必要がありますよね。
 ただ、Pivotの位置を意識しつつ、カメラの角度を調整するのは面倒くさいです。
 エディタ上において自動でカメラの角度を設定できる処理を作成しましょうか。

CameraController.cs

[ContextMenu( "ApplyTarget" )]
private void ApplyForceTarget()
{
    if( Target == null )
    {
        return;
    }

    transform.position = Target.position;

    SetCameraTransform();
    if( m_cameraTransform == null )
    {
        return;
    }

    m_cameraTransform.transform.LookAt( Target );
}

private void SetCameraTransform()
{
    Camera camera   = GetComponentInChildren<Camera>();
    Debug.AssertFormat( camera != null, "カメラが無ぇよ!" );
    if( camera == null )
    {
        return;
    }

    m_cameraTransform   = camera.transform;
    m_pivot             = m_cameraTransform.parent;
}

f:id:urahimono:20171009092609g:plain

 簡単ではありますが、メニューからApplyTargetを呼び出すことで、カメラの角度が指定できるようになりました。

カメラは常に追いかけてくるの?

 現在、カメラは常にターゲットを追いかけてきます。
 ちょっとターゲットが動いただけで、カメラが動き始めてしまいます。
 もう少しカメラに心の余裕を持たせるようにしましょう。
 ターゲットが焦点位置から一定以上離れたらカメラが動くようにしましょうか。

CameraController.cs

[SerializeField]
private float       m_waitRange = 0.0f;

private void UpdateCamera()
{
    if( Target == null )
    {
        return;
    }

    Vector3 toTargetVec = Target.position - transform.position;
    float sqrLength     = toTargetVec.sqrMagnitude;

    // 設定した範囲内なら更新しない。
    // magnitudeはルート計算が重いので、二乗された値を利用しよう。
    if( sqrLength <= m_waitRange * m_waitRange )
    {
        return;
    }

    // ターゲットの位置から指定範囲内ギリギリの位置を目指すようにするよ。
    Vector3 targetPos   = Target.position - toTargetVec.normalized * m_waitRange;

    float deltaSpeed    = m_speed * Time.deltaTime;
    transform.position  = Vector3.MoveTowards( transform.position, targetPos, deltaSpeed );
}

f:id:urahimono:20171009092654g:plain

 少し動いただけでは、カメラが動かないようになりました。

カメラは自分で操舵できないの?

 せっかくなのでカメラを自分で操作して、周りを見渡す機能が欲しいものです。
 今回のカメラのオブジェクトは分割して処理しているので、RigのオブジェクトをY軸回転させれば何とかなりそうです。

CameraController.cs

public void Turn( float i_angle )
{
    transform.rotation *= Quaternion.AngleAxis( i_angle, Vector3.up );
}

ObjectController.cs

[Header("Camera")]
[SerializeField]
private CameraController    m_camera    = null;
[SerializeField]
private float   m_camraTurnSpeed    = 0.0f;

private void Update()
{
    ControlCamera();
    ControlObject();
}

private void ControlCamera()
{
    if( Input.GetKey( KeyCode.A ) )
    {
        m_camera.Turn( m_camraTurnSpeed * Time.deltaTime );
    }
    if( Input.GetKey( KeyCode.D ) )
    {
        m_camera.Turn( -m_camraTurnSpeed * Time.deltaTime );
    }
}

f:id:urahimono:20171009092806p:plain
f:id:urahimono:20171009092816g:plain

 あら簡単。あっさり出来ました。

カメラの方向に前進できないの?

 カメラが回転するようになったのですが、おかげでオブジェクト操作が思ったようにいかなくなりました。

f:id:urahimono:20171009092934g:plain

 前進操作を行っているのに、全然前に進んでいない。
 現在のオブジェクトの操作では前進というのはワールド座標におけるZ方向に進んでいるだけですからねー。
 カメラは関係ないのです。

 それでは困るので、カメラの向いている方向を基準に移動できるように修正しましょう。

ObjectController.cs

private void ControlObject()
{
    Vector3 moveDir     = Vector3.zero;

    Vector3 forwardDir  = m_camera.transform.forward;
    Vector3 rightDir    = m_camera.transform.right;

    if( Input.GetKey( KeyCode.UpArrow ) )
    {
        moveDir += forwardDir;
    }
    if( Input.GetKey( KeyCode.DownArrow ) )
    {
        moveDir -= forwardDir;
    }
    if( Input.GetKey( KeyCode.RightArrow ) )
    {
        moveDir += rightDir;
    }
    if( Input.GetKey( KeyCode.LeftArrow ) )
    {
        moveDir -= rightDir;
    }

    if( moveDir.sqrMagnitude > Mathf.Epsilon )
    {
        moveDir = moveDir.normalized;
        Turn( moveDir );
        Move( moveDir );
    }

    if( Input.GetKeyDown( KeyCode.Space ) )
    {
        Jump();
    }
}

f:id:urahimono:20171009093042g:plain

 今回作ったカメラの挙動では、RigのオブジェクトはY軸回転以外の回転はさせていません。
 そのため、Rigtransform.forwardtransform.rightをそのまま使うだけで何とかなりました。

カメラが自動で回転しているときはどうしているの?

 MultipurposeCameraRigのようにカメラが回転までしている際には、上記のやり方では正しい方向が取得できません。
 うーん、どうすればいいのでしょうか。
 Exsampleプロジェクトを参考にしてみましょうか。

 CharacterThirdPersonシーンあたりがなんかやってんじゃないかなぁ。

f:id:urahimono:20171009093217g:plain

 このシーンでは、カメラをX軸方向にも操作できます。
 こんな状態でカメラのtransform.forwardを使って操作制御をしてしまうと、進行方向にY軸の値が絡んできてしまいます。
 空や地面に向かって移動しようとしてしまいます。

 これをどうやっているのでしょうか。
 スクリプトを見てみましょう。

ThirdPersonUserControl.cs

private void FixedUpdate()
{
    // read inputs
    float h = CrossPlatformInputManager.GetAxis("Horizontal");
    float v = CrossPlatformInputManager.GetAxis("Vertical");
    bool crouch = Input.GetKey(KeyCode.C);

    // calculate move direction to pass to character
    if (m_Cam != null)
    {
        // calculate camera relative direction to move:
        m_CamForward = Vector3.Scale(m_Cam.forward, new Vector3(1, 0, 1)).normalized;
        m_Move = v*m_CamForward + h*m_Cam.right;
    }
    else
    {
        // we use world-relative directions in the case of no main camera
        m_Move = v*Vector3.forward + h*Vector3.right;
    }
#if !MOBILE_INPUT
    // walk speed multiplier
    if (Input.GetKey(KeyCode.LeftShift)) m_Move *= 0.5f;
#endif

    // pass all parameters to the character control script
    m_Character.Move(m_Move, crouch, m_Jump);
    m_Jump = false;
}

 えーと、m_CamForwardを設定しているところっぽいですな。
 ふーむ、なるほど。
 Vector3.Scale()を使ってY軸の値を0にしてから、ベクトルを正規化したものを利用しているみたいですね。
 ということはこんな感じにもスクリプトを書けそうですね。

if (m_Cam != null)
{
    // calculate camera relative direction to move:
    // m_CamForward = Vector3.Scale(m_Cam.forward, new Vector3(1, 0, 1)).normalized;

    // ↑これと一緒かな
    m_CamForward = m_Cam.forward;
    m_CamForward.y = 0.0f;
    m_CamForward = m_CamForward.normalized;

    m_Move = v*m_CamForward + h*m_Cam.right;
}

 こっちのほうが処理的に早そうですが、元のスクリプトだと一行で書けるメリットがありますね。

 ちなみにこの処理では、カメラが真上または真下まで角度が返れる場合は注意が必要ですね。
 そうなると、進行方向として利用するのがY軸になってしまいます。
 ただ、今回の処理でY軸方向は0になってしまうので、前進後進は出来なくなってしまいます。

 ライトベクトルは何か処理するわけでもなく、カメラのtransform.rightをそのまま使っているみたいですね。
 カメラがz軸に回転しない限り、ライトベクトルの方向が乱れることはないからかなぁ。
 飛行機とかでは何か手を考える必要があるかもしれませんけどね。

できたの?

 今回作ったスクリプトを以下にまとめました。
 ゲームにとってカメラはとても大切ですからね。
 まだまだ勉強せねばなりませんね。

【Unity】オブジェクトについてくるカメラを作ったよ