공부/인프런 - Rookiss

Part 3-13-7. 미니 RPG : Destory

셩잇님 2023. 9. 1. 14:00
반응형

 

 

미니 RPG

 

Destroy
오브젝트가 Destroy 될 때 오브젝트가 메모리 해제되면 유니티의 Object의 == 연산자 오버로딩으로 인하여 해제된 객체를 == null 비교 연산할 땐 그 객체를 참조하는 참조 변수들은 댕글링 포인터가 되는 것이 아닌 “null”이 되도록 처리된다. 따라서 해제된 오브젝트의 컴포넌트를 참조하는 것들을 잘 확인해야 한다. 크러쉬가 날테니까! 오브젝트가 해제되면 당연히 그 오브젝트의 컴포넌트들에도 참조할 수 없다.

 

형변환 연산 비용 줄이는 대안
📜GameManager 에서 오브젝트들을 스폰하고 디스폰하는 작업을 할 것이다. 근데 어떤 오브젝트를 스폰해야하는지를 알아야 한다. 그러면 📜BaseController 타입으로 업캐스팅해서 받는 방법도 있을테고(근데 이러면 상속이 아닌 자신만의 고유한 부분들은 호출 못하겠지..?) GetComponent로 📜PlayerController 혹은 📜MonsterController 가져오고 이런 방법도 있을 것이다. 

 

 그러나 이런 방법들은 비싼 연산들이다보니 그냥 📜Define 에 스폰할 오브젝트들을 종류별로 정리한 enum을 정의하고 이를 받아서 어떤 오브젝트를 스폰해야 하는지 알면 성능상 더 좋을 것이다. 또 enum으로 정의하면 switch를 사용하기에도 좋다.

 

📜Define

public class Define
{
    public enum WorldObject
    {
        Unknown,
        Player,
        Monster,
    }

 

 

Define.WorldObject.Player 이면 플레이어 오브젝트 스폰할 것

Define.WorldObject.Monster 이면 몬스터 오브젝트 스폰할 것

 

📜BaseController

public Define.WorldObject WorldObjectType { get; protected set; } = Define.WorldObject.Unknown;

📜PlayerController,📜MonsterController은 WorldObjectType을 가짐.

 

📜MonsterController

    public override void Init()
    {
        WorldObjectType = Define.WorldObject.Monster;
        _stat = gameObject.GetComponent<Stat>();

        if (gameObject.GetComponentInChildren<UI_HPBar>() == null)
            Managers.UI.MakeWorldSpace<UI_HPBar>(transform);
    }

 

WorldObjectType = Define.WorldObject.Monster;

 

📜PlayerController

