어제 임시로 만들어둔 아티팩트 클래스를 기획에 맞게 구현했다.
먼저, 프리팹과 데이터를 만들었다.
[CreateAssetMenu(fileName = "ArtifactData", menuName = "ScriptableObjects/Artifact")]
public class ArtifactData : ScriptableObject
{
[field: SerializeField] public Mesh[] Model { get; private set; }
[field: SerializeField] public float HPMax { get; private set; }
[field: SerializeField] public float HpRegen { get; private set; }
[field: SerializeField] public float InfluenceAmount { get; private set; }
[field: SerializeField] public ItemDropTable[] LootingData { get; private set; }
[field: SerializeField] public GameObject Prefab { get; private set; }
[field: SerializeField] public int CreateCount { get; private set; }
private Artifact _artifact;
public Artifact Artifact
{
get
{
if (_artifact == null)
_artifact = Prefab.GetComponent<Artifact>();
return _artifact;
}
}
}
프리팹은 하나만 쓰고, 생성할 때마다 모델을 바꾸는 구조로 하기 위해 Mesh 데이터는 배열로 선언해줬다.
마찬가지로 드랍될 아이템도 다르게 적용될 수 있으니 배열로 선언해줬다.
이제 생성될 때, 프리팹을 바꿔서 생성하도록 생성기 클래스를 변경했다.
public void TryCreate()
{
if (_manager.Season.IsIceIslandActive)
{
_data.Artifact.SetSharedMesh(_data.Model[1]);
_data.Artifact.SetDropTable(_data.LootingData[1]);
for (int i = 0; i < _data.CreateCount; i++)
CoroutineManagement.Instance.StartCoroutine(TryCreate(_manager.IceIsland));
}
else if (_manager.Season.IsFireIslandActive)
{
_data.Artifact.SetSharedMesh(_data.Model[0]);
_data.Artifact.SetDropTable(_data.LootingData[0]);
for (int i = 0; i < _data.CreateCount; i++)
CoroutineManagement.Instance.StartCoroutine(TryCreate(_manager.FireIsland));
}
}
생성될 섬에 알맞는 데이터로 프리팹을 수정하고 Instantiate하도록 했는데 생각한대로 잘 동작했다.
public class ArtifactHitbox : MonoBehaviour, IHit
{
[SerializeField] private Artifact _parent;
public Condition HP { get; private set; }
private IAttack _lastAttacker;
public void SetInfo(float HPMax, float HPRegen, float? HPCurrent)
{
HP = new(HPMax)
{
regenRate = HPRegen,
currentValue = HPCurrent ?? HPMax,
};
HP.OnBelowedToZero += () => { _parent.DestroyByAttack(_lastAttacker); };
}
public void Hit(IAttack attacker, float damage)
{
_lastAttacker = attacker;
HP.Subtract(damage);
}
}
아티팩트가 공격을 받을 수 있도록 ArtifactHitbox
를 추가하고 프리팹의 자식 오브젝트로 넣어줬다.
Condition
이라는 객체는 스탯을 관리하기 위한 객체다. 수치가 0이 됐을 때 호출되는 델리게이트가 있는데, 여기에 부모객체인 Artifact
가 공격에 의해 파괴되는 메서드를 추가했다.
public void DestroyByAttack(IAttack attacker)
{
if (attacker is Behaviour behaviour)
{
var player = behaviour.GetComponentInParent<Player>();
if (player != null)
_dropTable.AddInventory(player.Inventory);
}
DestroyArtifact();
}
public void DestroyByNature()
{
// TODO: Call MonsterWave
DestroyArtifact();
}
public void DestroyArtifact()
{
_season.OnSeasonChanged -= DestroyByNature;
OnDestroy?.Invoke(this);
ReduceInfluence();
Destroy(gameObject);
}
공격에 의해 파괴되면 아이템 드랍테이블에서 아이템을 루팅하도록 했다.
플레이어가 죽어서 게임오버가 되면 타이틀 씬으로 돌아간다.
여태까지는 테스트 목적으로만 게임을 돌려봐서 보통 게임오버를 하지않거나, 게임오버를 해도 게임을 종료했었다.
근데 게임 오버 이후 타이틀 씬으로 돌아온 다음 게임을 이어서 하면
버그가 발생했었다.
어떤 버그냐면, World
클래스가 게임을 시작하지 않았는데도 Update
문을 실행하고 있었고, Update문 내부에서 GameManager
의 Player
객체를 참조하는데, Player 객체가 missing object
였다.
private void Update()
{
if (!Managers.Game.IsRunning) return;
if (!_player)
_player = Managers.Game.Player.transform;
_currentPlayerCoord = ConvertChunkCoord(_player.position);
if (_prevPlayerCoord != _currentPlayerCoord)
UpdateChunksInViewRange();
_prevPlayerCoord = _currentPlayerCoord;
}
이전에 다른 팀원분께서 DayCycle
객체의 이벤트에서 겪은 문제가 바로 떠올랐다.
GameManager
와 DayCycle
모두 싱글톤 객체인 Managers
에게 종속돼있기 때문에 씬 전환 시 파괴된 객체를 그대로 가리키고 있다.
처음엔 Player
를 스폰할때 다시 연결해줬는데, 그래도 missing 에러가 계속 났다.
왜 다시 연결해줬는데도 에러가 날까? 생각해봤는데, 플레이어가 스폰되기도 전에 Update 문이 실행되고 있었다.
즉, Managers.Game.IsRunning
이 게임이 시작되지도 않았는데 true인 것이다.
따라서, 플레이어를 다시 연결해주는 방법이 아니라, 좀 더 근본적인 해결법을 찾아야했다.
private IEnumerator Start()
{
Managers.Game.Clear();
// 0. 로딩씬 UI open
...
// 1. 리소스 로드
...
// 2. 맵 생성
...
// 3. 객체 생성, 초기화
...
// 4. NavMesh 생성
...
// 5. 게임 시작
SpawnPlayer();
UIInitialize();
Managers.Game.Init();
}
public void Clear()
{
IsRunning = false;
Player = null;
OnSaveCallback = null;
}
메인 씬을 시작할 때, 게임매니저를 클리어해주도록 했다.