기록 보관소

[Unity/유니티] 기초-뱀서라이크: 모바일 빌드하기[17] 본문

유니티 프로젝트/뱀서라이크

[Unity/유니티] 기초-뱀서라이크: 모바일 빌드하기[17]

JongHoon 2023. 9. 2. 21:34

개요

유니티 독학을 위해 아래 링크의 골드메탈님의 영상들을 보고 직접 따라 해보면서 진행 상황을 쓰고 배웠던 점을 요약한다.

https://youtube.com/playlist?list=PLO-mt5Iu5TeYI4dbYwWP8JqZMC9iuUIW2 

 

📚유니티 기초 강좌

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

www.youtube.com


뱀서라이크: 모바일 빌드하[17]

1. 조이스틱 추가

Window -> Package Manager -> Packages:Unity Registry -> Input System -> Samples -> On-Screen Controls 임포트 하기
Image 생성 후 수정. 조이스틱의 배경.
아까 임포트했던 Input System 폴더의 Stick을 Joy의 자식 오브젝트로 이동 및 변경
Text는 필요 없으므로 이를 제거하려면 프리펩의 연결을 끊어야 가능. Unpack Completely를 한다.
이미지 수정
Stick의 On-Screen Stick의 Movement Range를 10으로 변경
Player의 Player Input에서 Auto-Switch를 체크해제하고 Default Scheme 변경

  • Auto-Switch : 사용자의 디바이스에 따라서 장치 설정을 바꾸는 설정.
    • 현재 게임 테스트 환경에서는 마우스 + 게임 패드를 사용하는 것으로 인식하므로 이동에 방해를 줄 수 있다. 그래서 체크 해제.

게임 시작 후에 컨트롤러가 보이도록 Scale을 0으로 변경

//GameManager Script

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;	//장면 관리(Scene Manager 같은)를 사용하기 위한 네임스페이스.

public class GameManager : MonoBehaviour {
	public static GameManager instance;
	[Header("# Game Control")]
	public bool isLive;	//시간 정지 여부 확인 변수
	public float gameTime;	//게임 시간 변수
	public float maxGameTime = 2 * 10f; //최대 게임 시간 변수(20초).
	[Header("# Player Info")]
	public int playerId;
	public float health;
	public float maxHealth = 100;
    public int level;
	public int kill;
	public int exp;
	public int[] nextExp = { 3, 5, 10, 100, 150, 210, 280, 360, 450, 600 };
    [Header("# Game Object")]
    public PoolManager pool;
    public Player player;
	public LevelUp uiLevelUp;
	public Result uiResult;
	public Transform uiJoy;
	public GameObject enemyCleaner;

    void Awake() {
		instance = this;
	}

	public void GameStart(int id) {
		playerId = id;
		health = maxHealth;

		player.gameObject.SetActive(true);
		uiLevelUp.Select(playerId % 2);
		Resume();

		AudioManager.instance.PlayBgm(true);
        AudioManager.instance.PlaySfx(AudioManager.Sfx.Select);
    }

	public void GameOver() {
		StartCoroutine(GameOverRoutine());
	}

	IEnumerator GameOverRoutine() {
		isLive = false;
		
		yield return new WaitForSeconds(0.5f);

		uiResult.gameObject.SetActive(true);
		uiResult.Lose();
		Stop();

        AudioManager.instance.PlayBgm(false);
        AudioManager.instance.PlaySfx(AudioManager.Sfx.Lose);
    }

    public void GameVictory() {
        StartCoroutine(GameVictoryRoutine());
    }

    IEnumerator GameVictoryRoutine() {
        isLive = false;
		enemyCleaner.SetActive(true);

        yield return new WaitForSeconds(0.5f);

        uiResult.gameObject.SetActive(true);
        uiResult.Win();
        Stop();

        AudioManager.instance.PlayBgm(false);
        AudioManager.instance.PlaySfx(AudioManager.Sfx.Win);
    }

    public void GameRetry() {
		SceneManager.LoadScene(0);	//LoadScene() : 이름 혹은 인덱스로 장면을 새롭게 부르는 함수
	}

	void Update() {
		if (!isLive)
			return;

		gameTime += Time.deltaTime;

		if (gameTime > maxGameTime) {
			gameTime = maxGameTime;
			GameVictory();
		}
	}

