Ace Combat Zero: 유니티로 구현하기 #3 : 카메라 조작

Lunetis·2021년 3월 13일
1

Ace Combat Zero

목록 보기
4/27
post-thumbnail




3가지 시점

에이스 컴뱃 시리즈에서는 3가지 시점을 지원합니다.

각 시점에서는 오른쪽 스틱을 조종해서 주변을 바라볼 수 있고,
시점 전환은 오른쪽 스틱을 눌러서 (듀얼쇼크 : R3, XBOX 컨트롤러 : RS) 할 수 있습니다.

  1. 1인칭 시점에서 콕핏이 보이지 않는 시점입니다.
    카메라를 돌리면 제자리에서 주위를 돌아봅니다.
    비행기와 신체가 보이지 않기 때문에 모든 각도로 360도 볼 수 있습니다. (발 밑까지!)
    즉, 돌리는 각도에 한계가 없습니다.

  1. 1인칭 시점에서 콕핏이 보이는 시점입니다.
    카메라를 돌리면 역시 제자리에서 주위를 돌아보지만,
    실제 사람의 시점에서 보기 때문에 돌리는 각도에 한계가 있습니다.
    좌우는 약 135도까지, 상하는 위는 한계가 없지만 아래로는 콕핏까지 볼 수 있습니다.

  1. 3인칭 시점입니다.
    기본적으로 비행기 뒤에 카메라가 위치하고, 시점을 이동하면 비행기를 중심으로 카메라가 돕니다.

일반적으로는 시야 확보를 위해 콕핏 없는 1인칭 시점과 3인칭 시점을 주로 사용합니다.
비행기 커스터마이징 (스킨, 엠블럼 등)은 3인칭 시점에서 볼 수 있죠.


시점 전환

카메라 추가하기

둘러보기 이전에 좀 더 쉬운 카메라 전환부터 해보겠습니다.

근데 전환할 카메라가 있어야겠죠?

1인칭 시점과 3인칭 시점 카메라는 하나씩 있었으니, 1인칭 시점 카메라 하나를 복사했습니다.

여기서 1st View (No Cockpit) 카메라는 내 비행기가 보이지 않아야 합니다.
비행기를 안 보이게 하는 작업부터 시작하겠습니다.

먼저 레이어를 추가하는 작업을 해야 합니다.

아무 오브젝트나 클릭하고 Layers - Add Layer...를 클릭하거나,
Edit - Project Settings... - Services - Tags and Layers로 들어갑니다.

Aircraft라는 레이어를 추가하겠습니다.

그 다음 조종할 비행기의 모델링을 선택하고 Layer를 아까 만든 레이어로 바꿔줍니다.

Yes, change children을 선택해서 하위 모델링들도 동일한 레이어가 되도록 합시다.

비행기가 안 보이게 할 카메라를 선택하고, Culling Mask를 클릭한 후
방금 등록한 레이어를 체크 해제합니다.

옆에 있는 카메라 프리뷰에서 볼 수 있듯이 비행기가 카메라에서 더 이상 보이지 않게 됩니다.


카메라 전환하기

전술했듯이, 오른쪽 스틱을 눌러서 (듀얼쇼크 : R3, XBOX 컨트롤러 : RS) 시점을 전환할 수 있습니다.

Input Actions에서는 Gamepad - Right Stick Press로 매핑하면 됩니다.

지금까지는 입력한 값을 테스트하느라 AircraftController.cs에 모든 값을 받아서 테스트했었습니다.
이제는 카메라와 이동을 분리하죠.
CameraController.cs를 만들었습니다.


public class CameraController : MonoBehaviour
{
    public DebugText debugText;
    
    Vector2 lookValue;

    public Camera[] cameras = new Camera[3];
    Camera currentCamera;
    public Canvas canvas;

    int cameraViewIndex = 0;
    
    public void Look(InputAction.CallbackContext context)
    {
        lookValue = context.ReadValue<Vector2>();
    }

    public void ChangeCameraView(InputAction.CallbackContext context)
    {
        if(context.action.phase == InputActionPhase.Performed)
        {
            cameraViewIndex = (++cameraViewIndex) % 3;
            SetCamera();
        }
    }

    void SetCamera()
    {
        for(int i = 0; i < cameras.Length; i++)
        {
            if(i == cameraViewIndex)
            {
                currentCamera = cameras[i];
                canvas.worldCamera = currentCamera;
            }
            cameras[i].enabled = (i == cameraViewIndex);
        }
    }

