전체 페이지뷰

2017년 9월 19일 화요일

Unity Tutorial: Tanks, part 6

Game Managers


선수와 무대가 준비되었으니 이제 심판 차례입니다. 실제 게임을 컨트롤할 매니저를 만들어야겠죠.

탱크들이 스폰될 위치를 지정하는 것으로 이 작업을 시작하려 합니다.

Hierarchy에서 Create Empty를 선택하여 빈 오브젝트를 만들고 이름을 SpawnPoint1으로 정합니다. 그리고 그것을 Duplicate하고 이름을 SpawnPoint2라고 합니다.

그리고 SpawnPoint1의 Position을 (-3, 0, 30) Rotation을 (0, 180, 0), SpawnPoint2는 (13, 0, -5), (0, 0, 0)으로 합니다.

위치는 정해졌지만 이대로는 보기가 너무 어려습니다. SpawnPoint1을 선택하고 인스펙터의 입방체 모양을 클릭하면 색과 아이콘을 선택할 수가 있습니다. 파란색을 선택하면


아래와 같이 이름과 아이콘이 달리므로 알아보기가 편해집니다.

SpawnPoint2에도 같은 방식으로 빨간색 아이콘을 달아줍니다.


이제 화면에 무언가를 표시하기 위해서 Hierarchy에서 Create>UI>Canvas를 선택하여 스크린 공간의 캔버스를 추가하고 이름을 MessageCanvas라고 정합니다.


이 캔버스에 각종 메세지 텍스트를 출력할 것입니다. 편집의 편의성을 위해 화면 좌상단 부근의 2D 버튼을 눌러 화면을 2차원으로 고정시킵니다.

마우스 휠을 돌려 크기를 화면에 맞게 적당히 조절하고, MessageCanvas에 우클릭, UI>Text를 선택하여 텍스트를 추가합니다.

이 Text를 선택한 채로 Anchors를 Min (0.1, 0.1), Max(0.9, 0.9)로 바꾼다음, Rect transform의 Left, Top, Right, Bottom, Pos Z는 0으로 바꿉니다.


크기를 조절해 주었으면 인스펙터에서 텍스트 내용을 "TANKS!"로 해주고 폰트를 BowlbyOne-Regular로, 정렬은 센터, 미들로 합니다. Best fir에 체크하고, Max Size는 60, 색은 흰색으로 바꾸겠습니다.


글씨에 그림자 효과를 주기 위해 Add Component를 누르고 Shadow를 타이핑하여 찾아서 추가해줍니다. 그리고 사막 색에 어울리도록 그림자 RGB를 (114, 71, 40)로 바꾸고, Effect Distance를 (-3, -3)으로 합니다.


여기까지 했으면 이제 2D 모드를 해제하고 다시 배경으로 돌아옵니다.

Hierarchy 에서 CameraRig를 선택해 봅시다. 탱크들을 쫓아다니는 카메라에 관한 것이었는데, 우리가 Hierarchy에서 탱크를 제거했으므로 대상이 사라졌습니다. 일단 Targets 사이즈를 0으로 바꾸고 CameraControl 스크립트를 엽시다.


전에 주석 처리해 두었던
 /*[HideInInspector]*/ public Transform[] m_Targets;
cs
의 주석을 지워서
 [HideInInspector] public Transform[] m_Targets;
cs
로 만들고 스크립트를 저장합니다.

유니티로 돌아오면  Targets 프로퍼티가 사라져 있습니다. 이제 씬을 저장하고 나면 진짜 매니저를 만들 대가 되었습니다.

Hierarchy에서 Create>Create Empty하고 그 이름은 GameManager로 정합니다. 거기에 Scripts>Managers>GameManager 스크립트를 드래그하여 연결합니다.

일단 몇몇 프로퍼티들을 먼저 연결하겠습니다.

총 5라운드로 이루어지는 게임이며, 위 그림처럼 Camera control, Message Text, Tank를 연결하고, 아래 Tanks를 펼쳐 봅시다.

