모바일플랫폼에서 게임오브젝트및 프리팹을 동적으로 생성하는 방식은 부하를 증가시킨다. 따라서 미리 만들어 놓고 필요할때 가져다 쓰는 방식을 오브젝트 풀링이라 한다.

동적으로 만들던 몬스터를 게임시작시 미리 만들어 놓고 비활성화해서 하나씩 불러다 쓰고 사망시 Destroy하지 않고 오브젝트풀에서 재사용할 수 있게 변경한다. 다음과 같이 GameManager.cs 를 수정한다.

몬스터 풀로 사용할 List<>와 최대생성수를 정의한다.

    public List<GameObject> monsterPool = new List<GameObject>(); // 몬스트풀 리스트
    public int maxMonsters = 10; //몬스터 최대 생성 수

CreateMonsterPool()을 호출 몬스터프리팹을이용 Instantiate()로 몬스터를 생성하고 이름을 Monster_00방식으로 변경하고 비활성화후 풀 List에 add해준다.

    void CreateMonsterPool() {  //몬스트풀 생성 함수
        for(int i=0; i < maxMonsters; i++) {
            var _monster = Instantiate<GameObject>(monster); //몬스터 생성
            _monster.name = $"Monster_{i:00}"; //몬스터이름 생성
            _monster.SetActive(false);  //몬스터 비활성화
            monsterPool.Add(_monster); //생성된 몬스트를 풀에 저장
        }
    }

풀생성후 InvokeRepeating()함수를 통해 몬스터가 주기적으로 자동생성된다.

    void Start() {
        CreateMonsterPool();  // 몬스터풀 생성 함수
        InvokeRepeating("CreateMonster", 2.0f, createTime);  //2초후 반복적으로 몬스터를 만든다
    }

몬스터 생성은 Instantiate대신 풀에서 비활성환된 몬스터를 골라 리턴 GetMonsterInPool()을 호출 

    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);
    }

풀을 순환하며 비활성환된 몬스터를 골라 리턴해주는 GetMonsterInPool() 

    public GameObject GetMonsterInPool() {
        foreach(var _monster in monsterPool) {
            if(_monster.activeSelf == false) {  
                return _monster;  }
        }
        return null;
    }

이제 플레이해보면 10개까지 잘 생성되는데 몬스터를 죽이면 다시 생성되지 않는다 왜냐하면 몬스터가 Destroy되지만 active한 상태가 되기 때문에 풀에서 비활성 몬스터가 없어지기 때문에 Destroy대신 active를 false로 해준다

몬스터의 Hp관리는 MonsterCtrl에서 한다. Start()함수를 Awake()로 이름을 바꾸고

    void Awake()
    {
        monsterTr= GetComponent<Transform>();
        playerTr = GameObject.FindWithTag("PLAYER").GetComponent<Transform>();  
        agent = GetComponent<NavMeshAgent>();
        anim = GetComponent<Animator>();
        bloodEffect = Resources.Load<GameObject>("GoopSpray"); //혈흔효과      
    }

StartCourotine()2개를 OnEnable함수로 옮겨 연결되지 않는 컴포넌트의 참조하는 오류를 막는다

    private void OnEnable() {  //스크립트가 활성활 될때 콜백되는 함수
        PlayerCtrl.OnPlayerDie += this.OnPlayerDie; //교재는 PlayerCtrl.
        StartCoroutine(CheckMonsterState()); //몬스트 상태를 체크하는 코루틴
        StartCoroutine(MonsterAction());  //상태에 따라 행동을 수행하는 코루틴
    }

MonsterAction()의 case State.DIE부분을 수정한다. 책 코드대로 실행해본 결과 죽은 몬스터가 부활할때 다시 죽는 경우가 발생했다. 따라서 state=State.IDLE을 추가해서 해결하였다.(DIE상태로 부활하면 다시 DIE상태로 무한 반복한다)

