소소한 나의 하루들

탑다운 2d RPG(8) - 모바일 UI & 안드로이드 빌드 본문

개발/유니티

탑다운 2d RPG(8) - 모바일 UI & 안드로이드 빌드

소소한 나의 하루 2024. 2. 5. 21:37

출처: https://youtube.com/playlist?list=PLO-mt5Iu5TeYI4dbYwWP8JqZMC9iuUIW2&feature=shared

 

📚 유니티 기초 강좌

유니티 게임 개발을 배우고 싶은 분들을 위한 기초 강좌

www.youtube.com

모바일 UI도 만들고, 실제 어플리케이션 파일로 출력해서 안드로이드 폰에서 구동시켜본다.

지금까지는 플레이어 이동, 액션 등을 전부 키보드의 물리적인 키로 조작하고 있었다. 이제는 실제 화면에 옮겨보도록 하겠다.


#1. 컨트롤 UI 구축

우선 스크린 크기에 대응하기 위해 Canvas 오브젝트에서 Canvas Scaler 컴포넌트의 타입을 Scale with Screen Size로 설정하고, 아래의 참조 해상도를 적절하게 설정한다. (해상도 설정: 1920x1080)

 

Image UI를 생성하고, 적당한 이미지 sprite를 적용한다. 그리고 해당 이미지 Sprite Editor에서 Border Line을 설정한다.

Image UI 크기를 비주얼 키(조작하는 키가 들어갈 영역) 배경이 되는 이미지 생성 후, 아래로 앵커를 설정한다.

어떤 키가 필요할까? Move키, Action키(Jump키), Cancel키(esc키)가 필요할 것 같다.

 

1) Move키

Image UI 안에 빈 오브젝트(Rect Transform을 가져야함)를 하나 생성하고 그 안에 방향키 버튼 4개를 생성한다.

그리고 방향키 버튼 4개를 갖고있는 빈 오브젝트를 통해 방향키 전체의 크기 비율을 맞춰준다. 방향키의 위치도 빈 오브젝트를 통해 해상도에 대응하기위해 앵커를 설정해가며 좌측으로 맞춰준다.

 

2) Action키 3) esc키

Image UI 안에 빈 오브젝트(Rect Transform을 가져야함)를 하나 생성하고, 그 안에 액션키 버튼 2개를 생성한다. 해상도 대응을 위해 우측으로 앵커를 설정한다. 그 후 위치를 잘 조절해주면 된다.

화면 해상도를 설정할 때, Canvas Scaler 설정을 한 뒤, Game창에서 Type을 Aspect Ratio로 설정하고 화면 비율을 설정하고 보면 이렇게 모바일 환경으로도 볼 수 있다. (현재 해상도 9:19 / x600 y800)

 

유니티 여러 해상도에 대응시키기

출처: https://solution94.tistory.com/76

 

[유니티] 여러 해상도(가변 해상도) 대응하기 (UI)

유니티를 기준으로 빌드할 수 있는 플랫폼도 다양하고 플랫폼별 실행환경에도 셀 수 없을만큼 다양한 기기들이 존재합니다. 다행히 어느정도 규격이 되는 해상도들이 정해져있긴 하지만 사용

solution94.tistory.com

Canvas 오브젝트에서 Canvas 컴포넌트의 Render Mode를 Screen Space - Camera로 설정하고, Render Camera에 사용할 카메라 오브젝트를 드래그하여 적용시켜준다. 그리고 연결시킨 카메라 오브젝트의 Projection 값을 Orthographic으로 설정한다.

(카메라가 Player 오브젝트 안에 들어있다면) Screen Space - Camera로 설정하면, 해당 캔버스 UI가 캐릭터가 움직일 때 계속 흔들리는 문제가 있다. 따라서 Screen Space - Overlay로 설정해줘야할 것 같다.

아니면 UI에만 다른 카메라를 적용시켜주거나.. 다른 방법을 찾아봐야할 것 같다.

다시 Canvas 오브젝트에서 Canvas Scalar 컴포넌트에서 UI Scale Mode를 해상도에 따라 재배치가 가능한 Scale With Screen Size로 설정해주고, Reference Resolution에서 원하는 기준 해상도를 잡아준다.

마지막으로 Screen Match Mode를 세로 화면은 0 가로 화면은 1로 설정한다.

이제 UI를 적절히 배치해주면 된다.

 

카메라를 Player 오브젝트 안에 넣어주고, 카메라 위치를 살짝 올려서 중앙으로 맞춰준다.

*마찬가지로 다른 대화창 UI, 서브메뉴 UI 크기도 Camera 사이즈에 맞춰준다. (Anchor 위치 조정)