두 대의 탱크로 이루어지는 게임이므로 Size를 2로 적어주면 Element 0, 1이 생겨나서 각각 색과 스폰포인트를 지정할 수 있게 됩니다.

첫 탱크의 색 RGB를 (42, 100, 178), Spawn Point는 SpawnPoint1으로 하고, 두번째는 (229, 46, 40), SpawnPoint2로 지정합니다.



이제 우리의 게임 매니저에 관해 얘기해 봅시다.




우리의 게임 매니저가 하는 일들입니다. Intialization 단계로 탱크들을 소환하고 카메라 타겟을 설정한 후, 게임상태 관리 단계로 넘어가서 각 라운드 시작, 플레이, 끝의 상태를 관리합니다. 그리고 이것들은 또 다른 독립적인 스크립트인 TankManager로 연결되는데, 이 TankManager는 탱크의 발사, 이동 스크립트와 UI 같은 시각적 요소들을 관리합니다.


이 전체를 간략히 도식화하면 아래와 같습니다.



Game Manager를 이해하려면 우선 Tank Manager 스크립트를 이해해야 합니다.

TankManager 스크립트는 다른 클래스들과 달리 MonoBehaviour를 상속하고 있지 않습니다. 따라서 인스펙터에서 보이도록 하기 위해 [Serializable]이라는 어트리뷰트를 선언해 두었습니다.

public 변수들부터 보겠습니다.

public Color m_PlayerColor;  // 탱크 색          
public Transform m_SpawnPoint;    // 스폰되는 위치
[HideInInspector] public int m_PlayerNumber;   // 이 탱크 매니저가 어느 플레이어 용인지 구분          
[HideInInspector] public string m_ColoredPlayerText; // 탱크색과 일치하는 텍스트
[HideInInspector] public GameObject m_Instance;   // 생성된 탱크 인스턴스에 대한 레퍼런스       
[HideInInspector] public int m_Wins;     // 이 플레이어가 몇판 이겼는지 저장     
cs


프라이빗 변수들은 아래와 같습니다.
private TankMovement m_Movement;  // TankMovement 스크립트에 대한 레퍼런스     
private TankShooting m_Shooting;  // TankShooting 스크립트 레퍼런스
private GameObject m_CanvasGameObject; // 라운드마다 world space UI를 끄는데 사용     
cs

다음 SetUp() 메소드로 이어지는데 GameManger에서 호출할수 있도록 public입니다.

public void Setup()
{
    // 컴포넌트들에 대한 레퍼런스
    m_Movement = m_Instance.GetComponent<TankMovement>();
    m_Shooting = m_Instance.GetComponent<TankShooting>();
    m_CanvasGameObject = m_Instance.GetComponentInChildren<Canvas>().gameObject;
    // 스크립트에 이 플레이어의 번호를 알려 줌
    m_Movement.m_PlayerNumber = m_PlayerNumber;
    m_Shooting.m_PlayerNumber = m_PlayerNumber;
    // 플레이어의 색과 일치하는 문자열을 생성
    m_ColoredPlayerText = "<color=#" + ColorUtility.ToHtmlStringRGB(m_PlayerColor) 
        + ">PLAYER " + m_PlayerNumber + "</color>";
    // 탱크를 구성하는 모든 렌더러들을 얻어 옴
    MeshRenderer[] renderers = m_Instance.GetComponentsInChildren<MeshRenderer>();
    // 모든 렌더러의 색을 플레이어의 색으로 바꿈
    for (int i = 0; i < renderers.Length; i++)
    {
        renderers[i].material.color = m_PlayerColor;
    }
}
cs


다음은 라운드 사이에 인스턴스의 조작을 불가능하게 하고, 가능하게 하기 위한 두 개의 public 메소드입니다.
public void DisableControl()
{
    m_Movement.enabled = false;
    m_Shooting.enabled = false;
    m_CanvasGameObject.SetActive(false);
}
public void EnableControl()
{
    m_Movement.enabled = true;
    m_Shooting.enabled = true;
    m_CanvasGameObject.SetActive(true);
}
cs

