Ace Combat Zero: 유니티로 구현하기 #10 : 락온

Lunetis·2021년 5월 2일
0

Ace Combat Zero

목록 보기
11/27
post-thumbnail



미사일 락온

미사일을 발사하기 전에, 어느 목표물에 발사할 지 결정하는 과정이 필요합니다.

목표물을 선택해서 조준 상태로 만드는 것을 락온(Lock-on) 이라고 합니다.
락온 상태에서 미사일을 발사하면, 미사일은 조준된 목표물을 향해 날아가게 됩니다.


실제로는 락온을 하기 위해 레이더와 적외선 등 여러 기술들을 조합해서 사용합니다.
하지만 게임에서 쓰는 락온 로직은 과연 실제와 비슷할까요?

아마 실제와는 완전히 다른, 게임에 사용하기 적절한 락온 알고리즘을 사용할 것입니다.


먼저 게임 (에이스 컴뱃 7 기준)에서는 어떻게 표현하는지 살펴보죠.

탐지 범위 내에 목표물이 들어오면 락온 UI가 목표물을 따라가고,
락온이 완료되면 목표물 UI 위치에 고정되고 UI가 붉게 변합니다.

락온이 완료된 상태에서 미사일을 발사하면, 목표물을 따라가게 됩니다.
락온이 완료되지 않았는데 미사일을 발사하면 그냥 일직선으로 날아갑니다.

일반 미사일을 선택했을 때 락온 UI의 모양은 45도 돌린 정사각형 모양이고,
특수 미사일을 선택했을 때 락온 UI의 모양은 조준선 모양입니다.

무기를 교체할 때마다 UI도 바꿔줘야 하고, 락온 상태도 초기화해야 하죠.

그리고 탐지 범위를 벗어나게 되면 락온이 풀립니다.



그래서 알고리즘은 어떻게 돼요?

에이스 컴뱃에서 사용하는 락온 알고리즘이 어떤 방식인지는 모릅니다.

플레이 경험을 기반으로 추측해보는 수밖에 없죠.


먼저 탐색 범위에 대해서 생각해보자면,

  • 락온 가능한 거리와 각도는 한정되어 있음
  • 탐지 거리와 각도는 미사일마다 다름

미사일에 탐지 능력이 종속되기 때문에,
미사일 스크립트에 탐지 거리와 각도를 추가해야 할 것 같습니다.


그리고 탐색 알고리즘을 생각해보죠.

  • 중앙에 있는 목표물은 빠르게, 가장자리에 있는 목표물은 느리게 락온됨
  • 한 번 락온된 목표물은 탐지 범위를 넘어서기 전까지는 풀리지 않음

중요한 건 비행기가 바라보는 방향에 목표물이 가까이 있을수록 락온이 빠르게 된다는 것이죠.
진행 방향과 목표물과의 각도를 계산하는 절차가 필요할 것 같습니다.

각도가 작을수록 락온이 빨라지도록 만들어야겠네요.


추가로 UI도 생각해봐야 하는데요,

  • 락온 시도중일 때, 락온이 몇 %만큼 진행중인지를 알려주기 위해서 UI가 조준점에서부터 목표물까지 서서히 이동해야 함



여기까지 생각하고 제가 나름 생각했던 알고리즘을 설명해보자면,

초록색이 탐지 범위고, 빨간색이 목표물이라고 가정합시다.

타겟으로 선택된 목표물이 탐지 범위 내에 없으면 아무것도 하지 않습니다.

탐지 범위에 목표물이 들어오면, 매 프레임마다 비행기가 바라보는 방향, 비행기와 목표물 벡터 사이의 각도 (targetAngle)를 계산합니다.

그와 동시에 매 프레임마다 중앙에서부터 각도를 넓혀가는 계산을 수행합니다. (angle += ?)

이 계산되는 각도가 목표물과의 각도보다 크거나 같으면 (angle >= targetAngle)
락온이 완료되는 방식입니다.

만약 목표물이 중앙에 가까이 있었다면, targetAngle 값이 0에 가까울테니 락온이 빠르게 되었겠죠.


