Unityでソウルライクが作りたい人のためのカメラスクリプト

ソウルライクなカメラスクリプト

Unityには高度なカメラ制御ができるCinemachineがあるがソウルシリーズの様なロックオンをイメージして使ってみると違和感がある。自分がCinemachineに対する知識に疎いからかもしれないが、シンプルに使えるカメラスクリプトが欲しかったので自作した。

ThirdPersonCamera

using System.Collections;
using UnityEngine;

[RequireComponent(typeof(Camera))]
public class ThirdPersonCamera : MonoBehaviour
{
    public GameObject primaryTarget;
    public GameObject secondaryTarget;
    public Vector2 primaryLockOnPosition = new Vector2(0, 0);
    public Vector2 secondaryLockOnPosition = new Vector2(0, .3f);
    public CameraMode cameraMode;
    public float distance = 4;
    public float height = 1.5f;
    public float delay = 2;
    [Range(0, 1)]
    public float maxVerticalRotationLimit = .9f;
    [Range(0, 1)]
    public float minVerticalRotationLimit = .9f;
    public LayerMask layerMask;
    public float castRadius = .6f;
    [Range(.001f, 1)]
    public float lockOnSpeed = .1f;
    public AnimationCurve shakeDecayCurve = new AnimationCurve(new Keyframe(0, 1), new Keyframe(1, 0));
    public bool cursorLock = true;
    public bool inputInThisScript = true;

    public enum CameraMode
    {
        Manual,
        Auto,
        LockOn
    }

    private Camera cam;
    private Transform orbitalCenter;
    private Vector2 primPosOffset;
    private Vector2 secPosOffse;
    private float fixedDistance;
    private float frustumHeight;
    private float frustumWidth;
    private float lastAspect;
    private float shakePower;
    private float shakeSpeed;
    private float shakeLifeTime;
    private float shakeAge;
    private bool shake = false;

    private void Start()
    {
        cam = GetComponent();
        orbitalCenter = new GameObject().transform;

        SetFrustumSize();
        SetPositionOffset();
        orbitalCenter.position = primaryTarget.transform.position + Vector3.up * height;
        transform.position = orbitalCenter.position - orbitalCenter.forward * fixedDistance;
        transform.LookAt(orbitalCenter);
        transform.position = transform.position - transform.right * primPosOffset.x - transform.up * primPosOffset.y;
    }

    private void LateUpdate()
    {
        if (cursorLock)
        {
            Cursor.visible = false;
            Cursor.lockState = CursorLockMode.Locked;
        }
        else
        {
            Cursor.visible = true;
            Cursor.lockState = CursorLockMode.None;
        }

        if (inputInThisScript) Input();

        if (cam.aspect != lastAspect) SetFrustumSize();

        if (cameraMode == CameraMode.Auto) AutoCamera();
        else if (cameraMode == CameraMode.LockOn) LockOnCamera();

        ObstacleDetection();
        SetPositionOffset();
        MoveCamera();
        SetLastValues();
        if (shake) Shake();
    }

    private void Input()
    {
        RotateCamera(UnityEngine.InputSystem.Mouse.current.delta.ReadValue() * 10);
    }

    private void ObstacleDetection()
    {
        RaycastHit hitInfo;
        if (Physics.SphereCast(orbitalCenter.position, castRadius, transform.position - orbitalCenter.position, out hitInfo, distance, layerMask) ||
            Physics.Raycast(orbitalCenter.position, transform.position - orbitalCenter.position, out hitInfo, distance, layerMask))
        {
            fixedDistance = Mathf.Sqrt(primPosOffset.x * primPosOffset.x + primPosOffset.y * primPosOffset.y + hitInfo.distance * hitInfo.distance);
        }
        else
        {
            fixedDistance = distance;
        }
    }

    private void SetFrustumSize()
    {
        frustumHeight = 2 * Mathf.Tan(cam.fieldOfView * .5f * Mathf.Deg2Rad);
        frustumWidth = frustumHeight * cam.aspect;
    }

    private void SetPositionOffset()
    {
        primPosOffset.x = frustumWidth * fixedDistance * primaryLockOnPosition.x;
        primPosOffset.y = frustumHeight * fixedDistance * primaryLockOnPosition.y;
        secPosOffse.x = frustumWidth * fixedDistance * secondaryLockOnPosition.x - primaryLockOnPosition.x;
        secPosOffse.y = frustumHeight * fixedDistance * secondaryLockOnPosition.y - primaryLockOnPosition.x;
    }

    private void MoveCamera()
    {
        var camTgtPos = primaryTarget.transform.position + Vector3.up * height;
        var curVel = Vector3.zero;
        orbitalCenter.position = Vector3.SmoothDamp(orbitalCenter.position, camTgtPos, ref curVel, delay * Time.deltaTime);
        transform.position = orbitalCenter.position - orbitalCenter.forward * fixedDistance;
        transform.LookAt(orbitalCenter);
        transform.position = transform.position - transform.right * primPosOffset.x - transform.up * primPosOffset.y;
    }

    private void AutoCamera()
    {
        orbitalCenter.rotation = Quaternion.Lerp(orbitalCenter.rotation, Quaternion.LookRotation(primaryTarget.transform.forward, Vector3.up), lockOnSpeed);
    }
    
