[Unity Course 2] 08. 게임 매니저 2
위키북스 출판사 이재현 저자님의 '절대강좌! 유니티' 책을 참고하여 필기한 내용입니다.
싱글턴 디자인 패턴
앞서 구현한 GameManager 에 접근하는 방식은 코드도 길고, 접근할 때 GetComponent 함수를 사용해야하는 번거로움이 있음
싱글턴 디자인 패턴
: 오직 하나의 인스턴스만 생성하고, 그 인스턴스에 전역적인 접근을 제공하는 소프트웨어 디자인 패턴 중 하나
GameManager 인스턴스를 static 키워드를 사용하여 정적 메모리 영역에 올려두고 다른 스크립트에서 바로 접근할 수 있게 구현하는 방식
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class GameManager : MonoBehaviour
{
// 몬스터가 출현할 위치를 저장할 List 타입 변수
public List<Transform> points = new List<Transform>();
// 몬스터의 프리팹을 연결할 변수
public GameObject monster;
// 몬스터의 생성 간격
public float createTime = 3.0f;
// 게임의 종료 여부를 저장할 멤버 변수
private bool isGameOver;
public bool IsGameOver
{
...생략
}
// 싱글턴 인스턴스 선언
public static GameManager instance = null;
// 스크립트가 실행되면 가장 먼저 호출되는 유니티 이벤트 함수
private void Awake()
{
// instance가 할당되지 않았을 경우
if(instance ==null)
{
instance = this;
}
// instance에 할당된 클래스의 인스턴스가 다를 경우 새로 생성된 클래스를 의미
else if(instance != this)
{
Destroy(this.gameObject);
}
// 다른 씬으로 넘어가더라도 삭제하지 않고 유지함
DontDestroyOnLoad(this.gameObject);
}
private void Start()
{
...생략
}
void CreateMonster()
{
...생략
}
}
public 으로 외부에서 접근이 가능하도록하고 Static으로 선언하여 메모리에 상주시켜야됨
씬을 전환했다가 다시 돌아오면 기존에 있던 GameObject의 인스턴트와 두번째 생성된 인스턴스가 다르기 때문에 두번째 생긴 인스턴스는 삭제한다.
**최초에 생긴 GameManager 만 남게 되어 하나의 클래스가 지속하여 유지됨
void PlayerDie()
{
Debug.Log("Player Die !");
// 주인공 사망 이벤트 호출(발생)
OnPlayerDie();
// GameManager 스크립트의 IsGameOver 프로퍼티 값을 변경
//GameObject.Find("GameMgr").GetComponent<GameManager>().IsGameOver = true;
GameManager.instance.IsGameOver = true;
}
playerCtrl의 PlayerDie() 함수를 수정한다.
오브젝트 풀링
: 모바일 플랫폼에서 게임오브젝트 또는 프리팹을 동적으로 생성하는 작업은 물리적인 부하가 걸릴 수 밖에 없음
주기적으로 생성하는 객체는 씬을 처음 로드할 때 모두 생성한 다음 사용하는 방식이 속도면에서 유리함
***객체를 미리 생성하여 필요할때마다 가져다 사용하는 방식을 말함
몬스터 생성 로직을 오브젝트 풀 방식으로 바꾸기
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class GameManager : MonoBehaviour
{
// 몬스터가 출현할 위치를 저장할 List 타입 변수
public List<Transform> points = new List<Transform>();
// 몬스터를 미리 생성해 저장할 리스트 자료형
public List<GameObject> monsterPool = new List<GameObject>();
// 오브젝트 풀에 생성할 몬스터의 최대 개수
public int maxMonster = 10;
// 몬스터의 프리팹을 연결할 변수
public GameObject monster;
// 몬스터의 생성 간격
public float createTime = 3.0f;
// 게임의 종료 여부를 저장할 멤버 변수
private bool isGameOver;
public bool IsGameOver
{
... 생략
}
// 싱글턴 인스턴스 선언
public static GameManager instance = null;
// 스크립트가 실행되면 가장 먼저 호출되는 유니티 이벤트 함수
private void Awake()
{
...생략
}
private void Start()
{
// 몬스터 오브젝트 풀 생성
CreateMonsterPool();
// SpawnPointGroup 게임오브젝트의 Transform 컴포넌트 추출
Transform spawnPointGroup = GameObject.Find("SpawnPointGroup")?.transform;
// SpawnPointGroup 하위에 있는 모든 차일드 게임오브젝트의 Transform 컴포넌트 추출
//spawnPointGroup?.GetComponentsInChildren<Transform>(points);
// SpawnPointGroup 하위에 있는 모든 차일드 게임오브젝트의 Tranasform 컴포넌트 추출
foreach (Transform point in spawnPointGroup)
{
points.Add(point);
}
// 일정한 시간 간격으로 함수를 호출
InvokeRepeating("CreateMonster", 2.0f, createTime);
}
void CreateMonster()
{
...생략
}
void CreateMonsterPool()
{
for(int i=0;i<maxMonster; i++)
{
// 몬스터 생성
var _monster = Instantiate<GameObject>(monster);
// 몬스터의 이름을 지정
_monster.name = $"Monster_{i:00}";
// 몬스터 비활성화
_monster.SetActive(false);
// 생성한 몬스터를 오브젝트 풀에 추가
monsterPool.Add(_monster);
}
}
}
오브젝트 풀로 사용할 List 타입의 변수를 선언
List 배열과 유사하지만 추가, 삭제, 및 검색과 같은 기능을 쉽게 처리할 수 있음
생성할 몬스터의 개수를 maxMonster에 선언함
// 몬스터를 미리 생성해 저장할 리스트 자료형
public List<GameObject> monsterPool = new List<GameObject>();
// 오브젝트 풀에 생성할 몬스터의 최대 개수
public int maxMonster = 10;
사용자 지정 숫자 형식 문자열 - .NET | Microsoft Learn
사용자 지정 숫자 형식 문자열 - .NET
.NET에서 사용자 지정 숫자 데이터 서식 문자열 만들어 숫자 데이터 서식을 지정하는 방법을 알아봅니다. 사용자 지정 숫자 서식 문자열에는 하나 이상의 사용자 지정 숫자 지정자가 있습니다.
learn.microsoft.com
C# 의 숫자 형식의 포맷과 관련한 내용
// 몬스터의 이름을 지정
_monster.name = $"Monster_{i:00}";
숫자의 포맷 i:00은 콜론 뒤에 표기하며 00이 표시하는 의미는 숫자의 자릿수를 두자리로 유지하고 자릿수가 부족할 경우 0 으로 채운다는 뜻
오브젝트 풀에 추가한 모든 몬스턴는 생성하자마자 비활성화 시키고 사용할 때만 활성화함.