    // Start is called before the first frame update
    void Start()
    {
        SetCamera();
    }

    // Update is called once per frame
    void Update()
    {
        debugText.AddText("look\t: " + lookValue);
        debugText.AddText("cameraViewIndex\t: " + cameraViewIndex);
    }
}

일단 (InputAction.CallbackContext context)를 매개변수로 받는 Look(), ChangeCameraView(), 관련된 변수들은 AircraftController.cs에서 빼왔습니다.

SetCamera()에서는 현재 cameraViewIndex에 따라 어떤 카메라를 보여줄지 (정확히 말하면 어떤 카메라만 활성화시키고 나머지를 비활성화시킬지) 결정합니다.

그리고 현재 입력값을 디버깅하기 위해 그동안 게임 화면 왼쪽 위에 텍스트 UI를 띄워놨었습니다.
이 UI는 카메라가 바뀌어도 계속 보여야 하기 때문에,
Canvas 컴포넌트를 가져와서 canvas.worldCamera를 선택한 카메라로 바꿔줍니다.

CameraController.cs를 비행기에 추가한 후, Cameras에 카메라들을 모두 추가합니다.

그리고 PlayerInput에서 카메라와 연관된 Events를 모두 CameraController.cs의 함수로 연결시킵니다.

오른쪽 스틱을 누를 때마다, cameraViewIndex가 바뀌면서 카메라의 시점이 전환됩니다.


카메라 회전하기

1. 콕핏 없는 1인칭

제일 쉬운 콕핏 없는 1인칭입니다.
값 보정 없이 그대로 회전시키면 되니까요.

void Rotate1stViewCamera()
{
    Vector3 rotateValue = new Vector3(lookValue.y * -90, lookValue.x * 180, 0);
    currentCamera.transform.localEulerAngles = rotateValue;
}

우선 1인칭 카메라 전용 함수를 추가했습니다.
상/하로는 90도, 좌/우로는 180도만큼 회전할 수 있습니다.

회전값 계산한 후 적용할 때는 localEulerAngles에 적용합니다.

enum CameraIndex
{
    ThirdView,
    FirstView,
    FirstViewWithCockpit,
}

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

    switch((CameraIndex)cameraViewIndex)
    {
        case CameraIndex.FirstView:
            Rotate1stViewCamera();
            break;
    }
    ...
}

카메라 상태를 나타낼 enum 값을 추가했고,

여기서도 화면을 돌아볼 때 바로 움직이지 않고 Lerp를 사용해서 약간의 딜레이를 주었습니다.
그리고 switch() 문에서는 현재 카메라 인덱스에 따라서 그 시점에 맞는 카메라 회전 함수를 실행시키도록 구현했습니다.

look 값에 따라서 화면이 잘 회전하고 있습니다. 약간의 딜레이도 포함해서요.
(좌, 우, 상, 하 순서대로 스틱을 움직이고 있습니다.)


2. 콕핏 있는 1인칭

1인칭 카메라와 비슷하지만 약간 다릅니다.
각도에 제한이 있어요.

void Rotate1stViewWithCockpitCamera()
{
    Vector3 rotateValue = new Vector3(lookValue.y * -90, lookValue.x * 135, 0);
    if(rotateValue.x > 0)
        rotateValue.x *= 0.3f;

    currentCamera.transform.localEulerAngles = rotateValue;
}

상/하는 90도, 좌/우는 135도 쯤으로 두되, 아래를 볼 때 (rotateValue.x > 0, 양수일 때 아래로 내려갑니다.)는 30%만 내려가도록 설정했습니다.

음... 약간 마음에 안 드네요. 날개가 보여야 하는데...





구조를 살짝 바꾸겠습니다.

카메라를 돌리는 방법 대신, 피봇을 만들어서 카메라를 약간 떨어진 곳에 넣고 피봇을 돌리겠습니다.

카메라 짐벌을 돌리는 것과 비슷하게요.

빈 오브젝트를 추가한 다음 자식으로 1인칭 카메라들을 넣어주고,

약간 떨어진 곳에 카메라를 놓아줍니다.

public Transform cameraPivot;

void Rotate1stViewCamera()
{
    Vector3 rotateValue = new Vector3(lookValue.y * -90, lookValue.x * 180, 0);
    cameraPivot.localEulerAngles = rotateValue;
}