대화창 애니메이션 높이도 조작바를 고려해서 수정해준다. (y20->y150)


#2. 방향키

모바일에서 사용될 키 UI 배치는 완료했다. 이제 하나씩 기능을 부여해본다.

버튼에 적용할 스크립트의 함수들은 다 public으로 선언한다. (에디터에서 적용시켜야하므로)

//PlayerAction
public void ButtonDown(string type)
{
    switch (type)
    {
        case "U":
            break;
        case "D":
            break;
        case "L":
            break;
        case "R":
            break;
    }
}

public void ButtonUp(string type)
{
    switch (type)
    {
        case "U":
            break;
        case "D":
            break;
        case "L":
            break;
        case "R":
            break;
    }
}

방향키 4방향을 처리하기위해 매개변수를 활용한 Switch - case문을 사용한다. 미리 이렇게 틀을 만들어놓고, 에디터에서 버튼에 연결시켜놓는다.

OnClikc()은 버튼 Down과 버튼 Up이 한 세트이다. 

 

버튼 UI에 ButtonDown ButtonUP 이벤트 따로 주기

EventTrigger 컴포넌트 : UI 이벤트를 관리하는 컴포넌트

버튼 UI 오브젝트에서 EventTrigger 컴포넌트를 추가한다. 그리고 Add New Event Type을 클릭하면 버튼 UI에서 일어날 수 있는 모든 Type들을 볼 수 있다. 여기서 PointerDown과 PointerUp 이벤트를 사용한다.

//Screen Button Control
int up_Value;
int down_Value;
int left_Value;
int right_Value;

bool up_Down;
bool down_Down;
bool left_Down;
bool right_Down;
bool up_Up;
bool down_Up;
bool left_Up;
bool right_Up;

public void ButtonDown(string type)
{
    switch (type)
    {
        case "U":
            up_Value = 1;
            up_Down = true;
            break;
        case "D":
            down_Value = -1;
            down_Down = true;
            break;
        case "L":
            left_Value = -1;
            left_Down = true;
            break;
        case "R":
            right_Value = 1;
            right_Down = true;
            break;
    }
}

public void ButtonUp(string type)
{
    switch (type)
    {
        case "U":
            up_Value = 0;
            up_Up = true;
            break;
        case "D":
            down_Value = 0;
            down_Up = true;
            break;
        case "L":
            left_Value = 0;
            left_Up = true;
            break;
        case "R":
            right_Value = 0;
            right_Up = true;
            break;
    }
}
//PlayerAction
private void FixedUpdate()
{
    //Move
    Vector2 moveVec = isHorizonMove ? new Vector2(h, 0) : new Vector2(0, v);
    rigid.velocity = moveVec * Speed;
    ...
}

void Update()
{
    //Move Value PC
    h = gameManager.isAction ? 0 : Input.GetAxisRaw("Horizontal");
    v = gameManager.isAction ? 0 : Input.GetAxisRaw("Vertical");

    //Move Value Mobile
    h = gameManager.isAction ? 0 : right_Value + left_Value;
    v = gameManager.isAction ? 0 : up_Value + down_Value;
    
    //Check Button Down & Up
    //PC
	hDown_PC = gameManager.isAction ? false : Input.GetButtonDown("Horizontal");
	vDown_PC = gameManager.isAction ? false : Input.GetButtonDown("Vertical");
	hUp_PC = gameManager.isAction ? false : Input.GetButtonUp("Horizontal");
	vUp_PC = gameManager.isAction ? false : Input.GetButtonUp("Vertical");


	//Check Button Down & Up
	//Mobile
	hDown_Mobile = gameManager.isAction ? false : left_Down || right_Down;
	vDown_Mobile = gameManager.isAction ? false : up_Down || down_Down;
	hUp_Mobile = gameManager.isAction ? false : left_Up || right_Up;
	vUp_Mobile = gameManager.isAction ? false : up_Up || down_Up;
	...
    //Mobile Variable Init
    up_Down = false;
    down_Down = false;
    left_Down = false;
    right_Down = false;
    up_Up = false;
    down_Up = false;
    left_Up = false;
    right_Up = false;
}