아직 락온을 시도중인 상태라면, 현재 각도와 목표물과의 각도를 계산해서 현재 각도에 해당하는 부분에 락온 UI를 배치합니다.

위치는 조준점과 목표물과의 화면 상의 거리 * (현재 각도 / 목표물과의 각도) 입니다.

이렇게 각도를 넓히다가 목표물과의 각도에 다다르게 되면, 락온이 완료됩니다.

이 방식으로 구현하면, 비행기가 바라보는 방향에 목표물이 가까이 있을수록 빠르게 락온이 되고,
UI를 띄워줄 때도 (현재 각도 / 목표물과의 각도) 값으로 UI의 위치를 조정해줄 수 있습니다.



상상한 알고리즘대로 구현하기

미사일 데이터 추가

이 알고리즘을 구현하기 위해서 미사일마다 3가지 값을 가지고 있어야 합니다.

  • 탐지 거리
  • 탐지 각도
  • 락온 각도를 넓히는 속도

이 중에서 탐지 각도는 처음에 미사일을 구현할 때 만들었으니, 탐지 거리와 락온 속도만 추가하면 되겠네요.

public class Missile : MonoBehaviour
{
    ...

    public float targetSearchSpeed;
    public float lockDistance;
    
    ...

Missile 스크립트에 파라미터를 추가하고,

이전에 만들었던 미사일 2개의 파라미터 값을 설정합니다.

특수 미사일의 성능은 일반 미사일보다 좀 더 높게 설정했습니다.


목표물 탐지

GameManager.cs

public static float GetAngleBetweenTransform(Transform otherTransform)
{
    Vector3 direction = PlayerAircraft.transform.forward;
    Vector3 diff = otherTransform.position - PlayerAircraft.transform.position;
    return Vector3.Angle(diff, direction);
}

먼저 구현에 사용할 유틸리티를 하나 추가했습니다.

float GetAngleBetweenTransform(...) : 비행기와 다른 오브젝트 사이의 각도를 구합니다.

위에서 설명할 때 썼던 그림입니다. 파란색으로 표시된 각도를 구하는 유틸리티입니다.

탐지 각도 내에 목표물이 있는지를 확인하기 위해서도 사용합니다.



TargetLock.cs

public class TargetLock : FollowTransformUI
{
    RawImage rawImage;

    [SerializeField]
    Texture mslLockTexture;
    [SerializeField]
    Texture spwLockTexture;
    [SerializeField]
    RectTransform crosshair;

    // From Missile Data
    float targetSearchSpeed;
    float boresightAngle;
    float lockDistance;

    // Status
    float lockProgress;
    bool isLocked;

    public bool IsLocked
    {
        get { return isLocked; }
    }

    // Calculated Target Screen Position
    Vector2 targetScreenPosition;

    // Set image invisible
    void ResetLock()
    {
        isLocked = false;
        lockProgress = 0;
        rawImage.color = GameManager.NormalColor;
        rawImage.enabled = false;

        GameManager.TargetController.SetTargetUILock(false);
    }

    public void SetTarget(Transform targetTransform)
    {
        this.targetTransform = targetTransform;
        ResetLock();
    }

    public void SwitchWeapon(Missile missile)
    {
        // Change missile's angle and search speed
        boresightAngle = missile.boresightAngle;
        targetSearchSpeed = missile.targetSearchSpeed;
        lockDistance = missile.lockDistance;

        // Progress needs to be reset
        ResetLock();

        rawImage.texture = (missile.isSpecialWeapon == true) ? spwLockTexture : mslLockTexture;
    }