	public void GetExp() {
		if (!isLive)	//EnemyCleaner로 경험치를 못얻게 하기 위함
			return;

		exp++;

		if (exp == nextExp[Mathf.Min(level, nextExp.Length - 1)]) {
			level++;
			exp = 0;
			uiLevelUp.Show();
		}
	}

	public void Stop() {
		isLive = false;
		Time.timeScale = 0;
		uiJoy.localScale = Vector3.zero;
	}

    public void Resume() {
        isLive = true;
        Time.timeScale = 1; //값이 1보다 크면 그만큼 시간이 빠르게 흐름. 모바일 게임에서 시간 가속하는 것이 이것..
        uiJoy.localScale = Vector3.one;
    }
}

uiJoy 변수 할당
테스트 실행. 조이스틱은 보이지 않는다
캐릭터를 선택하고 게임이 시작되니 조이스틱이 나타나서 사용할 수 있다


2. 종료버튼 만들기

GameStart의 앵커를 전체 화면 크기로 확장
종료 버튼으로 쓸 Button 생성 및 수정
버튼 색상과 위치 수정 및 텍스트 수정
버튼에 Shadow 컴포넌트 추가 및 이름 변경

//GameManager Script

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;	//장면 관리(Scene Manager 같은)를 사용하기 위한 네임스페이스.

public class GameManager : MonoBehaviour {
	public static GameManager instance;
	[Header("# Game Control")]
	public bool isLive;	//시간 정지 여부 확인 변수
	public float gameTime;	//게임 시간 변수
	public float maxGameTime = 2 * 10f; //최대 게임 시간 변수(20초).
	[Header("# Player Info")]
	public int playerId;
	public float health;
	public float maxHealth = 100;
    public int level;
	public int kill;
	public int exp;
	public int[] nextExp = { 3, 5, 10, 100, 150, 210, 280, 360, 450, 600 };
    [Header("# Game Object")]
    public PoolManager pool;
    public Player player;
	public LevelUp uiLevelUp;
	public Result uiResult;
	public Transform uiJoy;
	public GameObject enemyCleaner;

    void Awake() {
		instance = this;
	}

	public void GameStart(int id) {
		playerId = id;
		health = maxHealth;

		player.gameObject.SetActive(true);
		uiLevelUp.Select(playerId % 2);
		Resume();

		AudioManager.instance.PlayBgm(true);
        AudioManager.instance.PlaySfx(AudioManager.Sfx.Select);
    }

	public void GameOver() {
		StartCoroutine(GameOverRoutine());
	}

	IEnumerator GameOverRoutine() {
		isLive = false;
		
		yield return new WaitForSeconds(0.5f);

		uiResult.gameObject.SetActive(true);
		uiResult.Lose();
		Stop();

        AudioManager.instance.PlayBgm(false);
        AudioManager.instance.PlaySfx(AudioManager.Sfx.Lose);
    }

    public void GameVictory() {
        StartCoroutine(GameVictoryRoutine());
    }

    IEnumerator GameVictoryRoutine() {
        isLive = false;
		enemyCleaner.SetActive(true);

        yield return new WaitForSeconds(0.5f);

        uiResult.gameObject.SetActive(true);
        uiResult.Win();
        Stop();

        AudioManager.instance.PlayBgm(false);
        AudioManager.instance.PlaySfx(AudioManager.Sfx.Win);
    }

    public void GameRetry() {
		SceneManager.LoadScene(0);	//LoadScene() : 이름 혹은 인덱스로 장면을 새롭게 부르는 함수
	}

    public void GameQuit() {
        Application.Quit();
    }

    void Update() {
		if (!isLive)
			return;

		gameTime += Time.deltaTime;

		if (gameTime > maxGameTime) {
			gameTime = maxGameTime;
			GameVictory();
		}
	}

	public void GetExp() {
		if (!isLive)	//EnemyCleaner로 경험치를 못얻게 하기 위함
			return;

		exp++;

		if (exp == nextExp[Mathf.Min(level, nextExp.Length - 1)]) {
			level++;
			exp = 0;
			uiLevelUp.Show();
		}
	}

	public void Stop() {
		isLive = false;
		Time.timeScale = 0;
		uiJoy.localScale = Vector3.zero;
	}

    public void Resume() {
        isLive = true;
        Time.timeScale = 1; //값이 1보다 크면 그만큼 시간이 빠르게 흐름. 모바일 게임에서 시간 가속하는 것이 이것..
        uiJoy.localScale = Vector3.one;
    }
}