IEnumerator MonsterAction() {
        while (!isDie) {
            transform.LookAt(playerTr);
            switch (state) {
            //중략
                case State.DIE: //사망상태
                    isDie= true;  
                    agent.isStopped = true;  //추적 정지
                    anim.SetTrigger(hashDie); // 애니메이트 hasDie 트리거발생
                    GetComponent<CapsuleCollider>().enabled = false;  //충돌컴포넌트 비활성화
                    //일정시간 경과후 오브젝트풀링으로 환원
                    yield return new WaitForSeconds(0.3f); //일정시간경과후
                    hp = 10;  //hp를 초기화하고
                    isDie= false; // 상태를 살리고
                    GetComponent<CapsuleCollider>().enabled = true;  //콜라이더를 다시 true
                    this.gameObject.SetActive(false); //몬스터를 비활성화 한다.
                    state = State.IDLE;  //Idel로 안하면 태어나자 마자 죽는다
                    break;
            }
            yield return new WaitForSeconds(0.3f);
        }
    }

GameManager.cs
0.00MB
MonsterCtrl.cs
0.01MB

 

싱글턴 패턴은 메모리상에 오직 하나의 인스턴스만 생성하고 그 인스턴스에 전역적인 접근을 제공하는 방식이다. 

스크립트에서 제일먼저 실행되는 Awake()함수에서 GameManager를 static키워드로 정적 메모리영역에 올려두고 다른 스크립트에서 바로 접근할 수 있게 구현하는 방식이다.

그리고 자신이 아니라면 지우고 다른씬으로 넘어가도 삭제되지 않게 DontDestroyOnLoad()명령을 실행해 놓는다.

public static GameManager instance = null;
private void Awake() {
    if (instance == null) {
        instance = this; //싱글턴지정
    } else if (instance != this){  //싱글턴검사
        Destroy(this.gameObject);
    }
    DontDestroyOnLoad(this.gameObject);  //다른씬으로 넘어가도 삭제하지 않고 유지함
}

이제  PlayerCtrl에서 검색이나 연결없이 GameManager 컴포넌트의 프라퍼티로 바로 사용가능하다.

    void PlayerDie() {
        //MONSTER 태그를 가진 모든 게임오브젝트를 찾아옴
        /*
        GameObject[] monsters = GameObject.FindGameObjectsWithTag("MONSTER");
        foreach(GameObject monster in monsters) {  //모든 오브젝트를 순차적으로 불러옴
            monster.SendMessage("OnPlayerDie", SendMessageOptions.DontRequireReceiver);
        }
        */
        OnPlayerDie();  //주인공 사망 이벤트 호출(발생)
        //GameObject.Find("GameMgr").GetComponent<GameManager>().IsGameOver = true;  //적 생산 멈춤
        GameManager.instance.IsGameOver= true; // IsGameOver 프라퍼티로 바로 사용가능
    }

 

이번에는 게임 개발시 자주 사용하는 싱글턴 디자인 패턴을 활용해 게임 매니저를 제작하고 오브젝트 폴링을 활용해 성능을 높이는 기법에 대해 알아본다.

 

SpawnPointGroup 생성

몬스터가 출현할 불규칙한 위치 정보는 게임 매니저가 알고 있어야 한다. 먼저 빈 게임오브젝트를 생성하고 SpawnPointGroup으로 지정한다. Position은 (0,0,0)이다 SpawnPointGroup를 선택하고 하위에 빈게임오브젝트를 생성하고 Point라고 이름을 바꾼다. 시각적으로 표현하기 위해 MyGizmo.cs를 작성해서 연결한다.

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

public class MyGizmo : MonoBehaviour
{
    public Color _color = Color.yellow;
    public float _radius = 0.1f;
    private void OnDrawGizmos() {
        Gizmos.color = _color;
        Gizmos.DrawSphere(transform.position, _radius);
    }
}

게임 매니저는 전반적으로 게임을 제어 관리하는 기능을 모아 놓은 기능이다.

 

이제 몬스터를 일정시간 간격으로 여러 Spawn Point중에서 랜덤으로 발생시킬것이다. 씬뷰의 모든 몬스터는 삭제할것이지만 지우기전에 프리팹으로 전환되었는지 확인 바랍니다.

빈오브젝트를 하나 만들고 이름을 GameManager로 한다. 같은 이름으로 스크립트를 추가한다.

포인트를 Ctr-D로 복사해서 여러군대 배치한다.

 

Game  Manager 객체 생성

빈 오브젝트를 생성하고 GameMgr로 이름짓는다. GameManager라는 스크립트를 작성한다.

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