    void CheckTargetLock()
    {
        // No target
        if(targetTransform == null)
        {
            ResetLock();
            return;
        }

        float distance = Vector3.Distance(targetTransform.position, GameManager.PlayerAircraft.transform.position);

        // Exceed lockable distance
        if(distance > lockDistance)
        {
            ResetLock();
            return;
        }
        
        cam = GameManager.Instance.cameraController.GetActiveCamera();
        Vector3 screenPosition = cam.WorldToScreenPoint(targetTransform.position);
        
        // if screenPosition.z < 0, the object is behind camera
        if(screenPosition.z > 0)
        {
            // UI Position
            Vector2 screenPoint = RectTransformUtility.WorldToScreenPoint(cam, targetTransform.position);
            targetScreenPosition = screenPoint - canvasRect.sizeDelta * 0.5f;
        }
        
        float targetAngle = GameManager.GetAngleBetweenTransform(targetTransform);
        

        // When the target exists, increase lockProgress
        // if lockProgress >= targetAngle, it means the target is locked

        // Missed the Target
        if(targetAngle > boresightAngle)
        {
            ResetLock();
        }

        // Locking...
        else
        {
            rawImage.enabled = true;

            // Lock Progress
            if(isLocked == false)
            {
                lockProgress += targetSearchSpeed * Time.deltaTime;
            }

            // Locked!
            if(lockProgress >= targetAngle)
            {
                isLocked = true;
                lockProgress = boresightAngle;
                rawImage.color = GameManager.WarningColor;

                GameManager.TargetController.SetTargetUILock(true);
            }
            // Still Locking...
            else
            {
                isLocked = false;
                rawImage.color = GameManager.NormalColor;
            }

            rectTransform.anchoredPosition = Vector2.Lerp(crosshair.anchoredPosition, targetScreenPosition, lockProgress / targetAngle);
        }
    }

    void Awake()
    {
        rawImage = GetComponent<RawImage>();
    }

    // Start is called before the first frame update
    protected override void Start() 
    {
        base.Start();
        ResetLock();
    }

    // Update is called once per frame
    protected override void Update()
    {
        CheckTargetLock();
    }
}

락온 기능과 UI를 컨트롤하는 스크립트를 하나 추가했습니다.
이 스크립트는 이전에 만들었던 FollowTransformUI (화면 상의 오브젝트를 따라가는 UI) 를 상속받습니다.


이 스크립트가 하는 일은 대충 다음과 같습니다.

