카테고리 없음

유니티 레벨 디자인을 위한 장치 - 1. 배터리 디자인

akama 2024. 10. 2. 21:19

게임을 시작하면 처음에는 이것저것 기능을 익히면서 상대하기 쉬운 몬스터나 단순한 퀘스트를 수행하지만 플레이 중반부로 가면서 체력이나 공격력이 높은 몬스터를 만나거나 던전을 가야하는 퀘스트 등 난이도가 조금씩 상승하도록 레벨이 구성되어 있습니다. 액션이나 RPG 장르가 아닌 캐주얼 게임에서도 스테이지의 난이도를 주기 위해 여러가지 요소를 만들어 넣습니다. Home Scape나 Garden Scape와 같은 퍼즐 게임에서도 난이도를 주기 위해서 수행해야 하는 목표물의 개수를 늘리거나 종류를 늘리기도 하며 목표 달성을 방해하는 장치나 또는 도움을 주는 장치를 디자인 합니다. 다양할 수록 레벨 디자인에 많은 도움이 되지만 처음 게임 개발부터 너무 많은 레벨 디자인은 개발 비용 및 시간이 크게 들기 때문에 기본적인 몇 가지를 생각하고 차츰 몇 개씩 기획해서 게임에 적용하는 것이 좋습니다. 현재 게임 예시는 단순히 목표 오브젝트(쓰레기)를 회수하면 스테이지를 클리어하는 것이었습니다. 스테이지1은 게임을 처음 알아보는 방식이라고 해서 이렇게 할 수는 있으나 스테이지2부터는 조금 다른 부분들이 순차적으로 추가되어야 합니다. 게임 플레이를 아주 편하게 할 수 없도록 약간의 긴장을 주기 위한 장치를 생각하고 만들어 보겠습니다.

 

배터리 기능 추가하기

이 게임의 주인공은 AI 즉 인공지능을 지닌 로봇 청소기입니다. 플레이어는 똑똑한 청소기가 되어 게임월드 지역 구석구석에 쓰레기로 더럽혀진 곳을 청소해서 깨끗하게 만드는 임무를 수행합니다. 너무 쉬우면 안 되기 때문에 로봇이라는 기계 특성을 살려서 배터리 장치를 만들고자 합니다. 배터리가 모두 소모되어서 0이 되도록 임무를 수행하지 못 하면 게임 오버가 되도록 하는 것입니다. 배터리 장치가 존재한다면 당연히 플레이어에게 현재 로봇 청소기의 배터리가 얼마가 남았는지를 UI로 보여주어야만 합니다. 그래서 세팅이 필요한 준비는 배터리 게이지 바 UI를 만들고, 이를 수행시킬 스크립트를 만드는 것입니다. 그 전에 게이지바를 제어하는 스크립트의 경우는 해당 스크립트를 어느 오브젝트에서 관리할까부터 정해야 합니다. 유니티에서 이런 모든 처리는 어느 오브젝트에서 수행할까부터 설계를 해야 합니다. 어느 오브젝트에서 수행을 하든 방법이 다를 뿐 어떻게든 결과적으로 작동은 되지만 다른 기능과 연계 될 때 비효율적 또는 엉뚱한 오브젝트에서 실행되는 부분은 나중에 해당 스크립트의 대대적인 수정 작업을 요구할 수도 있습니다. 여기서는 게이지바 UI는 플레이어 캐릭터 오브젝트가 아닌 GamaManager 오브젝트에서 관리하는 것으로 정했습니다. 게임에서 캐릭터의 데미지나 예시 게임처럼 배터리의 소모 같은 처리는 주로 캐릭터 오브젝트에서 처리를 하고, 관련 기능의 UI 표현 처리는 일반적으로 GameManager 또는 UI GameObject를 처리하는 스크립트를 만들어서 처리합니다. 따라서 GameManager 게이지 UI에 변화를 주는 함수가 필요합니다. PlayerMovement 스크립트를 열어서 다음 명령어를 추가해 줍니다.

GameController gameManager;
float currentPower;
public float maxPower = 100f;
public float usingPower = 1f;
bool isUsingPower;

void Start ()
{
    currentPower = maxPower;
    gameManager = GameObject.Find("GameManager").GetComponent();
    isUsingPower = true;
}

void Update()
{
    UsingPower ();
}

void UsingPower ()
    {
        if(isUsingPower)
        {
            if (currentPower > 0)
            {
                currentPower -= usingPower * Time.deltaTime;
                gameManager.UpdateBattery(currentPower);
            }
            if (currentPower <= 0)
            {
                currentPower = 0f;
                moveSpeed = 0f;
                gameManager.UpdateBattery(currentPower);
                StartCoroutine(LateDestroyed());
                isUsingPower = false;
            }
        }
    }

IEnumerator LateDestroyed ()
{
      gameManager.GameOver();
      yield return new WaitForSeconds(1.0f);
      gameObject.SetActive(false);
}