public class GameManager : MonoBehaviour
{
    // Start is called before the first frame update
    public Transform[] points;
    void Start() {
        //null이 아니면 뒤를 실행
        Transform spawnPointGroup = GameObject.Find("SpawnPointGroup")?.transform;  
        points = spawnPointGroup?.GetComponentsInChildren<Transform>();
    }
}

배열은 삭제가 어렵기 때문에 동적으로 삭제가 가능한 List를 이용해 본다. List를 참조를 받아 오는게 아니라 참조를 전달한다.

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

public class GameManager : MonoBehaviour {
    // Start is called before the first frame update
    public List<Transform> points = new List<Transform>();
    void Start() {
        //null이 아니면 뒤를 실행
        Transform spawnPointGroup = GameObject.Find("SpawnPointGroup")?.transform;  
        spawnPointGroup?.GetComponentsInChildren<Transform>(points);  //points를 전달
    }
}

GetComponentsInChildren<Transform>(points);이 편하긴 하지만 Child뿐만이 아니라 Parent까지 추출한다. 인덱스0이 Parent이므로 1부터 쓰면되지만 다음과 같이 차일드만 추출하기도 한다..

        Transform spawnPointGroup = GameObject.Find("SpawnPointGroup")?.transform;  
        //spawnPointGroup?.GetComponentsInChildren<Transform>(points);  //points를 전달
        foreach(Transform point in spawnPointGroup) {
            points.Add(point);
        }

 

Invoke, InvokeRepeate 함수

일정한 시간 간격으로 몬스터를 불규칙한 위치에 생성하는 스크립트이다.

몬스터가 출연할 장소를 public List<Transform> points를 선언한다. spawnPointGroup에 Transform을 동적으로 연결하고 차일드Transform을 points에 add해준다. points는 몬스터를 만들때 위치벡터로만 사용된다.

몬스터를 연결하기 위해 public GameObject monster를 선언하고 인스펙터에서 연결한다.

CreateMonster()는 Instantiate()를 이용해 monster를 points[RandomIdx]위치에 찍어내는 함수다.

InvokeRepeate()에서 주기적으로 CrateMonster()를 불러  Monster를 만들게 한다. 

public class GameManager : MonoBehaviour
{

    public List<Transform> points = new List<Transform>();
    public GameObject monster;
    public float createTime = 5.0f;
    private bool isGameOver;  //게임종료 상태
    // 게임종료 프로퍼티ㄹ 체크용 메써드
    public bool IsGameOver {
        get { return isGameOver; }
        set { 
            isGameOver = value;
            if (isGameOver) {
                CancelInvoke("CreateMonster");  //몬스터 생산을 멈춘다.
            }
        }
    }
    // Start is called before the first frame update
    void Start() {
        //null이 아니면 뒤를 실행
        Transform spawnPointGroup = GameObject.Find("SpawnPointGroup")?.transform;  
        //spawnPointGroup?.GetComponentsInChildren<Transform>(points);  //points를 전달
        foreach(Transform point in spawnPointGroup) {
            points.Add(point);
        }
        InvokeRepeating("CreateMonster", 2.0f, createTime);  //2초후 반복적으로 몬스터를 만든다
    }
    // 불규칙한 위치에 몬스트를 만든다
    void CreateMonster() {
        int idx = Random.Range(0, points.Count);
        Instantiate(monster, points[idx].position, points[idx].rotation);
    }
}

IsGameOver() 함수는 Player를 죽었을때 상태를 변경해주고 몬스터의 반복 생성을 종료한다. 이 함수는 PlayerCtrl 스크립터에서 불리어진다. 참조시 GameManager 스크립터이름을 컴포넌트이름처럼 사용하여 접근한것이 재미있다.

    void PlayerDie() {
        //MONSTER 태그를 가진 모든 게임오브젝트를 찾아옴
        /*
        GameObject[] monsters = GameObject.FindGameObjectsWithTag("MONSTER");
        foreach(GameObject monster in monsters) {  //모든 오브젝트를 순차적으로 불러옴
            monster.SendMessage("OnPlayerDie", SendMessageOptions.DontRequireReceiver);
        }
        */
        OnPlayerDie();  //주인공 사망 이벤트 호출(발생)
        GameObject.Find("GameMgr").GetComponent<GameManager>().IsGameOver = true;  //적 생산 멈춤
    }

 

+ Recent posts