        private void LockOnCamera()
    {
        var tgtVecFromCam = transform.forward + transform.right * frustumWidth * (secondaryLockOnPosition.x - primaryLockOnPosition.x) + transform.up * frustumHeight * (secondaryLockOnPosition.y - primaryLockOnPosition.y);
        var secTgtDir = secondaryTarget.transform.position + Vector3.up * height - orbitalCenter.transform.position;
        var ratio = Mathf.Sqrt(secTgtDir.sqrMagnitude + tgtVecFromCam.sqrMagnitude);
        var tgtFwd = secTgtDir - transform.right * ratio * frustumWidth * (secondaryLockOnPosition.x - primaryLockOnPosition.x) - transform.up * ratio * frustumHeight * (secondaryLockOnPosition.y - primaryLockOnPosition.y);
        orbitalCenter.rotation = Quaternion.Slerp(orbitalCenter.rotation, Quaternion.LookRotation(tgtFwd, Vector3.up), lockOnSpeed);
    }

    private void SetLastValues()
    {
        lastAspect = cam.aspect;
    }

    private void Shake()
    {
        shakeAge += Time.deltatime;
        var time = Time.time * shakeSpeed;
        var noiseX = (Mathf.PerlinNoise(time, time) - .5f) * shakePower * shakeDecayCurve.Evaluate(shakeAge / shakeLifeTime);
        var noiseY = (Mathf.PerlinNoise(time, time + 128) -.5f) * shakePower * shakePower * shakeDecayCurve.Evaluate(shakeAge / shakeLifeTime);
        transform.rotation = Quaternion.Euler(noiseX, noiseY, 0) * transform.rotation;
    }

    IEnumerator SetShake(float waitTime)
    {
        shake = true;
        yield return new WaitForSeconds(waitTime);
        shake = false;
    }


    public void RotateCamera(Vector2 input)
    {
        if (cameraMode != CameraMode.Manual) return;
        input.y *= -1;
        var isExMaxLim = input.y < 0 && transform.forward.y > minVerticalRotationLimit;
        var isExMinLim = input.y > 0 && transform.forward.y < -maxVerticalRotationLimit;
        if (isExMaxLim || isExMinLim) input.y = 0;
        input *= Time.deltaTime;
        orbitalCenter.RotateAround(orbitalCenter.position, Vector3.up, input.x);
        orbitalCenter.RotateAround(orbitalCenter.position, transform.right, input.y);
    }

    public void ShakeCamera(float power, float speed, float shakeTime)
    {
        shakePower = power;
        shakeSpeed = speed;
        shakeLifeTime = shakeTime;
        shakeAge = 0;
        IEnumerator setShale = SetShake(shakeTime);
        StartCoroutine(setShale);
    }
}


独学素人のスクリプトなのでごちゃごちゃしているがとりあえず動くものができた。


パラメータの説明

PrimaryTarget:追従するゲームオブジェクト
SecondaryTarget:ロックオンするゲームオブジェクト
PrimaryLockOnPosition:PrimaryTargetの画面内の位置
SecondaryLockOnPosition:SecondaryTargetの画面内の位置
CameraMode:カメラの挙動の設定(後述)
Distance:カメラとPrimaryTargetの距離
Height:回転する軸の高さ
Delay:カメラがPrimaryTargetを追従時の遅延
MaxVerticalRotaionLimit:カメラの上から覗き込む角度制限
MinVerticalRotaionLimit:カメラの下から覗き込む角度制限
Layer Mask:カメラの当たり判定をする対象(後述)
CastRadius:カメラの当たり判定の半径(後述)
LockOnSpeed:ロックオン時のSecondaryTargetの追従速度
ShakeDecayCurve:CameraShake時の減衰を指定するカーブ(後述)
CursorLock:カーソルをロックするかどうか
InputInThisScript:このスクリプト内で操作するかどうか(後述)

既知の問題点

ロックオン機能はPrimaryLockOnPositionやSecondaryLockOnPositionが(0, 0)から離れる程、またDistanceが大きい程不安定になる。特に2ターゲット間の距離が短く、かつ上記のパラメータが大きいとカメラがくるくるまわる。

public関数について

RotateCamera(Vector2 input)とShakeCamera(float power, float speed, float shakeTime)がある。RotateCameraはマウスやスティックの入力を突っ込めば回転する。カメラ操作の反転や感度設定については外部スクリプトからマウスやスティックの入力をいじって実装する。ShakeCameraはカメラを振動させる。powerで振動の大きさ、speedで振動の速さ、shakeTimeで振動時間を設定する。ShakeDecayCurveから時間に応じた振動の減衰のしかたを指定できる。

Camera Modeについて

Manual:マウス等でカメラを操作する
Auto:自動でPrimaryTargetの前方向を向く
LockOn:SecondaryTargetをロックオンする

カメラの当たり判定について

カメラの当たり判定とは、いわゆる壁際だとカメラが近づく三人称視点特有のあれ。Layer Maskから当たり判定をするレイヤーを指定できる。またCast Radiusの半径を調整することで壁の裏の世界を見せないようにできる。

Input In This Scriptについて

Input In This Scriptにチェックを入れるとこのスクリプトだけでマウスからカメラを操作できる(Camera ModeがManual時のみ)。だたしこれはUnityの新しいInputSystemを使用しているので、古いバージョンを使っていたりInputSystemを導入していなかったりすると機能しない。InputSystemはPackageManagerからダウンロードしてProjectSettings>Player>OtherSettingsからActiveInputHandlingをInputSystemかBothにすれば使える。

コメント

このブログの人気の投稿

Unityで移動速度とアニメーション速度を同期させて足滑りを防ぐ方法