종료 버튼의 On Click 설정


3. 렌더러와 프레임 지정

Edit -> Project Settings -> Quality 항목 변경

//GameManager Script

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;	//장면 관리(Scene Manager 같은)를 사용하기 위한 네임스페이스.

public class GameManager : MonoBehaviour {
	public static GameManager instance;
	[Header("# Game Control")]
	public bool isLive;	//시간 정지 여부 확인 변수
	public float gameTime;	//게임 시간 변수
	public float maxGameTime = 2 * 10f; //최대 게임 시간 변수(20초).
	[Header("# Player Info")]
	public int playerId;
	public float health;
	public float maxHealth = 100;
    public int level;
	public int kill;
	public int exp;
	public int[] nextExp = { 3, 5, 10, 100, 150, 210, 280, 360, 450, 600 };
    [Header("# Game Object")]
    public PoolManager pool;
    public Player player;
	public LevelUp uiLevelUp;
	public Result uiResult;
	public Transform uiJoy;
	public GameObject enemyCleaner;

    void Awake() {
		instance = this;
		Application.targetFrameRate = 60;
	}

	public void GameStart(int id) {
		playerId = id;
		health = maxHealth;

		player.gameObject.SetActive(true);
		uiLevelUp.Select(playerId % 2);
		Resume();

		AudioManager.instance.PlayBgm(true);
        AudioManager.instance.PlaySfx(AudioManager.Sfx.Select);
    }

	public void GameOver() {
		StartCoroutine(GameOverRoutine());
	}

	IEnumerator GameOverRoutine() {
		isLive = false;
		
		yield return new WaitForSeconds(0.5f);

		uiResult.gameObject.SetActive(true);
		uiResult.Lose();
		Stop();

        AudioManager.instance.PlayBgm(false);
        AudioManager.instance.PlaySfx(AudioManager.Sfx.Lose);
    }

    public void GameVictory() {
        StartCoroutine(GameVictoryRoutine());
    }

    IEnumerator GameVictoryRoutine() {
        isLive = false;
		enemyCleaner.SetActive(true);

        yield return new WaitForSeconds(0.5f);

        uiResult.gameObject.SetActive(true);
        uiResult.Win();
        Stop();

        AudioManager.instance.PlayBgm(false);
        AudioManager.instance.PlaySfx(AudioManager.Sfx.Win);
    }

    public void GameRetry() {
		SceneManager.LoadScene(0);	//LoadScene() : 이름 혹은 인덱스로 장면을 새롭게 부르는 함수
	}

    public void GameQuit() {
        Application.Quit();
    }

    void Update() {
		if (!isLive)
			return;

		gameTime += Time.deltaTime;

		if (gameTime > maxGameTime) {
			gameTime = maxGameTime;
			GameVictory();
		}
	}

	public void GetExp() {
		if (!isLive)	//EnemyCleaner로 경험치를 못얻게 하기 위함
			return;

		exp++;

		if (exp == nextExp[Mathf.Min(level, nextExp.Length - 1)]) {
			level++;
			exp = 0;
			uiLevelUp.Show();
		}
	}

	public void Stop() {
		isLive = false;
		Time.timeScale = 0;
		uiJoy.localScale = Vector3.zero;
	}

    public void Resume() {
        isLive = true;
        Time.timeScale = 1; //값이 1보다 크면 그만큼 시간이 빠르게 흐름. 모바일 게임에서 시간 가속하는 것이 이것..
        uiJoy.localScale = Vector3.one;
    }
}

테스트 실행. Stats를 보니 프레임이 60대에서 고정되고 있다.


4. 포스트 프로세싱

Global Volume 생성
Volume 컴포넌트의 Profile에 New를 눌러 생성 후 Add Override 하기
Main Camera의 Camera 컴포넌트 -> Rendering에서 Post Processing 체크
Bloom 설정

  • Bloom : 빛 번짐 효과

Film Grain 설정

  • Film Grain : 필름 노이즈 효과

Vignette 설정

  • Vignette : 모서리 음영 처리 효과

후처리가 완성된 모습
Global Volume에서 Volum 컴포넌트의 Weight을 0으로 처리해서 보면 꽤 차이가 느껴진다


5. 모바일 시뮬레이터