게임이 시작되면 배터리 최대치 값을 통해 현재 값이 설정됩니다. Update 함수에서 currentPower가 0보다 크면 usingPower * Time.deltaTime 명령을 통해서 사용량을 깎고 게임 매니저에 있는 UpdateBattery 함수에 깎인 결과값 float을 반영해서 호출합니다. currentPower가 0보다 작거나 같으면 0을 반영하면서 isUsingPower를 통해서 해당 명령어의 실행을 못하도록 해주고 코루틴 함수를 호출해서 게임 매니저에서 게임오버가 되도록 전달하면서 오브젝트는 비활성화 되도록 합니다. 이제 값을 전달하는 스크립트를 만들었다면 이 값을 전달 받아서 UI 게이지로 반영하는 스크립트를 만들 차례입니다. 게이지바의 제어는 여러 가지 방법이 있는데 각 사용법을 알아보겠습니다.

 

UI Slider를 이용하는 방법

유니티에는 Slider라는 UI가 있습니다. 이는 게임 메뉴창의 스크롤에 사용되지만 게이지 바를 만들 때도 사용합니다. Hierarchy뷰에서 Canvas를 클릭한 상태로 마우스 우클릭 UI - Slider를 생성하고 이름을 BatteryGaugage라고 정해줍니다. 해당 슬라이더를 클릭하면 하위 오브젝트들이 있는데 이 중 Handle Slide Area와 Handle 오브젝트는 필요가 없으므로 삭제합니다. 남은 Fill Area 오브젝트와 Fill 오브젝트의 색상을 지정합니다. 끝으로 해당 오브젝트를 게임 화면에 위치를 정해주는데 대개 왼쪽 상단에 배치하지만 이 부분은 정해진 것이 없으니 괜찮은 위치에 배치시킵니다.

public Slider batteryGauage;
public float currentBattery = 100f;
float maxBattery;

void Start ()
{
    maxBattery = currentBattery;
    batteryGauage.maxValue =  maxBattery;
    batteryGauage.value =  currentBattery;
}

public void UpdateBattery (float battery)
{
     currentBattery = battery;
     batteryGauage.value =  currentBattery;
}

Slider public 변수를 만들고, 해당 변수에 Canvas에서 생성한 BatteryGaugage Slider 오브젝트를 드래그 앤 드롭합니다. 게임이 시작되면 currentBattery 초기 세팅값인 100f가 maxBattery 값이 되고 이 값들을 모두 Slider의 maxValue와 value에 할당해 줍니다. 배터리 값을 바꾸어 주는 업데이트 함수를 만들어서 변경되는 currentBattery를 Slider 게이지바에 적용시킵니다. 그런데 이 방법은 게이지의 변화가 부드럽지 않습니다. 감소되는 게이지를 부드럽게 표현하는 방법을 UI Image를 통해서 알아보겠습니다.

 

게이지바의 수치 변경을 update를 통해 부드럽게 반영되는 방법 소개

 

UI Image를 이용하는 방법

UI Image와 Update 함수를 이용해서 부드러운 게이지 수치 반영을 구현할 수 있습니다. 먼저 UI Image를 이용한 게이지바를 만들기 위해서 Canvas를 클릭한 상태에서 마우스 우클릭을 합니다. Empty object를 만들어주고 이름을 BatteryGaugage라고 정해줍니다. 이번에는 BatteryGaugage 클릭한 상태로 우클릭을 하고 UI - image를 클릭해서 이름을 Bar라고 지정합니다. 다시 BatteryGaugage 클릭한 상태로 우클릭을 하고 image를 만들어서 BarBack으로 이름을 지정해 줍니다. 순서는 Bar 오브젝트가 하단에 가도록 위치시켜 줍니다. 그리고 게임 화면에서 배터리 게이지바 UI 위치를 정해 줍니다.

Battery Gauage를 위한 세팅

Canvas에서 UI 세팅이 끝나면 기존 GameController(GameManager) 스크립트를 열어서 아래 명령어들을 추가해 줍니다.

public Image batteryGauage;
public float lerpSpeed;
public float currentBattery = 100f;
float maxBattery;

void Start ()
{
    maxBattery = currentBattery;
}

void Update ()
{
    UpdateBattery (currentBattery);
}

public void UpdateBattery (float battery)
{
     currentBattery = battery;
     batteryGauage.fillAmount = Mathf.Lerp(batteryGauage.fillAmount, currentBattery / maxBattery, lerpSpeed);

     Color barColor = Color.Lerp(Color.red, Color.green, (currentBattery / maxBattery));
     batteryGauage.color = barColor;
}

게이지바 이미지를 담을 변수를 public으로 선언해 줍니다. public으로 선언 되었기 때문에 Inspector에서 Canvas에서 만든 Bar Image를 할당해 줍니다. lerpSpeed는 게이지가 줄어드는 속도입니다. 현재 남은 배터리 용량 값을 currentBattery에 담고 초기값을 정해 줍니다. 그리고 최대 배터리 용량은 첫 시작 시 currentBattery 값을 정해줍니다. 배터리값을 받는 함수를 한 개 만들어 줍니다. batter 인자를 받아서 currentBattery 값으로 지정하도록 합니다. 이 값을 최대 배터리 값으로 나눈 값을 Mathf.Lerp 값으로 게이지의 fillAmount를 변하도록 합니다.

