미니 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은 피해자.
'공부 > 인프런 - Rookiss' 카테고리의 다른 글
Part 3-13-9. 미니 RPG : 몬스터 자동 생성, 完 (0) | 2023.09.02 |
---|---|
Part 3-13-8. 미니 RPG : 레벨업 (0) | 2023.09.02 |
Part 3-13-6. 미니 RPG : 몬스터 AI (0) | 2023.09.01 |
Part 3-13-5. 미니 RPG : 체력 게이지 (0) | 2023.08.31 |
Part 3-13-4. 미니 RPG : 공격 (0) | 2023.08.31 |