전체 페이지뷰

2017년 8월 19일 토요일

Unity Tutorial: Tanks, part 2

Camera Control


이 게임은 탱크 두 대로 이루어지는 대전 게임입니다. 당연히 필드를 두 대의 탱크가 돌아다니게 되며 멀어지거나 가까워질 때에도 두 대의 탱크를 모두 비추어야 합니다. 따라서 멀어지면 모두를 비추기 위해 카메라가 줌 아웃 되어야 할 테고 가까워지면 줌 인 되어야 할 것입니다.

먼저 Hierarchy에서 Create>Create Empty를 선택하여 빈 오브젝트를 만들고 이름은 CameraRig라고 정합니다. Reset하여 기준위치에 오게 해놓고 로테이션을 (40, 60, 0)으로 합니다.


그리고 이 CameraRig에 Main Camera를 드래그하여 자식으로 만들어줍니다.


그러면 이 메인카메라의 포지션이 살짝 이상해졌을 수 있는데 다시 정확하게 position을 (0, 0, -65)로, rotation을 (0, 0, 0)으로 만들어줍니다.

이 카메라를 작동시키기에 앞서 알아두어야할 개념은 Frustum이라는 것입니다.



Perspective 뷰에서 카메라는 위의 그림처럼 화면을 비추게 됩니다. Frustum은 절두체라는 것으로 카메라가 비추는 피라미드 형태에서 실제 보여지게 되는 near clip plane에서 far clip plane 까지의 공간을 말하는 것입니다. 따라서 카메라로부터의 거리에 따라 크기가 달라지게 됩니다.


반면 Orthographic 뷰에서는 위의 그림과 같이 near와 far 사이에 거리에 따른 크기의 변화를 고려하지 않습니다.



따라서 perspective뷰에서는 좌측과 같이 거리가 먼 오브젝트는 작게 나타나지만 orthographic뷰에서는 우측처럼 거리가 멀다고 해서 오브젝트가 작게 표현되지 않습니다.

Orthographic 뷰에서는 거리요소를 고민할 필요가 없이 단순히 카메라의 크기를 크게하면 frustum의 크기가 커져 더 많은 물체가 보이게 되므로 줌아웃이 되고 작게하면 더 적은 물체가 보이게 되므로 줌인이 됩니다.

또 고려해야 할 것은 aspect입니다.


1080p 화면의 경우 픽셀 사이즈로는 1920 * 1080이고 이는 aspect ratio로 나타내면 16:9입니다. aspect는 16을 9로 나눈 값으로 약 1.778입니다.

이제 기본적인 사항을 인지했으니 우리의 카메라가 수행해야할 것이 무엇인가를 알아봅니다.

1. 탱크를 따라다닐 것
2. 탱크가 화면에 모두 들어오도록 카메라 크기를 조절할 것(스크린 끝까지 오지 않도록 약간의 버퍼를 둔다)

Assets>Scripts>Camera폴더에 보면 CameraControl이라는 스크립트가 들어있습니다. 이것을 드래그하여 CameraRig에 연결하고(Main Camera가 아닙니다) 에디터로 엽니다.

이 스크립트는 이미 완성되어 있습니다. 대략적으로 내용만 살펴보고 넘어갑시다.

public float m_DampTime = 0.2f;                 
public float m_ScreenEdgeBuffer = 4f;           
public float m_MinSize = 6.5f;                  
[HideInInspector] public Transform[] m_Targets;
cs

몇 개의 public 변수가 선언되었습니다. m_DampTime은 카메라를 원하는 위치로 이동시키는데 요구되는 대략적인 시간입니다. 여기서는 0.2f로 설정되었습니다. m_ScreenEdgeBuffer는 탱크가 카메라 밖으로 나가지 않게 설정한 버퍼이며, m_MinSize는 카메라가 줌인 되었을 때 가질 수 있는 최소크기 입니다. 다음 m_Targets는 카메라가 쫓아야할 탱크들의 어레이입니다. 나중에 만들 게임 매니저가 탱크들을 스폰하게 되면 그것들에 대한 레퍼런스를 제공하게 됩니다.

[HideInInspector]가 사용되면 퍼블릭이지만 인스펙터 상에서는 보이지 않게 됩니다. 이것은 나중에 게임매니저가 직접 연결할 수 있도록 하기 위한 것인데 아직 매니저는 만들지 않았으므로 일단 대괄호 부분만 주석처리 해 두는 것이 좋겠습니다. 그래야 테스트가 가능해집니다.

/*[HideInInspector]*/ public Transform[] m_Targets; 
cs

다음으로 private 변수 몇 가지가 또 선언됩니다.

private Camera m_Camera;                        
private float m_ZoomSpeed;                      
private Vector3 m_MoveVelocity;                 
private Vector3 m_DesiredPosition; 
cs

m_Camera는 카메라에 대한 레퍼런스이고, m_ZoomSpeedm_MoveVelocity는 카메라가 나중에 등장할 메소드에서 사용하게 될 수치를 저장할 곳 입니다. m_DesiredPosition은 탱크들의 평균 위치를 나타냅니다.