탱크의 스크립트와 UI를 모두 멈췄다 작동했다 할 수 있습니다.
public void Reset()
{
    m_Instance.transform.position = m_SpawnPoint.position;
    m_Instance.transform.rotation = m_SpawnPoint.rotation;
    m_Instance.SetActive(false);
    m_Instance.SetActive(true);
}
cs
Reset()은 위의 두 메소드와는 달리 위치도 스폰포인트로 옮기고 인스턴스 자체를 껐다가 켜서 새로 시작할 수 있게 합니다.

이제 게임매니저를 보겠습니다. 게임 매니저를 열면 네 군데에 주석 처리가 된 곳이 있습니다. 내용이 동영상에 있는 것과 좀 다른데, 예전에 만들어진 동영상이라 지금은 obsolete가 된 기술을 사용하기 때문인 듯 합니다. 코드는 변경 사항을 반영해 두었지만 동영상은 바꿀 수가 없으니까요. 일단 주석을 전부 제거합니다.

여느 때와 마찬가지로 public, private 변수 선언으로 시작합니다.

public int m_NumRoundsToWin = 5;    // 게임의 전체 판 수    
public float m_StartDelay = 3f;    // RoudStarting과 RoundPlaying 사이의 대기시간     
public float m_EndDelay = 3f;    // RoundPlaying과 RoundEnding 사이의 대기 시간       
public CameraControl m_CameraControl;   // CameraControl 스크립트의 레퍼런스
public Text m_MessageText;    // 승리 메시지 등을 내 보낼 텍스트 레퍼런스          
public GameObject m_TankPrefab;    // 탱크 프리팹 레퍼런스     
public TankManager[] m_Tanks;    // 탱크들을 컨트롤할 TankManager들에 대한 레퍼런스       
private int m_RoundNumber;     // 현재 몇 판 째인가         
private WaitForSeconds m_StartWait;    // 라운드가 시작되는 동안 사용되는 딜레이 
private WaitForSeconds m_EndWait;       // 라운드가 끝나는 동안 사용될 딜레이
private TankManager m_RoundWinner;  // 현재의 판에 누가 이겼는가에 대한 레퍼런스
private TankManager m_GameWinner;   // 게임 전체를 누가 이겼는가    
cs

그리고 Start() 메소드에서 시작 딜레이, 엔딩 딜레이를 지정하고, 탱크 스폰에 쓰일 SpawnAllTanks(), 카메라 세팅에 쓰일 SetCameraTargets()를 호출하고 코루틴을 시작합니다.
private void Start()
{
    m_StartWait = new WaitForSeconds(m_StartDelay);
    m_EndWait = new WaitForSeconds(m_EndDelay);
 
    SpawnAllTanks();
    SetCameraTargets();
 
    StartCoroutine(GameLoop());
}
cs

SpawnAllTanks()는 말 그대로 모든 탱크를 지정된 위치에 스폰하고 플레이어 번호를 지정하며, TankManger가 SetUp()을 실행하게 합니다.
private void SpawnAllTanks()
{
    for (int i = 0; i < m_Tanks.Length; i++)
    {
        m_Tanks[i].m_Instance =
            Instantiate(m_TankPrefab, m_Tanks[i].m_SpawnPoint.position,
            m_Tanks[i].m_SpawnPoint.rotation) as GameObject;
        m_Tanks[i].m_PlayerNumber = i + 1;
        m_Tanks[i].Setup();
    }
}
cs

SetCameraTargets()에서는 스폰된 탱크들의 위치를 얻어와서 CameraControl 스크립트에 넘겨줍니다.
private void SetCameraTargets()
{
    Transform[] targets = new Transform[m_Tanks.Length];
 
    for (int i = 0; i < targets.Length; i++)
    {
        targets[i] = m_Tanks[i].m_Instance.transform;
    }
 
    m_CameraControl.m_Targets = targets;
}
cs


