Chapter 13-9. 미니 RPG : Destroy
카테고리: Unity Lesson 2
태그: Unity Game Engine
인프런에 있는 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
에서 관리
- 플레이어 오브젝트 👉 1 개이므로 그냥
- 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 되는게 아닌 비활성화 됨!
- 그냥 Destroy가 아니라 Managers.Resource.Destroy(go) 이기 때문에! 예전에 풀링 매니저 때 구현해놨던 오브젝트 풀링 처리가 된다.
📜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
은 피해자.
🌜 개인 공부 기록용 블로그입니다. 오류나 틀린 부분이 있을 경우
언제든지 댓글 혹은 메일로 지적해주시면 감사하겠습니다! 😄
댓글 남기기