Chapter 13-9. 미니 RPG : Destroy

Date:     Updated:

카테고리:

태그:

인프런에 있는 Rookiss님의 [C#과 유니티로 만드는 MMORPG 게임 개발 시리즈] Part3: 유니티 엔진 강의를 듣고 정리한 필기입니다. 😀
🌜 강의 들으러 가기 Click

Chapter 13. 미니 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로 관리가 되야 한다는 의미이므로 Dictionaryr가 적당!

근데 온라인 게임 만드는건 아니니까 싱글 게임에선 딱히 필요는 없고 해서 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이여서 가능한 일
    • 몬스터면
      • _monsters HashSet 에 추가 캐싱
    • 플레이어면
      • _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은 피해자.



🌜 개인 공부 기록용 블로그입니다. 오류나 틀린 부분이 있을 경우 
언제든지 댓글 혹은 메일로 지적해주시면 감사하겠습니다! 😄

맨 위로 이동하기

Unity Lesson 2 카테고리 내 다른 글 보러가기

댓글 남기기