/* 합쳐도 된다 */
void Update()
{
	//Move Value PC & Mobile
	h = gameManager.isAction ? 0 : Input.GetAxisRaw("Horizontal") + right_Value + left_Value;
	v = gameManager.isAction ? 0 : Input.GetAxisRaw("Vertical") + up_Value + down_Value;

	//Check Button Down & Up
    //PC & Mobile
	hDown = gameManager.isAction ? false : Input.GetButtonDown("Horizontal") || left_Down || right_Down;
	vDown = gameManager.isAction ? false : Input.GetButtonDown("Vertical") || up_Down || down_Down;
	hUp = gameManager.isAction ? false : Input.GetButtonUp("Horizontal") || left_Up || right_Up;
	vUp = gameManager.isAction ? false : Input.GetButtonUp("Vertical") || up_Up || down_Up;
	...
    //Mobile Variable Init
    up_Down = false;
    down_Down = false;
    left_Down = false;
    right_Down = false;
    up_Up = false;
    down_Up = false;
    left_Up = false;
    right_Up = false;
}

그리고 버튼 입력을 받을 변수 12개를 생성한다. Input 클래스에서 상 / 하 / 좌 / 우 움직일 때 -1 0 1 값을 내부적으로 받았던 것처럼 Switch문에서 각 방향마다 변수를 할당한다. [버튼 Down일 때만 -1 1 값이 들어가도록, 버튼 Up일 때에는 0]

 

Button Down과 Up을 확인하는 로직은 Input.GetButtonDown("Horizontal") 또는 left_Down 또는 right_Down 중 하나라도 true라면 hDown이 true가 될 수 있으므로 OR 연산자를 사용한다.

Input.GetButton("Hrozontal")은 1프레임만 true로 하고, 그 다음 프레임으로 넘어가면 바로 자동으로 false가 된다. 그런데, left_Down, right_Down, left_Up, right_Down 등의 bool형 변수들은 전역변수이다. 한번 true로 걸어버리면 다시 false로 안돌아온다.

따라서 만들어둔 bool타입 전역변수들은 매 프레임마다 false로 돌려놔야한다. 따라서 Update() 함수 등에서 활용할 때는 로직이 끝나면 False로 초기화시킨다.


#3. 액션키

//PlayerAction
void Update()
{
	//Scan Object
	if (Input.GetButtonDown("Jump") && scanObject != null)
    	gameManager.Action(scanObject);
    ...
}

이제 남은 것은 esc키와 Action키이다. esc키와 Action키는 버튼 Up은 필요없고 버튼 Down만 필요하다.

일단 Action키의 역할을 보면 스페이스바(Jump키)를 눌렀고, 사물오브젝트가 인식됐다면 gameManager의 (스캔한 사물오브젝트의 정보를 전달해서 대화를 받아오고, 대화창 애니메이션을 활성화시키는) Action() 함수를 호출했었다.

