본문 바로가기
2D 콘텐츠 제작/[언데드 서바이벌] 제작 일지

[언데드 서바이벌 03] 오브젝트 풀링으로 몬스터 생성 & 몬스터 레벨 적용

by 잰쟁 2023. 9. 15.
728x90

 

1. 오브젝트 풀링으로 몬스터 생성

 

오브젝트 풀링을 사용하는 이유?

: Instantiate 와 Destory를 자주 사용하면 메모리에 문제가 생길 수 있음

따라서 오브젝트들을 미리 생성해놓고 활성/비활성을 통해 게임신에 노출하는 것을 조절하자

 

 

01. 목표

Player 주변에 설정해둔 Point들 中 랜덤으로 1곳에서 0.2초 마다 3가지 몬스터 中 하나를 랜덤으로 생성하기

 

 

설정해둔 Point(몬스터 생성 위치) 들

 

몬스터 종류 3가지 - PoolManager에 넣어주기

 

[스크립트]

 

PoolManager

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

public class PoolManager : MonoBehaviour
{
    //프리팹 보관할 변수
    public GameObject[] prefabs;

    //pool 담당하는 리스트들
    //prefab 종류에 따라 리스트 개수 생성 -> List도 배열화
    List<GameObject>[] pools;

    void Awake()
    {
        //List[] 초기화
        pools = new List<GameObject>[prefabs.Length];

        //for문 돌려서 배열 안의 각각의 List들 초기화
        for(int i = 0;i< pools.Length; i++)
        {
            //List 초기화
            pools[i] = new List<GameObject>();
        }
        Debug.Log(pools.Length);
    }
    //pool 안의 게임오브젝트(item) 반환(가져오는) 메서드
    public GameObject Get(int i)
    {
        //게임오브젝트 중에 '선택'
        GameObject select = null;  //일단 비워둠

        //... 선택한 pool의 놀고있는(비활성화된) 게임오브젝트 접근
        foreach (GameObject item in pools[i])
        {
            //...비활성화 되었으면
            if(!item.activeSelf)
            {
                //... 발견하면 -> select 변수에 할당 후 활성화
                select = item;
                select.SetActive(true);
                break;
            }
        }

        //... 못 찾았으면?
        if(!select)
        {
            //...새롭게 생성하고 -> select 변수에 할당
            select = Instantiate(prefabs[i],transform);
            pools[i].Add(select);
        }

       return select;    //선택한 것을 반환
    }
}

 

Reposition

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

public class Reposition : MonoBehaviour
{
    //모든 class의 collider2D를 아우름
    private Collider2D coll;

    private void Awake()
    {
        this.coll = this.GetComponent<Collider2D>();
    }

    //Trigger가 체크된 Collier에서 나갔을 때 
    void OnTriggerExit2D(Collider2D collision)
    {

        if (!collision.CompareTag("Area"))
        {
            return;
        }

        //Player 포지션
        Vector3 playerPos = GameManager.instance.player.transform.position;
        //타일맵 포지션
        Vector3 myPos = this.transform.position;
        //둘 사이의 거리 측정(x,y), 절대값으로 반환
        float disX = Mathf.Abs(playerPos.x - myPos.x);
        float disY = Mathf.Abs(playerPos.y - myPos.y);

        //Player 방향(x,y)
        Vector3 playerDir = GameManager.instance.player.inputVec;
        //대각선일 때는 nomalized에 의해 1보다 값이 작아짐
        //연산자 (조건) ? (true일 때 값) : (false일 때 값)
        float dirX = playerDir.x < 0 ? -1 : 1;
        float dirY = playerDir.y < 0 ? -1 : 1;

        switch (transform.tag)
        {
            case "Ground":
                //좌우로 이동하면
                if (disX > disY)
                {
                    //dirX 방향으로 40칸 건너서 이동
                    this.transform.Translate(Vector3.right * dirX * 40);
                }
                //상하로 이동하면
                else if (disX < disY)
                {
                    //dirY 방향으로 40칸 건너서 이동
                    this.transform.Translate(Vector3.up * dirY * 40);
                }
                break;
            case "Enemy":
                //몬스터가 살아있으면
                if (coll.enabled)
                {
                    //Player 맞은편에서 몬스터 생성(화면 밖)
                    //화면 크기인 20 떨어진 거리에서 +-3만큼 랜덤으로 생성
                    transform.Translate(playerDir * 20 + new Vector3(Random.Range(-3f,3f),Random.Range(-3f,3f),0));
                }
                break;
        }
    }
}

GameManager

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

public class GameManager : MonoBehaviour
{
    public Player player;
    public PoolManager pool;

    //바로 메모리에 저장
    public static GameManager instance;

    void Awake()
    {
        instance = this;
    }
}

Spawner

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

public class Spawner : MonoBehaviour
{
    public Transform[] spawnPoint;
    float timer;
    int level;

    void Start()
    {
        this.spawnPoint = this.GetComponentsInChildren<Transform>();    
    }
    void Update()
    {

        timer += Time.deltaTime;
        
		//0.2초 후 마다 몬스터 생성하기
        if (timer > 0.2f)
        {
            timer = 0f;
            this.Spawn();
        }
    }