우리는
1. 탱크의 평균 위치를 찾고,
2. 매 프레임마다 CameraRig를 그 위치로 옮겨주는
방식으로 카메라 위치를 정할 것입니다.

private void Awake()
{
    m_Camera = GetComponentInChildren<Camera>();
}
cs
Awake()에서 CameraRig의 자식에 있는 카메라에 대한 레퍼런스를 얻어 옵니다.

다음 FixedUpdate()에서 카메라를 이동, 줌 시킵니다. 카메라는 Physics update가 없으므로 FixedUpdate()가 필요없지만 우리의 타겟인 탱크들은 Physics update가 있고, 따라서 그것들과 동조를 이루어 움직이기 위해서 FixedUpdate()를 사용합니다.
private void FixedUpdate()
{
    Move();
    Zoom();
}
cs

Move()에서 탱크들의 평균 위치를 찾고 그리로 이동시킵니다.
private void Move()
{
    FindAveragePosition();
 
    transform.position = Vector3.SmoothDamp(transform.position, m_DesiredPosition, ref m_MoveVelocity, m_DampTime);
}
 
 
private void FindAveragePosition()
{
    Vector3 averagePos = new Vector3();
    int numTargets = 0;
 
    for (int i = 0; i < m_Targets.Length; i++)
    {
        if (!m_Targets[i].gameObject.activeSelf)
            continue;
 
        averagePos += m_Targets[i].position;
        numTargets++;
    }
 
    if (numTargets > 0)
        averagePos /= numTargets;
 
    averagePos.y = transform.position.y;
 
    m_DesiredPosition = averagePos;
}
cs
중간에 쓰인 activeSelf라는 프로퍼티는 오브젝트가 액티브한 상태일때 true입니다. 부서졌거나 어떠한 이유로든 active상태가 아닌 오브젝트를 따라다닐 필요는 없으므로 사용되었습니다. 그 외에는 그다지 이해하기 어려운 코드는 아닙니다.

평균 위치를 찾아 카메라 위치를 지정해 주는 것은 이렇게 비교적 간단합니다만 줌의 경우는 조금 더 복잡합니다.


우리는 중앙으로부터 상단까지의 세로 높이가 카메라의 size라는 것을 알고 있습니다. 그리고 앞서 나왔던  aspect를 size에 곱해주면 그것이 가로 길이가 됩니다. 따라서 탱크가 이 안에 들어오게만 하면 언제나 탱크를 볼수가 있습니다.

만약 탱크가 화면 상단에 있고 위 아래 축을 Y축이라고 한다면, Y축 상의 거리가 카메라 size가 될 것입니다.

만일 탱크가 예를 들어 우측 끝에 있고 가로방향을 X축이라고 한다면, 가로상의 거리는 size*aspect가 될 것입니다.



이 간단한 방정식을 기초로 탱크 마다 평균위치로부터 카메라 size를 정할 수가 있고, 그 중 가장 큰 것을 고르면 전체 탱크들을 모두 볼 수 있게 될 것입니다.

private void Zoom()
{
    float requiredSize = FindRequiredSize();
    m_Camera.orthographicSize = Mathf.SmoothDamp(
        m_Camera.orthographicSize, requiredSize, ref m_ZoomSpeed, m_DampTime);
}
cs

필요한 사이즈를 계산하고 그 크기로 카메라를 재설정합니다.

private float FindRequiredSize()
{
    Vector3 desiredLocalPos = transform.InverseTransformPoint(m_DesiredPosition);
 
    float size = 0f;
 
    for (int i = 0; i < m_Targets.Length; i++)
    {
        if (!m_Targets[i].gameObject.activeSelf)
            continue;
 
        Vector3 targetLocalPos = transform.InverseTransformPoint(m_Targets[i].position);
 
        Vector3 desiredPosToTarget = targetLocalPos - desiredLocalPos;
 
        size = Mathf.Max (size, Mathf.Abs (desiredPosToTarget.y));
 
        size = Mathf.Max (size, Mathf.Abs (desiredPosToTarget.x) / m_Camera.aspect);
    }
        
    size += m_ScreenEdgeBuffer;
 
    size = Mathf.Max(size, m_MinSize);
 
    return size;
}
cs

CameraRig로부터 탱크까지의 X축, Y축까지의 거리 중 큰 것을 찾는 작업을 전체 탱크를 대상으로 시행하고, 정해진 size에 버퍼를 더해줍니다. 마지막으로 카메라 크기가 최소값보다 작아지지 않도록 한번 더 비교해주면 됩니다.

마지막으로 퍼블릭 메소드가 하나 있습니다.
public void SetStartPositionAndSize()
{
    FindAveragePosition();
 
    transform.position = m_DesiredPosition;
 
    m_Camera.orthographicSize = FindRequiredSize();
}
cs

매 라운드 씬이 시작할 때 바로 제 카메라 위치를 찾아주기 위해 작성된 것입니다.

이제 스크립트를 저장하고 유니티로 돌아갑니다.

생성된 프로퍼티 중 Targets에 Tank 오브젝트를 드래그하여 연결합니다.

실행해보면 카메라가 탱크를 잘 따라다니는 것을 확인할 수 있습니다.

댓글 없음:

댓글 쓰기