DontDestoryOnLoad 씬 하위에 표시고 몬스터 10 개가 리스트에 정상적으로 들어간 것을 확인할 수 있다.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class GameManager : MonoBehaviour
{
// 몬스터가 출현할 위치를 저장할 List 타입 변수
public List<Transform> points = new List<Transform>();
// 몬스터를 미리 생성해 저장할 리스트 자료형
public List<GameObject> monsterPool = new List<GameObject>();
// 오브젝트 풀에 생성할 몬스터의 최대 개수
public int maxMonster = 10;
// 몬스터의 프리팹을 연결할 변수
public GameObject monster;
// 몬스터의 생성 간격
public float createTime = 3.0f;
// 게임의 종료 여부를 저장할 멤버 변수
private bool isGameOver;
public bool IsGameOver
{
get { return isGameOver; }
set
{
isGameOver = value;
if (isGameOver)
{
CancelInvoke("CreateMonster");
}
}
}
// 싱글턴 인스턴스 선언
public static GameManager instance = null;
// 스크립트가 실행되면 가장 먼저 호출되는 유니티 이벤트 함수
private void Awake()
{
// instance가 할당되지 않았을 경우
if(instance ==null)
{
instance = this;
}
// instance에 할당된 클래스의 인스턴스가 다를 경우 새로 생성된 클래스를 의미
else if(instance != this)
{
Destroy(this.gameObject);
}
// 다른 씬으로 넘어가더라도 삭제하지 않고 유지함
DontDestroyOnLoad(this.gameObject);
}
private void Start()
{
// 몬스터 오브젝트 풀 생성
CreateMonsterPool();
// SpawnPointGroup 게임오브젝트의 Transform 컴포넌트 추출
Transform spawnPointGroup = GameObject.Find("SpawnPointGroup")?.transform;
// SpawnPointGroup 하위에 있는 모든 차일드 게임오브젝트의 Transform 컴포넌트 추출
//spawnPointGroup?.GetComponentsInChildren<Transform>(points);
// SpawnPointGroup 하위에 있는 모든 차일드 게임오브젝트의 Tranasform 컴포넌트 추출
foreach (Transform point in spawnPointGroup)
{
points.Add(point);
}
// 일정한 시간 간격으로 함수를 호출
InvokeRepeating("CreateMonster", 2.0f, createTime);
}
void CreateMonster()
{
// 몬스터의 불규칙한 생성 위치 산출
int idx = Random.Range(0, points.Count);
// 몬스터 프리팹 생성
// Instantiate(monster, points[idx].position, points[idx].rotation);
// 오브젝트 풀에서 몬스터 추출
GameObject _monster = GetMonsterInPool();
_monster?.transform.SetPositionAndRotation(points[idx].position, points[idx].rotation);
// 추출한 몬스터 활성화
_monster?.SetActive(true);
}
void CreateMonsterPool()
{
for(int i=0;i<maxMonster; i++)
{
// 몬스터 생성
var _monster = Instantiate<GameObject>(monster);
// 몬스터의 이름을 지정
_monster.name = $"Monster_{i:00}";
// 몬스터 비활성화
_monster.SetActive(false);
// 생성한 몬스터를 오브젝트 풀에 추가
monsterPool.Add(_monster);
}
}
public GameObject GetMonsterInPool()
{
// 오브젝트 풀의 처음부터 끝까지 순회
foreach (var _monster in monsterPool)
{
// 비활성화 여부로 사용 가능한 몬스터를 판단
if(_monster.activeSelf == false)
{
// 몬스터 반환
return _monster;
}
}
return null;
}
}
GameManager 스크립트를 위와 같이 변경한다.
CreateMonsterInPool() 함수는 오브젝트 풀 배열을 순회하면서 비활성화 여부를 판단해 비활성화된 몬스터 프리팹을 반환
public GameObject GetMonsterInPool()
{
// 오브젝트 풀의 처음부터 끝까지 순회
foreach (var _monster in monsterPool)
{
// 비활성화 여부로 사용 가능한 몬스터를 판단
if(_monster.activeSelf == false)
{
// 몬스터 반환
return _monster;
}
}
return null;
}
Transform.SetPositionAndRotation 함수는 위치와 회전 값을 동시에 설정하는 함수
GetMonsterInPool 함수로 추출한 몬스터는 활성화하기 전에 위치와 회전 값을 설정
앞에서는 몬스터가 사망했을 때 몬스터의 모든 코루틴 함수와 NavMeshAgent 를 정지시키는 방법으로 처리함
그러나 몬스터가 사망하고 일정 시간이 지난 후에 오브젝트 풀에 재사용 가능한 상태로 돌려줘야됨
- 각종 컴포넌트를 할당하는 로직을 맨 먼저 수행하기 위해 Start 함수명을 Awake로 변경한다
- 기존 Start 함수에 있던 코루틴 실행 코드는 OnEnable 함수로 옮긴다.
- MonsterAction 함수에서 일정 시간이 지난 몬스터를 비활성화 한다.
이 순서대로 MonsterCtrl 스크립트를 변경한다.
using System.Collections;
using UnityEngine;
using UnityEngine.AI; // 내비게이션 기능을 사용하기 위해 추가해야 하는 네임 스페이스
public class MonsterCtrl : MonoBehaviour
{
...생략
// 스크립트가 활성화될 때마다 호출되는 함수
private void OnEnable()
{
// 이벤트 발생 시 수행할 함수 연결
PlayerCtrl.OnPlayerDie += this.OnPlayerDie;
// 몬스터의 상태를 체크하는 코루틴 함수 호출
StartCoroutine(CheckMonsterState());
// 상태에 따라 몬스터의 행동을 수행하는 코루틴 함수 호출
StartCoroutine(MonstorAction());
}
// 스크립트가 비활성화될 때마다 호출되는 함수
private void OnDisable()
{
...생략
}
private void Awake()
{
// 몬스터의 Transform 할당
monsterTr = GetComponent<Transform>();
// 추적 대상인 Player의 Trasnform 할당
playerTr = GameObject.FindWithTag("PLAYER").GetComponent<Transform>();
// NavMEshAgent 컴포넌트 할당
agent = GetComponent<NavMeshAgent>();
// Animator 컴포넌트 할당
anim = GetComponent<Animator>();
// BloodSprayEffect 프리팹 로드
bloodEffect = Resources.Load<GameObject>("BloodSprayEffect");
}
// 일정한 간격을 몬스터의 행동 상태를 체크
IEnumerator CheckMonsterState()
{
...생략
}
IEnumerator MonstorAction()
{
while(!isDie)
{
switch(state){
// Idle 상태
case State.IDLE:
// 추적 중지
agent.isStopped = true;
// Animator의 IsTrace 변수를 false로 설정
anim.SetBool(hashTrace, false);
break;
// 추적 상태
case State.TRACE:
// 추적 대상의 좌표로 이동 시작
agent.SetDestination(playerTr.position);
agent.isStopped = false;
// Animator의 IsTrace 변수를 true로 설정
anim.SetBool(hashTrace, true);
// Animator의 IsAttack 변수를 false로 설정
anim.SetBool(hashAttack, false);
break;
// 공격 상태
case State.ATTACK:
// Animator의 IsAttack 변수를 true로 설정
anim.SetBool(hashAttack, true);
break;
// 사망
case State.DIE:
isDie = true;
//추적 정지
agent.isStopped = true;
//사망 애니메이션 실행
anim.SetTrigger(hashDie);
// 몬스터의 Collider 컴포넌트 비활성화
GetComponent<CapsuleCollider>().enabled = false;
// 일정 시간 대기 후 오브젝트 풀링으로 환원
yield return new WaitForSeconds(3.0f);
// 사망 후 다시 사용할 때를 위해 hp 값 초기화
hp = 100;
isDie = false;
// 몬스터의 Collider 컴포넌트 활성화
GetComponent<CapsuleCollider>().enabled = true;
// 몬스터 비활성화
this.gameObject.SetActive(false);
break;
}
yield return new WaitForSeconds(0.3f);
}
}
private void OnCollisionEnter(Collision coll)
{
...생략
}
void ShowBloodEffect(Vector3 pos, Quaternion rot)
{
...생략
}
void OnPlayerDie()
{
...생략
}
private void OnDrawGizmos()
{
...생략
}
}
내가 코드를 잘못 작성했는지 몬스터가 죽고나서 다시 Idle 상태로 돌아가야하는데
활성화가 되어도 상태가 Die라서 끊임없이 죽는 모션이 나오게 되었다..
인터넷에 검색해 봤는데 나와 똑같은 오류를 겪고 있는 사람이 있는걸 보아하니 코드에서 초기화의 문제인듯 하여
지은이의 블로그와 깃허브를 찾아본 결과 코드에 한줄이 빠져있는 것을 확인할 수 있었다.
절대강좌! 유니티 2021 출간 | IndieGameMaker (unity3dstudy.com)
절대강좌! 유니티 2021 출간
절대강좌! 유니티 절판된지 무려 2년만에 개정판을 새롭게 출간했습니다. 2020 버전으로 집필을 시작해 최종 2021 버전으로 출간하게 됐습니다. 6년전 초판본에 수록되었던 포톤 네트워크와 유니
unity3dstudy.com
해당 블로그이다.
MonsterCtrl 스크립트의 MonsterAction 함수에 Case State.DIE: 를 다음과 같이 수정하도록 한다.
// 사망
case State.DIE:
isDie = true;
//추적 정지
agent.isStopped = true;
//사망 애니메이션 실행
anim.SetTrigger(hashDie);
// 몬스터의 Collider 컴포넌트 비활성화
GetComponent<CapsuleCollider>().enabled = false;
// 일정 시간 대기 후 오브젝트 풀링으로 환원
yield return new WaitForSeconds(3.0f);
// 사망 후 다시 사용할 때를 위해 hp 값 초기화
hp = 100;
isDie = false;
state = State.IDLE; // 추가한 코드
// 몬스터의 Collider 컴포넌트 활성화
GetComponent<CapsuleCollider>().enabled = true;
// 몬스터 비활성화
this.gameObject.SetActive(false);
break;