흔히 우리가 사용하는 모바일 핸드폰의 배터리 이미지를 보면 충전이 많이 되어 있을 때는 녹색이지만 배터리가 많이 소모된 상태면 붉은색으로 표시되는 것을 알 수 있습니다. 게임에서도 이 부분을 적용시켜 보겠습니다. 방법은 Color.Lerp 를 사용하는 단 한 줄의 스크립트로 가능합니다. (currentBattery / maxBattery)의 프레임 마다 변경되는 값에 따라서 green에서 red로 이미지의 색이 변경되며 이를 게이지의 색에 적용시킵니다. 플레이를 해보면 Slider 방식의 스크립트와 달리 감소되는 부분이 Mathf.Lerp를 통해서 부드럽게 표현됩니다. 물론 Slider 방식도 LateUpdate를 통해서 부드럽게 처리할 수 있으나 개인적으로는 이미지를 활용하는 것이 좀 더 깔끔한 것 같습니다.

 

여기서 이 예시 게임과는 상관 없지만 UI Gauage Bar에 대해서 한 가지 더 알아보겠습니다. 대전 게임을 하거나 액션 게임을 하다보면 데미지를 주거나 피격 당했을 때 체력 게이지를 깎을 때 바로 체력이 깎이지 않고 데미지 받은 수치의 값을 게이지에서 잠깐 보여주고 체력을 깎는 것을 보셨을 것입니다.

(상단) 피해 및 피격 값을 표현해주는 단계 없음 (하단) 피해 및 피격 값을 표현해주는 단계 존재

최종 값을 바로 게이지에 반영하지 않고 피해나 피격 부분을 표현해 주는 단계를 한 개 더 만드는 것입니다. 방법은 한 개 더 추가해 주고 이를 코루틴으로 처리하는 것으로 매우 간단합니다. 우선 Canvas에 이를 표현할 Bar Image가 한 개 더 필요합니다. UI - image를 생성하고 Bar2라고 해주고 HPbar스크립트에서 DamagedBar에 지정해 줍니다.

public class HPbar : MonoBehaviour
{
    public Image HpBar;
    public Image DamagedBar;
    public float lerpSpeed;
    public float currentHP = 500f;
    float maxHP;
    private float damaged;

    void Start ()
   {
         maxHP = currentHP;
   }

    void Update ()
   {
         UpdateHP (damaged);
   }

    public void UpdateHP (float damage)
    {
         float momentHP;
         currentHP -= damage;
         momentHP  = currentHP;
         HpBar.fillAmount = Mathf.Lerp(HpBar.fillAmount, currentHP / maxHP, lerpSpeed);
         StartCoroutine(UpdadeDamaged(momentHP));
    }

    IEnumerator UpdadeDamaged (float moment)
    {
        yield return new WaitForSeconds(0.5f);
        DamagedBar.fillAmount = Mathf.Lerp(DamagedBar.fillAmount, moment / maxHP, lerpSpeed);
    }
}

스크립트를 보면 예시 게임에서 했던 스크립트와 유사하지만 몇 가지 추가된 부분이 있습니다. DamagedBar가 Image 변수로 있으며 UpdateHP 함수를 통해서 데미지 값을 받습니다. currentHP에 damage 값을 깎아서 반영하고 이를 momentHP에 적용시킵니다. momentHP는 코루틴에서 DamagedBar.fillAmount를 변경시킬 때 사용합니다. 코루틴 함수 내에서 currentHP를 사용하지 않는 이유는 0.5초의 딜레이 시간에 재차 피격이 들어올 경우 currentHP의 값은 갱신되는데 이 값을 사용하면 재차 피격된 부분으로 반영되기 때문에 momentHP를 잠깐 이용해서 전달합니다. 이렇게 하면 코루틴을 통해서 피해 또는 피격 받는 부분이 딜레이 시간 만큼 보여졌다가 Mathf.Lerp(DamagedBar.fillAmount, moment / maxHP, lerpSpeed); 명령을 통해서 DamagedBar도 줄여주게 됩니다.

 

예시 게임으로 돌아와서 게임을 실행하면 시작과 동시에 배터리의 색은 녹색으로 시작하지만 시간이 흐를수록 감소하면서 점차 노란색에서 빨간색으로 바뀌게 되고 최종적으로 0이 되면 게임 오버 화면이 발생하게 됩니다. 이제 시간의 제약이라는 난이도 요소가 한 개 생겼습니다. 다음 시간에는 목표물 수거 용량을 제한해서 이를 폐기하는 곳에 가도록 만드는 장치를 만들어 보겠습니다.