void Rotate1stViewWithCockpitCamera()
{
    Vector3 rotateValue = new Vector3(lookValue.y * -90, lookValue.x * 135, 0);
    if(rotateValue.x > 0)
        rotateValue.x *= 0.3f;

    cameraPivot.localEulerAngles = rotateValue;
}

지금까지 만들었던 1인칭 카메라 조작 함수를 약간 수정합니다.
Transform cameraPivot을 추가하고, 카메라 대신 피봇의 localEulerAngles를 조정합니다.

이제 날개가 보이네요.


3. 3인칭

이번에는 비행기를 중심으로 도는 카메라를 만들어야 합니다.

(https://commons.wikimedia.org/wiki/File:Ways_To_Use_A_Selfie_Stick.png)

비행기를 중심으로 대략 20m쯤 되는 엄청나게 긴 셀카봉을 휘두른다고 생각하죠.

3인칭 카메라를 회전하는 중심축을 만든 다음, 그 하위에 3인칭 카메라를 놓습니다.

public Transform thirdViewCameraPivot;

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

    switch((CameraIndex)cameraViewIndex)
    {
        case CameraIndex.FirstView: ...
        case CameraIndex.FirstViewWithCockpit: ...
        case CameraIndex.ThirdView:
            Rotate3rdViewCamera();
            break;
    }
}

void Rotate3rdViewCamera()
{
    Transform cameraTransform = currentCamera.transform;
    Vector3 rotateValue = new Vector3(lookValue.y * -90, lookValue.x * 180, 0);
    thirdViewCameraPivot.localEulerAngles = rotateValue;    
}

3번째 카메라를 조작하는 함수도 아까와 비슷하게 만들어줍니다.

이 정도면 충분하겠네요.

추가: 비행기의 하부와 상부를 볼 때, 상부가 하부보다 조금 더 넓게 비추고 있는 것을 볼 수 있습니다.
카메라 피봇의 위치가 비행기보다 약간 위에 있어서 생긴 문제이며, 아래 항목에서 보정이 들어갑니다.


역동적인 카메라 움직임

지금 3인칭 카메라에는 큰 문제가 한 가지 있습니다.

아무리 격한 움직임을 해도 고정된 것처럼 보인다는 점이지요.

(실제로 고정되어 있고요.)

출처: https://youtu.be/ZUU-3wYJ1a4

실제로는 비행기가 기동할 때 비행기가 이동 방향으로 치우치는 것이 조금씩 보입니다.

비행기가 회전할 때는 카메라가 회전 방향에 맞게 약간 기울어지고,
가속/감속할 때는 카메라가 줌인/줌아웃되며,
기수 상승/하강 시에는 카메라가 방향에 맞춰 상/하로 움직입니다.

(가속/감속은 실제로 줌을 건드리는지 카메라가 앞/뒤로 위치를 이동하는지는 잘 모르겠습니다.)


플레이어가 어느 방향으로 이동하고 있는지 조금 더 확실하게 체감할 수 있고, 좀 더 역동적인 느낌을 받을 수 있습니다.

현재 상태로는 재미가 없으니, 카메라를 조금 더 역동적으로 만들어보겠습니다.




가속/감속, 회전, 기수 상승/하강시 카메라의 움직임을 구현하려면 각각의 값이 필요합니다.

이 값들은 #2. 비행기 움직이기를 구현하는 동안 얻어왔었고, AircraftController에서 사용하고 있습니다.

AircraftController에서 사용하고 있는 값들을 지금 구현중인 CameraController에 넘겨주는 방법을 사용하려고 합니다.

카메라 컨트롤러에도 입력을 받아오는 함수를 만드는 방법도 있지만,
코드가 너무 길어지고 입력 이벤트도 관리를 해야하기 때문에 일단 다른 방법을 사용하겠습니다.

참고: 위에서 했던 Look 입력 이벤트 변경 작업은 AircraftController에서 더 이상 필요가 없어진 값들을 CameraController로 옮겨오는 작업이었습니다.


CameraController

float zoomValue;
public float zoomLerpAmount;
public float zoomAmount;

float rollValue;
public float rollLerpAmount;
public float rollAmount;

float pitchValue;
public float pitchLerpAmount;
public float pitchAmount;

