에이스 컴뱃 시리즈에서는 3가지 시점을 지원합니다.
각 시점에서는 오른쪽 스틱을 조종해서 주변을 바라볼 수 있고,
시점 전환은 오른쪽 스틱을 눌러서 (듀얼쇼크 : R3, XBOX 컨트롤러 : RS) 할 수 있습니다.
일반적으로는 시야 확보를 위해 콕핏 없는 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인칭입니다.
값 보정 없이 그대로 회전시키면 되니까요.
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 값에 따라서 화면이 잘 회전하고 있습니다. 약간의 딜레이도 포함해서요.
(좌, 우, 상, 하 순서대로 스틱을 움직이고 있습니다.)
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
를 조정합니다.
이제 날개가 보이네요.
이번에는 비행기를 중심으로 도는 카메라를 만들어야 합니다.
(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
두 개의 값을 가져와서, 카메라의 이동에 쓰일 zoomValue
와 rollValue
를 계산합니다.
이번에도 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
를 추가했습니다.
rollValue
는 AdjustCameraValue(...)
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