2014. 10. 1.

Unity 2D Game Development - Review

유니티 2D가 나온지도 어느덧 1년이 다되가고, 가볍게 볼 수 있는 책을 찾아보다 이 책을 읽게 되었습니다.
챕터 목록은 다음과 같습니다.
  • Chapter 1 “Introduction to the 2D World of Unity”
  • Chapter 2 “It Lives!”
  • Chapter 3 “No Longer Alone”
  • Chapter 4 “Give It Some Sugar”
  • Chapter 5 “The Ultimate Battle of Ultimate Destiny”
  • Chapter 6 “The Finishing Touches”

추천 독자: 프로그래밍 기초 지식을 보유한, 유니티 2D를 배우고자 하는 사람

100페이지 정도의 얇은 책이며, 책 표지에 나온 그림이 이 책에서 만드는 게임의 완성본입니다. 누구나 볼 수 있는 수준의 기초 책이지만, C# 문법이나 기초적인 프로그래밍 지식이 있다고 가정하고 설명하기에, C#에 대한 지식이 충분해야합니다. 예를 들어, C# 문법인 delegate와 event를 선언하고, 왜 이것을 사용하는지에 대해서는 설명하지만 delegate와 event의 구체적인 문법에 대해서는 설명하지 않습니다.

대다수의 한국책처럼 코드 따라하기식이 아닌 "왜 이렇게 구현하였는가"에 대한 설명이 자세하게 서술되어 있으며, 심지어 독자들에게 안좋은 방법으로 구현하게 만들고, 몇 페이지 뒤에 가서 "이전에 썻던 그 방법엔 이런 단점이 있다는걸 눈치챘니?" 하면서, 다 뜯어 고치고 새로운 방법에 대한 장점을 알려주기도 합니다.

반면, 저자가 코드만 짜고, 테스트는 한 번도 안해본 것 같은 생각이 들 정도로 예제 코드에 버그가 많다. 어느 정도냐하면, 예제 코드 완성본을 유니티에서 실행시키면 캐릭터 점프부터 작동하지 않는다. 점프 키를 누르면 그냥 캐릭터가 그 자리에 멈추어 버려서 아무것도 할 수없다. 이런식으로 버그가 무수히 많으니 디버깅 능력이 전혀 없다면, 책을 다 읽고나서도 제대로 된 게임이 나오지 않을지도 모릅니다.

장점:
  • 100페이지 정도의 짧은 분량으로 적은 시간내에 유니티 2D를 배울 수 있음
  • 새로 생긴 유니티 2D의 핵심적인 시스템을 전부 포함
  • 코드 따라치기가 아닌 "왜 이렇게 구현하는가?"에 대한 설명이 풍부함
단점:
  • 전문적인 고급 기술들에 대한 내용이 부족.
  • 버그가 너무 많아서 대부분의 예제 코드가 제대로 작동 안함.

유니티에서 Death Trigger 를 구현해보자

Death Trigger란 플레이어가 언제 죽는지 기점을 잡는 역할을 합니다. 2D 러닝 게임을 예로 들자면, 낭떠러지로 떨어지거나 장애물/적 등과 충돌했을때 Death Trigger가 발동 되어야합니다. 이 경우, Death Trigger의 구현 방법은 크게 두 가지로 나뉘어집니다.

첫 번째는 낭떠러지, 장애물, 적 등 충돌시 플레이어를 죽게 만드는 오브젝트에 Death Trigger를 구현하는 방법입니다. 위 그림과 같이 바닥에 DeathTrigger란 이름의 게임 오브젝트를 생성하고, Box 2d collider를 붙여 길게 늘어뜨린 후, Is Trigger를 체크해줍니다. 그리고, DeathTriggerScript를 만들어 DeathTrigger 오브젝트에 붙여줍니다.
using UnityEngine;
using System.Collections;

public class DeathTriggerScript : MonoBehaviour
{
  void OnTriggerEnter2D (Collider2D collidedObject)
  {
    collidedObject.SendMessage ("hitDeathTrigger");
  }
}
이렇게 구현하면, 이제 DeathTrigger 오브젝트의 붙인 Box 2d Collider에 무언가 충돌하는 순간 SendMessage 함수를 통해 충돌한 오브젝트에 hitDeathTrigger 함수가 호출됩니다(잘 이해가 안가시면 SendMessage 함수의 API를 참조하세요). 이제 플레이어의 collider가 붙어있는 오브젝트에 적당한 스크립트 공간을 찾아서 hitDeathTrigger 함수를 구현해 주면 됩니다. 저는 hitDeathTrigger 함수에 플레이어가 죽는 상태로 바뀌도록 구현하였습니다.

