본문 바로가기
3D 콘텐츠 제작/[위저드 히어로] 제작 일지

[위저드 히어로 03] 오브젝트 풀링으로 몬스터 생성 & 몬스터 레벨 적용

by 잰쟁 2023. 9. 27.
728x90

 

 

1. 몬스터 생성하기

 

에셋들에서 Monster로 사용할 프리팹을 하나 가져온 후

기존의 애니메이션을 필요한 부분만 골라 재생성해준 후 Animator에 넣어준다.

 

 

 

현재로는 걷는 동작만 필요하므로 Walk만 남겨두고 다 삭제

기존 애니메이터(AC_Sparrow)                                                          편집한 애니메이터(AC_Sparrow 1)

 

 

 

Monster(몬스터의 동작 관리) 스크립트를 작성후 컴포넌트로 부착

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

public class Monster : MonoBehaviour
{
    public float speed = 1f;
    [SerializeField]
    private Player player;

    void Awake()
    {
        this.player = this.GetComponent<Player>();
    }

    //Player를 향해 이동
    void Update()
    {
        //방향 설정
        Vector3 dir = this.player.transform.position - this.transform.position;
        //player 바라보기
        this.transform.LookAt(this.player.transform.position);
        //이동하기
        this.transform.Translate(Vector3.forward * this.speed * Time.deltaTime);
    }

    //몬스터가 마법진에 닿으면 사라지게 하기
    private void OnTriggerEnter(Collider other)
    {
        if (other.CompareTag("Weapon"))
        {
            Debug.Log("Destroy");
            //pool에 다시 넣어주기
            PoolManager.instance.Release(this.gameObject);
        }
    }
}

 

 


 

**오류 발생

 

이렇게 설정한 후 프리팹화 시켰더니...

 

프리팹화 하기 전에 잘만 인식하던 Player가 'None'이라고 나온다.

 

 

 

 

 

 

무슨 이유인가 하고 봤더니

.

.

.

 

prefab에 연결되는 객체는 반드시 프리팹 내부에 있는 오브젝트여야 함

 

즉, prefab은 다른 scene의 오브젝트와 달리 public GameObject를 선언해주어도

오브젝트를 외부에서 드래그 & 드랍으로 할당해줄 수 없다!

 

 

=> 따라서 GameManager스크립트를 생성해 싱글톤으로 만들어주고,

GameManager에 Player를 넣어주고 GameManager에서 가져오는 방식으로 스크립트를 수정해줬다.

 

 

GameManager 스크립트

수정한 Monster 스크립트

 


 

 

 

2. 한 가지 종류의 몬스터만 풀링으로 생성해보기

 

 

01. 풀링을 관리할 PoolManager 스크립트 생성

 

: 오브젝트 활성, 비활성화 ->몬스터 생성 및 제거를 관리할 것임.

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

public class PoolManager : MonoBehaviour
{
    //싱글톤
    public static PoolManager instance;

    [Header("#Prefabs")]
    [SerializeField]
    private GameObject monsterPrefab;
    //풀 리스트
    private List<GameObject> pool = new List<GameObject>();

    public int maxMonsters = 30;

    void Awake()
    {
        instance = this;
        DontDestroyOnLoad(gameObject);
    }

    void Start()
    {
        for(int i = 0; i < maxMonsters; i++)
        {
            //몬스터 프리팹(게임오브젝트) 생성
            GameObject monsterGo = Instantiate(this.monsterPrefab);
            //비활성화 시키기
            monsterGo.SetActive(false);
            //현재 transform을 부모로 설정
            monsterGo.transform.SetParent(this.transform);
            //pool List에 추가
            this.pool.Add(monsterGo);
        }
    }

    //monster 가져오기(pool에서 빼기)
    public GameObject GetMonster()
    {
        foreach(GameObject monsterGo in pool)
        {
            //monsterGo가 비활성화 되어있으면
            if(monsterGo.activeSelf == false)
            {
                //pool에서 monsterGo 꺼내기
                monsterGo.transform.SetParent(null);
                //몬스터 활성화
                monsterGo.SetActive(true);
                //monsterGo 반환
                return monsterGo;
            }
        }
        return null;
    }