  • 목표물이 탐지 범위 내에 있는지 확인, 각도 계산 (targetAngle에 저장)
  • 범위 내에 있을 경우, 매 프레임마다 lockProgress를 증가시킴
  • 락온 도중에는 락온 UI를 목표물에 점점 가까이 가도록 만듦
  • lockProgress >= targetAngle 일 때 락온 활성화, 색 변경
  • 탐지 범위를 벗어나거나, 무기를 교체할 경우 락온 비활성화, 색 변경 (처음부터 계산)

락온이 되었는지에 대한 여부는 IsLocked로 알아냅니다.

락온 UI를 점점 가까이 가도록 만드는 방법에 대해서 설명하자면,

락온 도중에는 조준점으로부터 목표물까지 UI가 움직이는 것처럼 만들어주려고 합니다.

단순하게 매 프레임마다 두 지점을 잇는 직선 상의 어딘가에 놓는 방식으로요.

조준점이 0, 목표물이 1이라고 할 때, (lockProgress / targetAngle) 에 해당하는 위치에 UI를 놓습니다.

lockProgress = 45, targetAngle = 90일 경우에는 두 지점의 중간에 UI가 놓이게 됩니다.


UI 기능 추가


TargetUI.cs

...
public void SetLock(bool isLocked)
{
    if(isLocked == true)
    { 
        SetBlink(false);
        frameImage.color = GameManager.WarningColor;
    }
    else
    {
        SetTargetted(targetObject != null);
        frameImage.color = GameManager.NormalColor;
    }
}

TargetUI도 기능을 추가해줘야 합니다.

목표물로 선택되었을 때는 깜빡이는 이펙트를 실행하지만,
락온이 완료되면 깜빡임을 멈추고 사각형 테두리의 색상을 빨간색으로 바꿔줘야 합니다.

락온이 풀리면 다시 이전 상태로 돌아가야 하고요.

락온 여부에 따라 깜빡임과 색상을 변경시키는 코드만 추가합니다.



TargetController.cs

public void SetTargetUILock(bool isLocked)
{
    currentTargettedUI?.SetLock(isLocked);
}

모든 타겟을 관리하는 TargetController 도 손봐줍시다.
TargetLock에서 목표물이 락온되거나 해제되었다는 것을 여기로 알려주면, 현재 선택된 TargetUISetLock()을 호출합니다.



WeaponController.cs

void LaunchMissile(ref int weaponCnt, ref ObjectPool objectPool, ref WeaponSlot[] weaponSlots)
    {
        ...
        Transform targetTrasnform = (target != null && GameManager.TargetController.IsLocked == true) ? target.transform : null;
        missileScript.Launch(targetTrasnform, aircraftController.Speed + 15, gameObject.layer);

이제 미사일을 쏠 때 락온이 되었는지, 안 되었는지 확인하는 코드를 추가합시다.

타겟이 있으면서 락온이 완료되었다면(IsLocked == true) 미사일에게 따라갈 목표물을 넘겨주고,
타겟이 없거나, 있어도 락온이 안 되었다면 목표물을 null로 넘겨줘서 그냥 일직선으로 날아가게 합니다.


먼저 락온 UI를 만든 다음 TargetLock 스크립트를 추가하고, 일반 미사일/특수 미사일에 사용할 텍스처를 연결합니다.
그리고 조준점으로부터 UI가 움직여야 하니까 조준점의 RectTransform도 연결해줍니다.

TargetController가 있는 스크립트에는 TargetLock을 가지고 있는 오브젝트를 추가합니다.

모든 스크립트를 추가하고 데이터를 추가/수정했다면, 이제 테스트해볼 차례입니다.

목표물이 선택됐지만 아직 탐지 거리 (1500)에 들어가지 않는 동안에는 락온 UI가 보이지 않습니다.

탐지 거리와 범위 내에 들어가면, 락온 UI가 나타나서 목표물이 있는 곳까지 이동합니다.
락온이 완료되면 UI가 붉게 변합니다.

탐지 범위를 벗어나면 락온이 풀리는 것도 확인하고,

다양하게 기동하면서 락온 기능을 확인합니다.

무기 발사도 테스트해봅니다.

락온이 안 되어있거나 락온 중에 미사일을 발사하면 일직선으로 날아가고,
락온이 완료된 상태에서는 미사일이 목표물을 향해 날아갑니다.

무기 변경 시에도 락온 기능이 잘 작동하는지 확인합시다.



이 정도면 거의 다 확인한 것 같네요.

제 생각에는 나름 원본 게임과 비슷하게 구현한 것 같습니다.



카메라 락온

유튜브에서 플레이 영상을 보시면, 타겟된 목표물에 시선을 고정시킨듯한 화면을 볼 수 있습니다.

젤다의 전설이나 몬스터 헌터 등의 3D 게임에서도 특정 목표물에 시선을 고정시키는 기능이 있습니다.
이 기능을 두고 카메라 락온이라고 부르는 게임도 있고, 주목이라고 부르는 게임도 있지만, 아무튼 중요한 건 목표물에 시선을 고정시킨다는 점입니다.

일단 이 포스트에서는 편의상 카메라 락온이라고 부르겠습니다.

에이스 컴뱃에서도 비슷한 기능이 있는데, 플레이스테이션 컨트롤러 (듀얼쇼크/듀얼센스) 기준으로 △ 키를 오래 누르고 있으면 현재 타겟된 목표물에 시선이 맞춰지게 됩니다.

(게임을 한지 2년이 넘도록 되도록 몰랐다가 이걸 작성하는 시점에서 알았다는 놀라운 사실이 있습니다.)


이전에 카메라를 구현할 때 이 기능을 구현하지 못한 이유는, 당시에는 아직 타겟 시스템을 구현하지 않았기 때문이었습니다.

하지만 이제는 카메라 락온 기능을 구현할 준비가 되었습니다. 시작해보죠.


Input 속성 변경

△버튼을 짧게 누르면 타겟을 변경하는 기능이 작동합니다.
하지만 1:1 보스전을 만들거니까 바꿀 타겟이 없으니 그냥 유야무야 넘어갔었는데요,

사실 언급만 안 해서 그렇지 저는 다 만들어놓고 있었습니다.
(언젠가 일대다 섬멸전을 만드리라 꿈꾸면서 말이죠)

어쨌든 지금 △버튼은 단순한 버튼 입력만 처리하도록 되어있고, 오래 누르는 홀드에 대해서는 아직 처리하고 있지 않습니다.


1편에서 다루었던 입력 시스템을 다시 건드릴 시간입니다.

그 때 뭐라고 작성했는지 잠깐 볼까요?



Hold를 감지하기 위한 pressPoint가 있고, 이 pressPoint를 넘어가는 순간 started가 실행됩니다.
그리고 그 상태에서 duration만큼의 시간이 흐르면 그때가 되어서야 performed가 실행됩니다.


Hold에 대한 예시를 적어놨었는데, 그 때는 그냥 짚고 넘어가는 수준이었지만 이제는 진짜로 활용해볼 때입니다.

아마 사용할 필요도 없어서 △키에 해당하는 액션을 만들어놓지도 않았을 수 있었을텐데요,
없었다면 하나 만들어줍시다.

그리고 Interactions의 + 버튼을 눌러서 Hold를 추가해줍니다.

Hold Interation이 추가되면, 이 버튼은 Hold 이벤트를 처리할 수 있게 됩니다.

지금 Hold Time이 0.4초로 되어 있는데, 바꾸고 싶다면 Default를 풀고 입맛대로 조절하면 됩니다.

저는 0.3초로 하겠습니다.




이제 버튼을 누를 때 실행하는 코드를 수정해야 합니다.

짧게 누르면 타겟 변경, 길게 누르면 타겟을 변경하지 않고 시점만 이동하도록 구현해줘야 합니다.


WeaponController.cs

public void ChangeTarget(InputAction.CallbackContext context)
{
    if(context.action.phase == InputActionPhase.Started)
    {
        isFocusingTarget = false;
    }

    // Hold Interaction Performed (0.3s)
    else if(context.action.phase == InputActionPhase.Performed)
    {
        if(target == null) return;

        GameManager.CameraController.LockOnTarget(target.transform);
        isFocusingTarget = true;
    }
    
    else if(context.action.phase == InputActionPhase.Canceled)
    {
        // Hold
        if(isFocusingTarget == true)
        {
            GameManager.CameraController.LockOnTarget(null);
        }
        // Press
        else
        {
            TargetObject newTarget = GetNextTarget();
            if(newTarget == null || (newTarget != null && newTarget == target)) return;

            target = GetNextTarget();
            target.isNextTarget = false;
            GameManager.TargetController.ChangeTarget(target);
        }
    }
}

입력 이벤트에 할당되어있는 ChangeTarget 함수를 수정합니다.

일단 버튼을 누르면 Started 상태가 됩니다.
이 때는 isFocusingTarget = false로 놓아서 "아직 타게팅 안 됐음. 그냥 타겟 변경하려고 하나?" 이라고 설정합니다.


버튼을 누른 상태로 Hold Time만큼 시간이 자니면 Performed 상태가 됩니다.
이 때는 isFocusingTarget = true로 놓아서 "어 이거 카메라 락온임. 타겟 변경 아님." 이라고 설정합니다.

여기서 CameraController.LockOnTarget(...)을 실행해서 카메라에게 현재 목표물을 비춰주라는 명령을 내릴 겁니다. (구현은 아래에 있습니다.)
물론 현재 선택된 타겟이 없으면 이 부분은 실행하지 않습니다.


마지막으로 버튼을 떼면 Canceled 상태가 됩니다.
isFocusingTarget 값에 따라서 (이게 타겟 변경인가 아니면 카메라 락온인가) 실행할 코드가 달라지게 됩니다.

카메라 락온이었다면 (true) 카메라에게 현재 타겟을 그만 비춰달라는 명령을 전달합니다.
타겟 변경이었다면 (false) 새 타겟 또는 다음 타겟을 설정합니다.


카메라 락온 기능 - 3인칭

CameraController.cs

Transform lockOnTargetTransform;

void Rotate3rdViewCamera()
{
    Transform cameraTransform = currentCamera.transform;
    Quaternion rotateQuaternion;
    
    if(lockOnTargetTransform != null)
    {
        rotateQuaternion = Quaternion.Lerp(thirdViewCameraPivot.localRotation, CalculateLockOnRotation(), lerpAmount * Time.deltaTime);
        thirdViewCameraPivot.localRotation = rotateQuaternion;
    }
    else
    {
        Vector3 rotateValue = new Vector3(lookInputValue.y * -90, lookInputValue.x * 180, rollValue * rollAmount);
        rotateQuaternion = Quaternion.Lerp(thirdViewCameraPivot.localRotation, Quaternion.Euler(rotateValue), lerpAmount * Time.deltaTime);
    }
    thirdViewCameraPivot.localRotation = rotateQuaternion;

    Vector3 adjustPosition = new Vector3(0, pitchValue * pitchAmount - Mathf.Abs(lookValue.y) * 1.5f, -zoomValue * zoomAmount);
    thirdViewCameraPivot.localPosition = thirdPivotOriginPosition + adjustPosition;
}

public void LockOnTarget(Transform targetTransform)
{
    lockOnTargetTransform = targetTransform;
}

public Quaternion CalculateLockOnRotation()
{
    Vector3 targetLocalPosition = transform.InverseTransformPoint(lockOnTargetTransform.position);
    Vector3 rotateVector = Quaternion.LookRotation(targetLocalPosition, transform.up).eulerAngles;
    rotateVector.z = 0; // z value must be 0
    return Quaternion.Euler(rotateVector); // Recalculate
}

void Update()
{
    lookValue = Vector2.Lerp(lookValue, lookInputValue, lerpAmount * Time.deltaTime);
    ...
}

우선 3인칭 카메라 코드만 다뤄보죠.

WeaponController에서 실행하는 CameraController.LockOnTarget()은 시점을 고정시킬 목표물 (lockOnTargetTransform)을 설정하는 역할을 합니다.

그리고 Rotate...() 함수에서는 "현재 고정시킬 목표물이 있는가? if(lockOnTargetTransform != null)" 를 판단해서 기존처럼 계산할지, 아니면 현재 입력값에 상관없이 목표물을 보여줄지 결정합니다.


CalculateLockOnRotation() 에서는 카메라가 회전할 값을 반환하는데,
여기서는 목표물의 좌표를 비행기의 로컬 좌표로 변환시키는 과정이 필요합니다.

transform.InverseTransformPoint(...);을 사용해서 로컬 좌표로 변환시키고,
그 좌표를 다시 회전값으로 변환할 때는 Quaternion.LookRotation(...);을 사용합니다.
여기서 카메라 회전값 중 Z는 항상 0이어야 하기 때문에, Vector3로 먼저 변환한 다음 다시 Quaternion으로 변환합니다.


이전에는 회전시킬 때 Vector3 로 사용했지만, 지금은 Quaternion으로 모두 변환하고 Lerp로 값을 조절해서 사용합니다.
그 이유는 localEulerAngles에 바로 대입하면 카메라가 미친듯이 돌아가는 이슈가 발생했기 때문입니다.

예를 들어 -90도는 270도와 동일해서, -90도인 물체에 Lerp 없이 바로 270도를 대입하면 카메라에 변화가 없습니다.
하지만 Lerp를 사용하면 -90에서 270으로 바뀌는 과정이 보이기 때문에 카메라가 360도 회전하게 됩니다.

Vector3.Lerp를 사용하면 이런 문제가 생기길래 이걸 어떻게 해결하나 하다가,
Quaternion.Lerp를 사용할 때는 문제가 발생하지 않길래,


아예 Quaternion을 사용하도록 통일시켰습니다.


날아가면서 △버튼을 길게 눌렀다 떼어봅니다. 제대로 포커싱되는 것 같네요.

가끔 테스트하다보면 90도 이상 홱 돌아가는 것 같은 느낌을 주는 때가 있는데, 정상입니다. 원래 게임도 그래요.



카메라 락온 기능 - 1인칭

// Without Cockpit view
void Rotate1stViewCamera()
{
    Quaternion rotateQuaternion;
    
    if(lockOnTargetTransform != null)
    {
        rotateQuaternion = Quaternion.Lerp(firstViewCameraPivot.localRotation, CalculateLockOnRotation(), lerpAmount * Time.deltaTime);
    }
    else
    {
        Vector3 rotateValue = new Vector3(lookValue.y * -90, lookValue.x * 180, 0);
        rotateQuaternion = Quaternion.Lerp(firstViewCameraPivot.localRotation, Quaternion.Euler(rotateValue), lerpAmount * Time.deltaTime);
    }
    
    firstViewCameraPivot.localRotation = rotateQuaternion;
    uiController.AdjustFirstViewUI(rotateQuaternion.eulerAngles);
}

// With Cockpit view
void Rotate1stViewWithCockpitCamera()
{
    Quaternion rotateQuaternion;
    
    if(lockOnTargetTransform != null)
    {
        rotateQuaternion = Quaternion.Lerp(firstViewCameraPivot.localRotation, CalculateLockOnRotation(), lerpAmount * Time.deltaTime);

        // + Adjust/Clamp value
        Vector3 rotateValue = rotateQuaternion.eulerAngles;
        if(rotateValue.x > 180) rotateValue.x -= 360;
        if(rotateValue.y > 180) rotateValue.y -= 360;
        rotateValue.x = Mathf.Clamp(rotateValue.x, -90, 27);
        rotateValue.y = Mathf.Clamp(rotateValue.y, -135, 135);

        rotateQuaternion = Quaternion.Euler(rotateValue);
    }
    else
    {
        Vector3 rotateValue = new Vector3(lookValue.y * -90, lookValue.x * 135, 0);
        if(rotateValue.x > 0) rotateValue.x *= 0.3f;
        rotateQuaternion = Quaternion.Lerp(firstViewCameraPivot.localRotation, Quaternion.Euler(rotateValue), lerpAmount * Time.deltaTime);
    }
    
    firstViewCameraPivot.localRotation = rotateQuaternion;
    uiController.AdjustFirstViewUI(rotateQuaternion.eulerAngles);
}

이제 나머지 1인칭 카메라들도 조정해줍시다.

1인칭에서 유의해야 하는 부분은, 현재 시점에 따라서 UI 위치도 바뀌어야 한다는 것입니다.

1인칭 시점에서 카메라 락온을 할 때도 이 UI 위치는 계속 보정되어야 합니다.

uiController.AdjustFirstViewUI(...)에는 Vector3 값을 넘겨줘야 하는데, 이 값은 Quaternion을 다시 Vector3로 변환해서 넘겨주도록 하겠습니다.


그리고 콕핏이 보이는 1인칭 시점은 다른 시점과는 다르게 돌리는 각도에 제한이 있습니다.
Mathf.Clamp를 이용해서 최대값/최소값을 벗어나지 않도록 조절해줍시다.


UIController.cs

public void AdjustFirstViewUI(Vector3 cameraRotation)
{
    if(cameraRotation.x > 180) cameraRotation.x -= 360;
    if(cameraRotation.y > 180) cameraRotation.y -= 360;
        
    ...
}

UIController도 약간 손봐줍니다.

CameraController에서 AdjustFirstViewUI를 호출할 때, 넘어온 Vector3xy값이 (-180 ~ 180) 사이어야 하지만 따로 보정을 하지 않으면 (0 ~ 360)으로 넘어옵니다.
180이 넘어가는 경우에는 360을 빼서 -180 ~ 0의 구간으로 변환해주는 과정을 거쳐줍니다.


1인칭 화면에서 락온을 테스트해봅니다.
HUD UI 위치도 제대로 나오는지, 콕핏 시점에서는 각도 제한이 제대로 작동하는지 확인합니다.




미사일 락온과 카메라 락온 구현이 모두 끝났다면, 신나는 루프 기동을 하면서 미사일을 날려줍시다.



이 프로젝트의 작업 결과물은 Github에 업로드되고 있습니다.
https://github.com/lunetis/OperationZERO

0개의 댓글