	public override void Init()
    {
		WorldObjectType = Define.WorldObject.Player;
		_stat = gameObject.GetComponent<PlayerStat>();

WorldObjectType = Define.WorldObject.Player;

 

 


 

 

스폰 및 디스폰

📜GameManager

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

public class GameManager
{
    // int(ID) <-> GameObject : 게임 오브젝트를 ID로 들고 있기 
    // 따라서 Dictionary가 적당. 근데 이건 온라인 게임은 아니니까 일단 HashSet 사용(Key없는 자료구조)
    GameObject _player;
    HashSet<GameObject> _monsters = new HashSet<GameObject>();

    public GameObject GetPlayer() { return _player; } // 플레이어 리턴 함수

 

스폰된 오브젝트들은 ID 같은 것으로 관리되는게 좋다. (서버에서는 이 ID로 통신한다고 한다.) 이 얘기는 즉 Key와 Value로 관리가 되야 한다는 의미이므로 Dictionary가 적당하다. 그러나 지금은 온라인 게임 만드는건 아니니까 싱글 게임에선 딱히 필요는 없고 해서 HashSet 사용한다.

싱글 게임 이므로 플레이어는 1 명

플레이어 오브젝트 👉 1 개이므로 그냥 _player에서 관리

몬스터 오브젝트들 👉 HashSet에서 관리

 

HashSet

순서대로 저장되지 않는다.

중복이 허용되지 않는다.

 

    public GameObject Spawn(Define.WorldObject type, string path, Transform parent = null)
    {
        GameObject go = Managers.Resource.Instantiate(path, parent);

        switch(type)
        {
            case Define.WorldObject.Monster:
                _monsters.Add(go);
                break;
            case Define.WorldObject.Player:
                _player = go;
                break;
        }

        return go;
    }

 

스폰하기


오브젝트 Instantiate 👉 enum이여서 가능한 일

 

1. 몬스터면

_monsters HashSet 에 추가 캐싱

 

2. 플레이어면

_player에 캐싱

 

    // 오브젝트의 Define.WorldObject 값을 알려줌
    public Define.WorldObject GetWorldObjectType(GameObject go)
    {
        BaseController bc = go.GetComponent<BaseController>();
        if (bc == null)
            return Define.WorldObject.Unknown;
        return bc.WorldObjectType;
    }

    // 디스폰
    public void Despawn(GameObject go)
    {
        Define.WorldObject type = GetWorldObjectType(go);

        switch(type)
        {
            case Define.WorldObject.Monster:
                {
                    if (_monsters.Contains(go))
                        _monsters.Remove(go);
                }
                break;
            case Define.WorldObject.Player:
                {
                    if (_player == go)
                        _player = null;
                }     
                break;
        }

        Managers.Resource.Destroy(go);
    }
}

 

Destroy 시키기 전에, 캐싱해놨던거 없애기 null 처리 진행! 이는 그냥 Destroy가 아니라 Managers.Resource.Destroy(go) 이기 때문에! 예전에 풀링 매니저 때 구현해놨던 오브젝트 풀링 처리가 된다. 풀링하는 오브젝트면 진짜 Destroy 되는게 아닌 비활성화 된다.

 

📜Managers

GameManager _game = new GameManager();
public static GameManager Game { get { return Instance._game; } }

 

📜GameScene

public class GameScene : BaseScene
{
    protected override void Init()
    {
        //...
        GameObject player = Managers.Game.Spawn(Define.WorldObject.Player, "UnityChan");
        Camera.main.gameObject.GetOrAddComponent<CameraController>().SetPlayer(player);
        Managers.Game.Spawn(Define.WorldObject.Monster, "Knight");
    }

 

플레이어 및 몬스터 스폰하기

📜MonsterController, 📜PlayerController

    void OnHitEvent()
    {
        if (_lockTarget != null)
        {
            // 체력
            Stat targetStat = _lockTarget.GetComponent<Stat>();
            int damage = Mathf.Max(0, _stat.Attack - targetStat.Defense);
            targetStat.Hp -= damage;

            if (targetStat.Hp <= 0) 
            {
                Managers.Game.Despawn(targetStat.gameObject);
            }

 

체력이 0 이하면 디스폰


 

풀링으로 인한 비활성화 고려
📜Extension

public static class Extension
{
	public static bool IsValid(this GameObject go)
    {
		return go != null && go.activeSelf;
    }

 

풀링하는 오브젝트들도 있기 때문에 go != null만 체크해주면 안되고 활성화 상태인지도 체크해주어야 하기 때문에 이런 확장메서드를 만들었다.

 

📜CameraController

public void SetPlayer(GameObject player) { _player = player; }

    void LateUpdate()
    {
        if (_mode == Define.CameraMode.QuarterView)
        {
            if (_player.IsValid() == false) 
                return;

 

_player != null만 해주면 안되고 활성화 상태인지도 체크해야한다. 플레이어 오브젝트는 풀링으로 관리되기 때문!

Hp 깎기

공격 하는 주체가 직접 피해자가 자신의 HP를 깎는 함수를 호출하게 해주는 것이 좋다.

 

몬스터가 플레이어 공격할 때 👉 몬스터에서 플레이어 객체로부터 플레이어 본인의 HP 깎는 함수 호출

플레이어가 몬스터 공격할 때 👉 플레이어에서 몬스터 객체로부터 몬스터 본인의 HP 깎는 함수 호출

 

📜Stat

    public virtual void OnAttacked(Stat _attacker)
    {
        int damage = Mathf.Max(0, _attacker.Attack - Defense);
        Hp -= damage;
        if (Hp <= 0)
        {
            Hp = 0;
            OnDead();
        }
    }

    protected virtual void OnDead()
    {
        Managers.Game.Despawn(gameObject);
    }

 

HP 깎는 처리를 플레이어와 몬스터의 OnHitEvent 에서 하지 않고 📜Stat에서 해주기루..

📜MonsterController, 📜PlayerController

	void OnHitEvent()
	{
		if (_lockTarget != null)
        {
			Stat targetStat = _lockTarget.GetComponent<Stat>();
			targetStat.OnAttacked(_stat);
		}

 

_lockTarget 상대방의 📜Stat 의 OnAttacked 호출. 내가 _stat 공격 주체. targetStat은 피해자.

 

 

 

반응형