    void Spawn()
    {
        //pool들 중에 랜덤으로 가져옴
        GameObject enemy = GameManager.instance.pool.Get(Random.Range(0,3));

        //가져온 pool을 spawnPoint들 중에 랜덤으로 배치
        enemy.transform.position 
            = this.spawnPoint[Random.Range(1, this.spawnPoint.Length)].transform.position;
    }
}

Monster

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

public class Monster : MonoBehaviour
{
    [SerializeField]
    private Player player;
    private float speed =3f;
    private Rigidbody2D rBody;
    private int dirX;
    private Animator anim;
    private bool isLive = true;


    void Start()
    {
        this.rBody = this.GetComponent<Rigidbody2D>();
        this.anim = this.GetComponent<Animator>();
        this.player = GameManager.instance.player.GetComponent<Player>();
    }

    void Update()
    {
        if (!this.isLive)
        {
            return;
        }

        Vector3 dir = this.player.transform.position - this.transform.position;
        this.transform.Translate(dir.normalized * this.speed * Time.deltaTime);
        rBody.velocity = Vector2.zero;

        //바라보는 방향 설정    
        if (dir.x < 0)
        {
            dirX = -1;  //왼쪽
        }
        else if (dir.x > 1)
        {
            dirX = 1;  //오른쪽
        }

        //방향전환
        if (dir != Vector3.zero)
        {
            this.transform.localScale = new Vector3(dirX, 1, 1);
        }
        
    }

    private void OnCollisionEnter2D(Collision2D collision)
    {
        //"Player" 테그를 붙인 게임오브젝트와 충돌하면
        if (collision.collider.CompareTag("Player"))
        {
            //현재 게임오브젝트(몬스터)를 비활성화
            this.gameObject.SetActive(false);
        }
    }
}

결과

 

 


 

 

2. 몬스터 레벨 적용

 

01. 목표

: 플레이 시간에 따라 몬스터 종류를 구분하여 소환하기

 

 

 

 

 

결과

-시간에 따라 다른 종류의 몬스터 등장

 

 


 

02. 목표

 

: 몬스터 별로 레벨 정보(플레이 시간, 체력, 속도 등)를 다르게 하여

플레이 시간에 따라 해당되는 레벨의 몬스터 소환하기

 

 

※ 하나의 Script 내에 여러개의 Class 선언 가능!

=> 'Spawner' Script 안에 SpawnData 클래스 추가하기

 

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

public class Spawner : MonoBehaviour
{
    //spawnPoint 배열
    public Transform[] spawnPoint;
    //spawnData 배열
    public SpawnData[] spawnData;

    float timer;
    int level;

   //...중략

    void Spawn()   
    {
        //pool들 중에 level에 따라 몬스터 호출
        GameObject monster = GameManager.instance.pool.Get(0);

        //가져온 pool을 spawnPoint들 중에 랜덤으로 배치
        monster.transform.position 
            = this.spawnPoint[Random.Range(1, this.spawnPoint.Length)].transform.position;
        Debug.LogFormat("level : {0}", level);
        //몬스터 호출 및 초기화
        monster.GetComponent<Monster>().Init(spawnData[level]); //
    }
}

[System.Serializable]  //직렬화 해주기 - 개체 저장 혹은 전송 가능
//하나의 스크립트 내에 여러개의 클래스 선언 가능
public class SpawnData
{
    //소환시간, 스프라이트 타입, 체력, 속도
    public float spawnTime;
    public int spriteType;
    public int health;
    public float speed;
}

 

- 몬스터 종류(3가지) 별로 SpawnData 설정하기

 

- Monster 스크립트의 animCon에 몬스터 종류별 AnimatorController 넣어주기

 

 

나머지 Script 들도 잘 작성했는데....

 

오류 발생...

 

시간에 따라 몬스터 종류도 다르게 잘 나오는데 자꾸 아래와 같은 오류문구가 나왔다.

 

스크립트를 봐도 딱히 문제가 없는 것 같아 꽤 긴 시간 고민을 했다.

 

문득 해당 유튜브 댓글에 나와같은 문제를 겪는 분이 있지 않을까 해서 찾아본 결과

나와 비슷한 분들이 꽤 있었고 해결 방법을 찾을 수 있었다!

 

문제 발생 이유와 해결방법

 

Monster 스크립트에 Start보다 Init이 더 빨리 호출돼서 문제가 발생한 것이다..

그래서 Monster 스크립트의 Start와 Monster 스크립트의 Init을 호출하고 있는 Spawner 스크립트 에 Log를 찍어보았더니

 

 

실제로 Monster 스크립트의 Start보다 Init이 더 먼저 호출되고 있었다.

아직 Monster에 컴포넌트 부착이 안 되어있는 상태에서 Init을 호출하고 있으니 문제가 발생했던 것이라고 생각한다.

 

Monster 스크립트의 Start -> Awake 변경

 

Monster 스크립트의 Start -> Awake로 변경한 결과 아래와 같이 몬스터도 잘 호출되고 오류문도 없어졌다!

 

결과

레벨에 따라 다른 종류의 Monster 호출되는 모습

 

 

후련~

 

+느낀점
: 새롭게 무언가를 할 때 마다 문제는 매번 발생하고 그게 참 답답하고 그렇다.

'개발자' 는 문제를 해결하는 사람이라고 선생님께서 말씀해주셨는데 이제는 이런 문제가 발생하는 것은 일상이라고 생각하고 적응하며 문제 해결을 위해 검색하는 능력과 생각하는 능력을 더 키워야겠다고 생각했다..