    //풀에 반환하기
    public void Release(GameObject monsterGo)
    {
        monsterGo.SetActive(false);
        monsterGo.transform.SetParent(this.transform);
    }
}

 

 

 

02. 몬스터를 생성할 위치들(Point) 설정

 

: 추가한 Point들 중에 랜덤으로 몬스터를 일정시간이 지난 후(1초) 마다 생성하기

 

 

Player에 빈 오브젝트 'Spawn'을 추가하고 그 자식들로 'Point' 들을 추가

 

Spawn  스크립트 작성후 Spawn에 컴포넌트로 추가

using System.Collections;
using System.Collections.Generic;
using Unity.VisualScripting;
using UnityEngine;

public class Spawner : MonoBehaviour
{
    //Point들의 transform
    [Header("#Prefabs")]
    [SerializeField]
    private Transform[] pointTrans;

    private float timer;

    void Start()
    {
        //현재 transform의 자식으로 붙여주기
        this.pointTrans = this.GetComponentsInChildren<Transform>();
    }

    void Update()
    {
        //...1초마다 몬스터 생산하기

        //타이머 설정
        this.timer += Time.deltaTime;

        if (this.timer > 1f)
        {
            //타이머 초기화
            this.timer = 0f;
            this.Spawn();
        }
    }

    //...몬스터 생성 후 위치 할당 메서드
    public void Spawn()
    {
        //PoolManager에서 가져온 몬스터에 변수 할당
        GameObject monsterGo = GameManager.instance.pool.GetMonster();
        //가져온 몬스터의 위치를 pointTrans[]에서 랜덤 배치
        monsterGo.transform.position 
            = this.pointTrans[Random.Range(1, this.pointTrans.Length)].transform.position;
    }
}

 

 

실행 결과

 

 

 

 


 

 

3. 몬스터 레벨 적용하기

 

 

일정 시간에 따라 몬스터를 레벨별로 나누어 소환하기

 

이전 2D 프로젝트때와 마찬가지로 Animator를 변경하여 몬스터를 소환하려고 했으나...

 

 


 

(오류발생)

 

2D와 3D의 근본적인 차이를 간과하였다..

 

 

골드메탈님(2D) : Sprite로 Animation 생성 O, Animator 변경시 게임오브젝트의 Sprite도 변경 O 

현재 에셋(3D) : Sprite로 Animation 생성 X, Animator 변경시 게임오브젝트의 Sprite도 변경 X  

골드메탈님(2D)                                                                                  현재 에셋(3D)

 

 

현재 에셋(3D)의 경우 Animation이 Sprite로 구성되지 않아서

Animator를 바꿔준다고 해도 몬스터 게임오브젝트가 변하지 않는다.

 

 

따라서 몬스터를 종류별로 프리팹화 시킨 후 그것을 소환하는 방식으로 변경.

 

 

몬스터들을 각각 프리팹화 시킨 후 PoolManager에 넣어준다.

(**이 때 넣어준 순서 = 레벨 순서)

 

Spawner 스크립트에 아래와 같이 SpawnData를 추가하고 다음과 같이 설정해준다.

SpawnTime : 소환 시간(주기)

Type : 몬스터 타입

Health : 몬스터 체력

Speed : 몬스터 이동속도

 

 

스크립트 정리

 

 

Monster

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

public class Monster : MonoBehaviour
{
    public float speed = 1f;
    [SerializeField]
    private Player player;

    //...몬스터 정보
    private GameObject prefab;
    private float health;
    private float maxHealth;

    [SerializeField]
    private Bullet bullet;
    private Animator anim;

    void Awake()
    {
        this.anim = this.GetComponent<Animator>();
    }

    //Player를 향해 이동
    void Update()
    {
        //방향 설정
        Vector3 dir = this.player.transform.position - this.transform.position;
        //player 바라보기
        this.transform.LookAt(this.player.transform.position);
        //이동하기
        this.transform.Translate(Vector3.forward * this.speed * Time.deltaTime);
    }


    private void OnTriggerEnter(Collider other)
    {
        if (!other.CompareTag("Weapon"))
        {
            return;
        }

        this.health -= this.bullet.damage;
        
        if(health > 0)
        {

        }
        else
        {
            //죽음
            Debug.Log("Destroy");
            //코루틴 실행
            this.StartCoroutine(CoDie());
        }
    }