public void AdjustCameraValue(float aircraftAccelValue, float aircraftRollValue, float aircraftPitchValue)
{
    zoomValue = Mathf.Lerp(zoomValue, aircraftAccelValue, zoomLerpAmount * Time.deltaTime);
    rollValue = Mathf.Lerp(rollValue, aircraftRollValue, rollLerpAmount * Time.deltaTime);
    pitchValue = Mathf.Lerp(pitchValue, aircraftPitchValue, pitchLerpAmount * Time.deltaTime);
}

먼저 CameraController에서 값을 받아오는 함수를 작성합니다.
가속/감속을 받아오는 aircraftAccelValue, 회전을 받아오는 aircraftRollValue 두 개의 값을 가져와서, 카메라의 이동에 쓰일 zoomValuerollValue를 계산합니다.

이번에도 Lerp로 약간의 딜레이를 주겠습니다. 테스트 단계이므로 딜레이를 얼마나 줄 지 결정하는 ...LerpAmount는 public으로 두었습니다.


Vector3 thirdPivotOriginPosition;

void Start()
{
    ...
    thirdPivotOriginPosition = thirdViewCameraPivot.localPosition;
}

void Rotate3rdViewCamera()
{
    Transform cameraTransform = currentCamera.transform;

    Vector3 rotateValue = new Vector3(lookValue.y * -90, lookValue.x * 180, rollValue * rollAmount);
    Vector3 adjustPosition = new Vector3(0, pitchValue * pitchAmount - Mathf.Abs(lookValue.y), -zoomValue * zoomAmount);
    
    thirdViewCameraPivot.localEulerAngles = rotateValue;
    thirdViewCameraPivot.localPosition = thirdPivotOriginPosition + adjustPosition;
}

이 기능은 3인칭 카메라에서만 작동하므로 Rotate3rdViewCamera만 손봐줍시다.

먼저 비행기 회전에 따른 카메라 회전은 localEulerAngles = rotateValue에서 설정합니다.

Vector3 rotateValue의 z 값은 0이었지만, 이제 회전값만큼 약간 돌려주도록 rollValue * rollAmount를 추가했습니다.
rollValueAdjustCameraValue(...) 0 ~ 1 사이의 값을 가지고,
rollAmount는 최대로 돌아갈 각도입니다.


그 다음 가속/감속에 따른 카메라 줌인/줌아웃은 위치 이동으로 손보겠습니다.
이게 줌을 건드리는 건지, 카메라가 앞뒤로 움직이는건지는 솔직히 말해서 잘 모르겠거든요.

zoomValue * zoomAmount 값은 가속/감속 시 카메라가 앞뒤로 움직일 좌표입니다.
초기 좌표에다가 적당히 더하거나 빼서 가속/감속시의 움직임을 표현해주겠습니다.


pitchValue * pitchAmount 값은 기수 상승/하강 시 카메라가 상하로 움직일 좌표입니다.
역시 초기 좌표에다가 더하거나 빼서 기수 조정 시의 움직임을 표현합니다.

Vector3 adjustPosition의 y값에 - Mathf.Abs(lookValue.y) * 1.5f가 들어가있습니다.
이 값은 상술한 상부가 하부보다 넓게 보이는 문제를 약간 보정하는 용도로 사용됩니다.



AircraftController

CameraController cameraController;

void Start()
{
    ...
    cameraController = GetComponent<CameraController>();
}

void Update()
{
    MoveAircraft();
    PassCameraControl();
}

void PassCameraControl()
{
    float zoomValue = accelValue - brakeValue;
    cameraController.AdjustCameraValue(zoomValue, rollValue, pitchValue);
}

AircraftController에서는 CameraController에 현재 가속/감속값과 회전값을 넘겨줍니다.
이 때 가속과 감속값은 더해서 -1 ~ 1 사이의 값이 나오도록 가공한 후에 넘겨주겠습니다.

그 전에 GetComponent<CameraController>()로 받아오는 거 잊지 말고요.

마지막으로 CameraController에 추가된 값을 Inspector View에서 조정합니다.

적당히 구현된 모습입니다.

무작정 Lerp만 먹여서 약간 부자연스럽긴 하지만,
어떻게 움직이고 있는지는 대충 알 수는 있게 되었습니다.



이제 기본적인 움직임 구현은 끝났습니다. 디테일하게 갈고 닦는 게 많이 필요하겠지만요.

잠시 숨 좀 돌릴까요?



할 일이 산더미입니다.
속으로는 9월까지 잡고 있는데 그 안에는 되겠죠...?

아무튼 저기 써있는 대로 다음에는 무기를 다뤄보겠습니다.



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

0개의 댓글