아까 코루틴이라는 것을 언급했습니다. 간략하게 코루틴이 어떠한 것인가를 살펴보고 가려 합니다.

보통 일반적인 함수 내에서, 명령은 그냥 순차적으로 아래 그림과 같이 죽 진행됩니다.


그러나 코루틴의 과정은 좀 다릅니다.


일단 리턴 타입이 IEnumerator로 다르군요. 그리고 중간에 yield라는 명령어가 있습니다. 이 함수 내에서 순차적으로 실행되던 커맨드는 yield를 만나는 순간 멈추게 되고, 함수를 벗어나 어떤 특정 조건이 만족될 때까지 또는 정해진 시간 동안 기다렸다가 다시 돌아와 나머지 커맨드를 수행합니다.

while 루프가 있는 코루틴에 우리의 탱크 게임을 대입해 생각해 보면,


탱크가 1대 남을 때까지 루프가 도는 조건이라면 프레임마다 몇대 남았는지 체크하고 yield를 통해 다른 작업을 수행하고, 또 조건을 체크하여 1대 남을 때까지(나머지 탱크가 모두 파괴될 때까지)작업을 수행할 수 있습니다.

Start()함수 내에서 StartCoroutine(GameLoop());으로 코루틴을 시작했습니다.

private IEnumerator GameLoop()
{
    yield return StartCoroutine(RoundStarting());
    yield return StartCoroutine(RoundPlaying());
    yield return StartCoroutine(RoundEnding());
 
    if (m_GameWinner != null)
    {
        SceneManager.LoadScene(0);
    }
    else
    {
        StartCoroutine(GameLoop());
    }
}
cs

GameLoop()는 yield를 만나 RoundStarting()을 실행하고, 끝나면 돌아와 다시 RoundPlaying()을 실행하고 조건이 만족되면 다시 돌아와 RoundEnding()을 시작합니다. 그 모든 과정이 끝나고, 승자가 정해졌으면 게임을 재로딩하고, 안 정해졌다면 루프를 계속 돌립니다.

RoundStrating()은 탱크를 리셋하고, 컨트롤을 중지해 두며, 카메라를 재설정합니다. 그리고 몇판때인지를 계산해서 텍스트로 표시하고 정한 시간 후 끝납니다.
private IEnumerator RoundStarting()
{
    ResetAllTanks();
    DisableTankControl();
 
    m_CameraControl.SetStartPositionAndSize();
 
    m_RoundNumber++;
    m_MessageText.text = "ROUND " + m_RoundNumber;
    yield return m_StartWait;
}
cs


RoundPlaying()에서는 탱크 컨트롤을 가능하게 하고 메시지를 비워주고 한 대의 탱크가 남을 때까지 while문을 돌립니다.
private IEnumerator RoundPlaying()
{
    EnableTankControl();
    m_MessageText.text = string.Empty;
 
    while (!OneTankLeft())
    {
        yield return null;
    }
}
cs

RoundEnding()에서는 다시 탱크 컨트롤을 멈추고, 라운드의 승자를 정해 승수를 추가합니다. 그리고 게임 전체의 승자가 있는지도 판정하는 역할을 합니다.
private IEnumerator RoundEnding()
{
    DisableTankControl();
 
    m_RoundWinner = null;
    m_RoundWinner = GetRoundWinner();
 
    if (m_RoundWinner != null)
        m_RoundWinner.m_Wins++;
 
    m_GameWinner = GetGameWinner();
    string message = EndMessage();
    m_MessageText.text = message;
 
    yield return m_EndWait;
}
cs


OneTankLeft(), GetRoundWinner(), GetGameWinner() 등은 그다지 어렵지 않으므로 넘어가도록 하겠습니다.

씬을 저장하고 태스트 해봅시다.

댓글 없음:

댓글 쓰기