두 번째 방법은 첫 번째 방법과 반대로 플레이어쪽에서 부딪치는 오브젝트를 체크하는 방식입니다. 먼저 DeathTrigger 오브젝트를 첫 번째 방법처럼 Box 2d collider를 붙여 길게 늘어뜨리고, Is Trigger를 체크합니다. 스크립트는 붙이지 않으며, 대신 Tag에 DeathTrigger라는 태그를 선언해줍니다. collider가 붙어있는 플레이어 오브젝트에 적당한 스크립트 공간을 찾아서 아래와 같이 onTriggerEnter2D 함수를 구현합니다(플레이어의 collider에도 IsTrigger를 체크해야합니다).
void OnTriggerEnter2D (Collider2D collidedObject)
{
 switch (collidedObject.tag) {
 case "DeathTrigger":
  // Player hit the death trigger - kill 'em!
  // 여기에 플레이어가 죽도록 구현!
  break;
 }
}

첫 번째 방법은 DeathTrigger 오브젝트 마다 부딪치는 수 많은 오브젝트의 경우의 수를 생각해서 구현을 해야함으로 비용이 많이 드는 작업입니다. 반면 두 번째 방법은 DeathTrigger 오브젝트들에 태그를 붙여주고, 플레이어와 적 유닛 정도에만 OnTriggerEnter2D를 구현해주면 끝나는 일이니 비용이 비교적 적게 듭니다. 이러한 이유로  Death Trigger 구현에는 일반적으로 두 번째 방법을 더 선호합니다.

2014. 9. 29.

Update() , FixedUpdate() , LateUpdate() 의 차이점

유니티에서 제공하는 Update 함수로 Update, FixedUpdate, LateUpdate 3가지가 있습니다. 어떤 상황에 어떤 함수를 호출해야 하는지 알기 위해 각 함수별 특징과 차이점을 설명합니다.
  • Update() - 스크립트가 enabled 상태일때, 매 프레임마다 호출됩니다. 일반적으로 가장 빈번하게 사용되는 함수이며, 물리 효과가 적용되지 않은 오브젝트의 움직임이나 단순한 타이머, 키 입력을 받을 때 사용됩니다.
  • FixedUpdate() - 프레임을 기반으로 호출되는 Update 와 달리 Fixed Timestep에 설정된 값에 따라 일정한 간격으로 호출됩니다. 물리 효과가 적용된(Rigidbody) 오브젝트를 조정할 때 사용됩니다(Update는 불규칙한 호출임으로 물리엔진 충돌검사 등이 제대로 안될 수 있음).
  • LateUpdate() - 모든 Update 함수가 호출된 후, 마지막으로 호출됩니다. 주로 오브젝트를 따라가게 설정한 카메라는 LateUpdate 를 사용합니다(카메라가 따라가는 오브젝트가 Update함수 안에서 움직일 경우가 있기 때문).

2014. 9. 28.

OnTriggerEnter2D and OnTriggerExit2D not working?

유니티를 쓰던 도중, 충돌 체크가 필요해서 OnTriggerEnter2D와 OnTriggerExit2D를 선언했다.
using UnityEngine;
using System.Collections;

public class EnemyGuideWatcher : MonoBehaviour
{
  public EnemyControllerScript enemyObject = null;
  public bool onCollided = false;

  void onTriggerEnter2D (Collider2D collidedObject)
  {    
    onCollided = true;
  }
 
  void onTriggerExit2D (Collider2D collidedObject)
  {
    onCollided = false;
    // Is this trigger just left a Platform object, then the enemy is about to walk off the platform.
    // Tell the enemy that they need to switch directions!
    if (collidedObject.tag == "Platform") {
      enemyObject.switchDirections (); 
    }
  }
}
워낙 기초적이고 간단한 작업이라 뚝딱 완성하고 실행을 시켰더니 왠걸? 충돌 체크가 되지 않는 것이였다. 제대로 선언했나 확인도 해보고, 로그도 찍어보고, 유니티 포럼도 뒤져가면서 정보를 찾아보았으나 원인을 알 수 없었다. 너무나 기초적인 작업이라 버그일리도 없고, 논리적 오류가 발생했을리도 없었다. 도대체 왜 안되는거야?!