    private void OnEnable()
    {
        this.player = GameManager.instance.player.GetComponent<Player>();
        this.health = maxHealth;
        this.anim.SetBool("Dead", false);
    }

    //초기화
    public void Init(SpawnData data)
    {
        this.speed = data.speed;
        this.maxHealth = data.health;
        this.health = data.health;
    }

    //죽음 코루틴
    IEnumerator CoDie()
    {
        this.anim.SetBool("Dead", true);

        yield return new WaitForSecondsRealtime(0.3f);

        //pool에 다시 넣어주기
        PoolManager.instance.Release(this.gameObject);
    }
}

 

PoolManager

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

public class PoolManager : MonoBehaviour
{
    //싱글톤
    public static PoolManager instance;

    [Header("#Prefabs")]
    [SerializeField]
    private GameObject[] prefabs;
    //pool 리스트(각 프리팹 종류에 따라 관리)
    private List<GameObject>[] pools;

    public int maxMonsters = 30;

    void Awake()
    {
        instance = this;
        //프리팹 종류만큼 List생성
        this.pools = new List<GameObject>[prefabs.Length];
        
        //for문 돌려서 배열 안의 각각의 List들 초기화
        for(int i = 0; i < pools.Length; i++)
        {
            //List 초기화
            pools[i] = new List<GameObject>();
        }
    }

    //monster 가져오기(pool에서 빼기)
    public GameObject GetMonster(int i)
    {
        //...게임오브젝트 중에 선택
        GameObject select = null;  //일단 비워둠

        //...선택한 pool의 놀고 있는(비활성화된) 게임오브젝트 접근
        foreach(GameObject monster in pools[i])
        {
            //...비활성화면
            if (!monster.activeSelf)
            {
                //...발견하면 --> select 변수에 할당후 활성화
                select = monster;
                select.SetActive(true);
                break;
            }
        }
        //...못 찾았으면(전부 활성화상태면)
        if (!select)
        {
            select = Instantiate(prefabs[i], transform);
            pools[i].Add(select);
        }
        return select;
    }

    //풀에 반환하기
    public void Release(GameObject monsterGo)
    {
        monsterGo.SetActive(false);
        monsterGo.transform.SetParent(this.transform);
    }
}

 

Spawner

using System.Collections;
using System.Collections.Generic;
using Unity.VisualScripting;
using UnityEngine;


public class Spawner : MonoBehaviour
{
    //Point들의 transform
    [Header("#Prefabs")]
    //spawnPoint 배열
    [SerializeField]
    private Transform[] pointTrans;
    //spawnData 배열
    public SpawnData[] spawnData;

    //..타이머
    private float timer;
    //..레벨
    private int level;

    void Start()
    {
        //현재 transform의 자식으로 붙여주기
        this.pointTrans = this.GetComponentsInChildren<Transform>();
    }

    void Update()
    {
        //타이머 설정
        this.timer += Time.deltaTime;

        //레벨 : 게임시간/10
        //level -> int 형식, 나눈 나머지를 버리고 몫만 int로 변환
        this.level = Mathf.FloorToInt(GameManager.instance.gameTime / 10f);

        //spawnData[]에서 레벨에 해당하는 spawnTime이 되면 몬스터 소환
        if(timer > spawnData[level].spawnTime)
        {
            timer = 0f;
            this.Spawn();
        }
    }

    //몬스터 생성 후 위치 할당 메서드
    public void Spawn()
    {
        //pool들 중에서 level에 따라 몬스터 호출
        GameObject monster = GameManager.instance.pool.GetMonster(level);
        //가져온 몬스터의 위치를 pointTrans[]에서 랜덤 배치
        monster.transform.position 
            = this.pointTrans[Random.Range(1, this.pointTrans.Length)].transform.position;
        Debug.LogFormat("<color=yellow>level : {0}</color>", level);
        //몬스터 호출 및 초기화
        monster.GetComponent<Monster>().Init(spawnData[level]);
    }
}

[System.Serializable]
//...직렬화 해주기 - 개체 저장 또는 전송 가능
//하나의 스크립트 내에 여러개의 클래스 선언 가능
public class SpawnData
{
    public float spawnTime;
    public int Type;
    public int health;
    public float speed;
}