//PlaerAction
public void ButtonDown(string type)
{
    switch (type)
    {
        case "U":
            up_Value = 1;
            up_Down = true;
            break;
        case "D":
            down_Value = -1;
            down_Down = true;
            break;
        case "L":
            left_Value = -1;
            left_Down = true;
            break;
        case "R":
            right_Value = 1;
            right_Down = true;
            break;
         case "A":
            if (scanObject != null)
                gameManager.Action(scanObject);
    }

그래서 ButtonDown()함수와 ButtonUp() 함수에 액션 조건을 추가하기만 하면 된다.

//GameManager
private void Update()
{
    //Sub Menu
    if (Input.GetButtonDown("Cancel"))
    {
        if (menuSet.activeSelf)
        {
            //Time.timeScale = 1;
            menuSet.SetActive(false);
        }  
        else
        {
            menuSet.SetActive(true);
            //Time.timeScale = 0;
        } 
    }
       
}

그리고 esc키는 이전에 어떠한 역할을 수행했는지 살펴보면, GameManager 스크립트에서 Update() 함수 내에서 작성되었다. esc 키를 누르면 서브메뉴가 활성화되고, 다시 누르면 서브메뉴가 비활성화(꺼지는) 로직이 작성되어있다.

*이렇게 외부 스크립트의 특정 키 기능은 중복해서 로직에 작성하게될 때 따로 함수로 분리하여 호출하는 것이 좋다.

//GameManager
private void Update()
{
    //Sub Menu
    if (Input.GetButtonDown("Cancel"))
    {
        SubMenuActive(); 
    }   
}

public void SubMenuActive()
{
    if (menuSet.activeSelf)
    {
        //Time.timeScale = 1;
        menuSet.SetActive(false);
    }
    else
    {
        menuSet.SetActive(true);
        //Time.timeScale = 0;
    }
}

//PlayerAction
case "C":
    gameManager.SubMenuActive();
    break;

Cancel 기능은 함수로 분리하여 호출하도록 한다.

 

이제 에디터에서 버튼 UI 오브젝트에 Event Trigger 컴포넌트를 추가하고, PointerDown을 추가하여 연결해주면 된다.


#4. UI 스케일링

UI Canvas의 Scaler Mode를 Screen Size로 변경한다. 그리고 Reference Resolution을 원하는 해상도에 맞게 조정해주고, 세로화면이니까 Match를 0에 맞춰준다.


#5. 모바일 빌드

File 메뉴>Build Settings에서 Platform을 Android로 설정한다. 그리고 Switch Platform 버튼을 눌러준다. 

그리고 Player Settings 버튼을 눌러주면 어플리케이션으로 뽑아내기위해 설정해야할 것들을 확인할 수 있다.

아이콘 설정

그리고 세로 화면으로 설정하려면 Allowed Orientations for Auto Rotation의 Portrait을 체크한다.

가로모드인 Landscape Left, Landscape Right 옵션을 체크 해제한다. 아예 고정할 수도 있다.

Splash Image는 게임을 맨 처음 시작할 때 회사로고 및 유통사 로고 등의 설정이다.

개인 퍼스널 라이센스(혹은 소속 회사 라이센스)가 있다면 유니티 Splash를 제거할 수도 있다.

Other Settings에서 Identification에서 Package Name에서 Company와 Package Name은 작성을 해주는 것이 좋다. 실제 이 경로로 데이터가 저장이 된다. (레지스토리 저장)

그리고 업데이트 시에는 Version을 설정값을 올려주면 된다. Bundle Version Code와 Version은 구글플레이 스토어에 게시할 때 올리는 버전이다.  구글플레이는 2019.08 이후부터 64bits 정책을 시행한다. 따라서 32비트는 받지 않는다. 그래서  Configuration에서 Scripting Backend를 Mono가 아니라, IL2CPP로 바꿔줘야한다. (64비트 APK 빌드를 위해 IL2CPP로 변경) x86 체크옵션은 체크해제해야한다. ARMv7과 ARMv64 옵션만 체크한다. 그래야지 구글플레이에 게시할 수 있다.

Android TV Compatibility는 목적에 맞게 체크해도되고, 안해도 된다.

*나머지 옵션은 생략 나중에 따로 알아보기

 

이렇게 Project Settings 설정을 끝냈으면, 반드시 저장을 하고 Build를 눌러 경로를 정하여 APK 파일을 생성한다.


문제상황 - 피드백

'Rendering at an odd-numbered resolution' 경고메세지가 Game창에 뜨는 문제

그리고 "Rendering at an odd-numbered resolution (764*325). Pixel Perfect Camera may not work properly in this situation"라는 메세지가 Game창에 뜨는 경우가 있는데, Pixel Perfect Camera는 해상도가 홀수면 오류가 생긴다. 유니티 에디터의 Game창은 해상도가 홀수가 될 수도 있다. 따라서 이 해상도에서는 화면이 제대로 나오지 않을 수 있어요라는 경고성 메세지가 뜨는 것이고, 창 크기를 조절하면 어느 순간 메세지가 사라질 것이다.

 

실행 시 Camera Size가 임의로 변경되는 문제

유니티 에디터에서 실행하면 원래 설정해놓은 Camera Size가 임의로 변경되어서, 실행 전 보여지는 Game창에서의 모습보다 확대되거나 축소되어 보이는 경우가 있다. 이 문제 역시 Game창의 크기를 조절하다보면 Camera Size가 원하는 크기대로 변경되는 지점이 있기 때문에, Game창 크기 조절로 간단히 해결할 수 있는 문제이다.

 

*그리고 Pixel Perfect Camera를 활성화해놓으면 픽셀퍼펙트 카메라는 정수배율을 유지하기위해 Camera Size를 조절한다. 이것은 Pixel Perfect Camera의 주요동작 중 하나이고, 정수배율을 유지함으로서 레트로 픽셀아트 스타일 게임에서 픽셀을 깨끗하게 보이도록 하기 위함이다. 따라서 Pixel Perfect Camera가 활성화되어있을 때 실행 시 Camera Size가 변경되는 것은 이상한 일이 아니다.

 

안드로이드 APK 빌드 에러 문제

Project path 'C: \Users\사용자명\문서\GitHub\Unity_TopDown-2D-RPG' contains non-ASCII characters at positoin 15, Android Tools don't work properly with non-ASCII paths.Please move your project to path containg only ASCII characters.

프로젝트 경로가 ASCII문자가 아닌 것을 포함하고 있기 때문이다. 따라서 한글경로가 포함되어있으면 안된다.

폴더명을 영어로 변경하고 빌드하면 문제없다고 한다.

Comments