Simulator 선택
이렇게 모바일 시뮬레이팅이 가능해진다.
Safe Area 버튼을 누르면 기기 회전, 안전 구역(이미지가 가려지지 않음) 등을 미리 확인할 수 있다

  • 이를 이용해서 모바일 기기별로 UI가 깨지거나, 어색한 부분을 수정할 수 있다.

6. 모바일 빌드

File -> Build Settings에서 Android로 Switch Platform 해주기

  • Build Settings 화면 상 왼쪽 아랫부분의 Player Settings... 버튼을 클릭해서 아래와 같은 변경 사항들을 입력한다.

Player 설정
Resolution and Presentation 설정
Splash Image는 시작 화면의 유니티 로고 설정. 개인 기호에 따라 설정한다.
Other Settings의 Identification 설정. 두 API Level을 확인하는 것이 좋다.

  • Target API Level은 사용하는 유니티 버전이 너무 낮을 경우, Automatic 보다는 직접 설정하는 것이 안전하다고 한다.

Other Settings의 Configuration 설정. Scripting Backend를 IL2CPP로 바꾸고 ARM64도 체크.

  • 64비트로 빌드할 것이기 때문에 위 설정은 필수적으로 진행해야 한다.
  • 또한 구글 플레이 스토어에 등록할때도 해당 세팅은 꼭 필요하다고 한다.

설정이 다 끝났으면 Build 버튼을 눌러 어플리케이션 빌드.

 

Undead Survivor.apk

 

drive.google.com

apk 파일을 다운받아 설치해 실행한 모습.
잘 작동한다.


+) 번외 : 무기가 목표물을 바라보도록 만들기

https://youtu.be/NE5j8YmJ5Ds?si=SaGvi99BzeJlEUlk

  • 앞선 시간에 했었던 강좌 11+ 영상의 댓글을 보면, 골드메탈님이 총이 목표물을 바라보도록 설정하는 코드를 남겨주셨다.

해당 영상에 골드메탈님이 직접 남겨주신 댓글

  • 이를 이용해서 마지막으로 무기가 목표물을 바라보도록 만들 것이다.
    • 댓글처럼 저 부분만 추가해서 작동하지는 않는다. 답글처럼 player변수가 SpriteRenderer로 되어있어서, Player 변수를 따로 선언해서 해결해야한다. 아래 내가 작성한 스크립트는 따로 변수를 선언해서 해결했다.
  • 다만 빌드 이후에 갑자기 생각나서 하는거라 모바일 빌드는 하지 않고 간단하게 에디터 내에서 테스트를 진행했다.
//Hand Script

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Hand : MonoBehaviour {
    public bool isLeft;
    public SpriteRenderer spriter;

    SpriteRenderer player;
    Player p;

    Vector3 rightPos = new Vector3(0.35f, -0.15f, 0);
    Vector3 rightPosReverse = new Vector3(-0.15f, -0.15f, 0);
    Quaternion leftRot = Quaternion.Euler(0, 0, -35);
    Quaternion leftRotReverse = Quaternion.Euler(0, 0, -135);

    void Awake() {
        player = GetComponentsInParent<SpriteRenderer>()[1];
        p = GameManager.instance.player;
    }

    void LateUpdate() {
        bool isReverse = player.flipX;

        if (isLeft) {   //근접 무기
            transform.localRotation = isReverse ? leftRotReverse : leftRot;
            spriter.flipY = isReverse;
            spriter.sortingOrder = isReverse ? 4 : 6;
        }
        else if (p.scanner.nearestTarget) {
            Vector3 targetPos = p.scanner.nearestTarget.position;
            Vector3 dir = targetPos - transform.position;
            transform.localRotation = Quaternion.FromToRotation(Vector3.right, dir);

            bool isRotA = transform.localRotation.eulerAngles.z > 90 && transform.localRotation.eulerAngles.z < 270;
            bool isRotB = transform.localRotation.eulerAngles.z < -90 && transform.localRotation.eulerAngles.z > -270;
            spriter.flipY = isRotA || isRotB;
        }
        else {  //원거리 무기
            transform.localPosition = isReverse ? rightPosReverse : rightPos;
            spriter.flipX = isReverse;
            //spriter.sortingOrder = isReverse ? 6 : 4; //총이 타겟을 따라 다니므로, 잘 보이기 위해 6으로 고정해서 주석 처리.
        }
    }
}

총이 있는 Hand Right의 Order in Layer를 6으로 변경

테스트 영상. 무기가 잘 보이고 목표물 위치에 따라 잘 움직인다.