3시간 후, onCollided의 값이 true로 바뀌길 바라며 여기저기 포럼 글을 헤매다, 아래와 같은 질문글을 발견했다.

The function isn't working because the letter "o" is lowercase instead of uppercase.
Change this line: void onTriggerExit2D(Collider2D other)
to: void OnTriggerExit2D(Collider2D other)
대소문자 문제였다는 간단한 사실을 3시간 동안 깨닫지 못했다니... 이런건 IDE에서 자동으로 색상을 통해 알려줘야 하는거 아냐? 모노디벨롭에 아쉬움이 느껴지는 순간이였다.
결론은 OnTrigger 함수가 제대로 작동하지 않는다면, 첫 글자를 대문자로 선언했는지 다시 한 번 확인해보자!

2014. 9. 10.

유니티 Vector3.Lerp() 란 무엇인가?

유니티의 Vector3에 있는 Lerp() 함수는 API를 찾아보면
static Vector3 Lerp(Vector3 from, Vector3 to, float t);
구조로 이루어져 있으며, 두 벡터(from, to) 사이의 시간에 따른 위치를 구할 때 사용됩니다.
위 그림에 나온 것처럼 Lerp() 함수안에 시작벡터와 목표벡터를 넣고, 마지막 인자에는 0.0~1.0 사이의 값을 넣는데 이 값이 0.0 = 0%, 1.0 = 100% 라고 생각하시면됩니다. 위 그림처럼 0.5를 넣으면 50%니 시작벡터와 목표벡터의 중간에 있는 벡터값이 나오게 되고, 0.0은 0%니 시작벡터가 1.0은 100%니 목표벡터가 나오게 됩니다. 이를 이용해서 한 템포 늦게 플레이어를 따라가는 카메라를 구현한 예제입니다.
using UnityEngine;
using System.Collections;

public class CameraController : MonoBehaviour
{
 
  public GameObject playerObject = null;
  public float cameraTrackingSpeed = 0.2f;
  private Vector3 lastTargetPosition = Vector3.zero;
  private Vector3 currentTargetPosition = Vector3.zero;
  private float currentLerpDistance = 0.0f;
 
  void Start ()
  {
    // Set the initial camera positioning to prevent any weird jerking
  
    // around
    Vector3 playerPos = playerObject.transform.position;
    Vector3 cameraPos = transform.position;
    Vector3 startTargetPos = playerPos;
  
    lastTargetPosition = startTargetPos;
    currentTargetPosition = startTargetPos;
    currentLerpDistance = 1.0f;
  }
 
  void LateUpdate ()
  {  
    trackPlayer ();
  
    // Continue moving to the current target position
    currentLerpDistance += cameraTrackingSpeed;
    transform.position = Vector3.Lerp (lastTargetPosition, currentTargetPosition, currentLerpDistance);
  }
 
  void trackPlayer ()
  {
    // Get and store the current camera position, and the current player position, in world coordinates.
    Vector3 currentCamPos = transform.position;
    Vector3 currentPlayerPos = playerObject.transform.position;
  
    if (currentCamPos.x == currentPlayerPos.x && currentCamPos.y == currentPlayerPos.y) {
      // Positions are the same - tell the camera not to move, then abort.
      currentLerpDistance = 1f;
      lastTargetPosition = currentCamPos;
      currentTargetPosition = currentCamPos;
      return;
    }
  
    // Reset the travel distance for the lerp
    currentLerpDistance = 0f;
  
    // Store the current target position so we can lerp from it
    lastTargetPosition = currentCamPos;
  
    // Store the new target position
    currentTargetPosition = currentPlayerPos;
  }
}
Lerp() 함수의 시작벡터에 현재 카메라 위치를 목표벡터에 현재 플레이어 위치를 넣고, 마지막 인자(currentLerpDistance)에는 프레임마다 카메라 추적 속도(0.2)를 누적해서 더해준 값을 넣어, 프레임마다 20%씩 카메라가 플레이어를 따라잡게 됩니다. 5프레임이 지나면 currentLerpDistance는 1.0이 되어 유저와 카메라의 위치가 같아지게 되고, currentLerpDistance의 값이 0f로 초기화 됩니다.