온도 시스템을 만들었으니 섬들의 온도를 바꾸기 위한 계절 클래스를 만들기로했다.
계절 클래스에 필요한 기능을 정리해봤다.
DayCycle 클래스와 통신하여 특정 날짜마다 계절이 바뀐다.GameManager는 저녁마다 섬에 속성을 가진 몬스터를 소환하는데, 계절과 관련된 속성의 몬스터를 소환해야한다. 따라서 외부에서 현재 계절의 진행정도를 판단할 수 있는 메서드나 프로퍼티가 필요하다.ArtifactCreator 역시 저녁마다 아티팩트를 생성하는데, 현재 계절에 따라 활성화된 섬을 찾아와야한다. 따라서 현재 계절이 어떤 계절일지 판단할 수 있는 메서드나 프로퍼티가 필요하다.요약을 해보자면, 날짜가 변경될 때마다 계절의 상태를 갱신하고, 다른 객체가 계절의 상태를 알 수 있도록 해주는 클래스다.
using System;
public class Season
{
private int _lastState;
private float _value;
public SeasonData Data { get; private set; }
public event Action OnSeasonChanged;
public float CurrentValue => _value;
public bool IsRestPeriod => !IsFireIslandActive && !IsIceIslandActive;
public bool IsFireIslandActive => _value >= Data.FireIslandActivateThreshold;
public bool IsIceIslandActive => _value <= Data.IceIslandActivateThreshold;
public void Initialize(int date)
{
Data = Managers.Resource.GetCache<SeasonData>("SeasonData.data");
Update(date);
}
public void Update(int date)
{
_value = SetValue(date);
int currentState = SetState();
if (_lastState != currentState)
{
_lastState = currentState;
OnSeasonChanged?.Invoke();
}
}
private int SetState()
{
int state;
if (IsFireIslandActive)
state = 1;
else if (IsIceIslandActive)
state = 2;
else
state = 0;
return state;
}
private float SetValue(int date)
{
float t = ((float)date % Data.CycleValue) / Data.CycleValue;
return Data.SeasonCurve.Evaluate(t);
}
}
이후에, 섬에 메테오나 블리자드가 떨어지는 재난 비슷한 걸 넣을 예정인데, 재난은 계절이 바뀔 때마다 켜지거나, 꺼지거나 해야한다. 이때 사용할 이벤트로 OnSeasonChanged를 만들어놨다.
아티팩트의 생성은 DayCycle에서 매일 저녁에 생성하도록 할 것이기 때문에, MonoBehaviour의 상속 없이 구현했다.
단, 유효한 생성좌표를 찾을 때까지 매 프레임 코루틴을 돌도록 했는데, MonoBehaviour를 상속받지 않았기 때문에 CoroutineManagement라는 싱글톤 클래스를 이용해서 코루틴을 실행해줬다.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class ArtifactCreator
{
private Transform _root;
private GameManager _manager;
public HashSet<Artifact> Artifacts { get; private set; }
public ArtifactCreator(GameManager manager)
{
_root = new GameObject("Artifact Root").transform;
_manager = manager;
_manager.DayCycle.OnEveningCame += TryCreate;
Artifacts = new();
}
public void TryCreate()
{
if (_manager.Season.IsIceIslandActive)
CoroutineManagement.Instance.StartCoroutine(TryCreate(_manager.IceIsland));
else if (_manager.Season.IsFireIslandActive)
CoroutineManagement.Instance.StartCoroutine(TryCreate(_manager.FireIsland));
}
private IEnumerator TryCreate(Island island)
{
Vector3 pos;
var property = island.Property;
int minX = (int)property.center.x - property.diameter / 2;
int maxX = (int)property.center.x + property.diameter / 2;
int minZ = (int)property.center.z - property.diameter / 2;
int maxZ = (int)property.center.z + property.diameter / 2;
while (true)
{
int x = Random.Range(minX, maxX);
int z = Random.Range(minZ, maxZ);
pos = new Vector3(x, z);
if (IsValidPosition(ref pos))
break;
yield return null;
}
Create(pos, island);
}
public bool IsValidPosition(ref Vector3 pos)
{
pos += Vector3.up * 50f;
if (Physics.Raycast(pos, Vector3.down, out var hit, 100f, int.MaxValue, QueryTriggerInteraction.Collide))
{
pos = hit.point;
return hit.collider.gameObject.layer == 12;
}
else
return false;
}
public void Create(Vector3 spawnPosition, Island island)
{
var obj = GameObject.CreatePrimitive(PrimitiveType.Cube).AddComponent<Artifact>();
obj.SetInfo(island, false, spawnPosition, _root);
obj.SetActive(true);
Artifacts.Add(obj);
}
아티팩트 생성기의 생성 로직을 짰으니, 아티팩트를 간단하게 생성해서 생성이 잘 되는지 확인하기로 했다.
임시 테스트용으로, 상호작용을 해서 활성화된 아티팩트를 비활성화 할 수 있도록 했다.
활성화, 비활성화 상태를 알아보기 위해서 되게 볼품없는 코루틴 연출도 넣어놨다.
설계에서 가장 신경 쓴 부분은, SetInfo, SetActive, Set~~~ 메서드들이다.
생성기 클래스에서 생성 시, SetInfo를 통해서 아티팩트의 정보를 초기화 할 수 있게 해놨다.
또, 코드를 보면 알 수 있듯이 SetActive를 하면 모든 Set~~~ 메서드들을 같이 호출한다.
하지만 다른 Set~~~ 메서드들도 public으로 설정해서 외부에서 호출할 수 있게 했다.
이렇게 한 이유는, 게임이 저장될 때, 아티팩트에 의해 상승한 섬의 영향력도 저장되고, 생성돼있는 아티팩트도 저장된다.
불러오기 시 이미 섬의 영향력은 이미 이전 게임에서 아티팩트에 의해 상승돼있는데, 불러오면서 생성한 아티팩트로 인해 또 영향력이 상승하면 안되기때문에, 따로 불러내서 설정할 수 있도록 해봤다.
using System.Collections;
using UnityEngine;
public class Artifact : MonoBehaviour, IInteractable
{
private Island _island;
private Vector3 _originPos;
private Vector3 _activePos;
private Coroutine _animateCoroutine;
private bool _isActive;
public bool IsActive => _isActive;
public string IslandName => _island.Property.name;
public Vector3 OriginPos => _originPos;
public void SetInfo(Island island, bool active, Vector3 pos, Transform root)
{
_island = island;
gameObject.tag = "Gather";
transform.position = pos;
_originPos = pos;
_activePos = _originPos + Vector3.up;
transform.parent = root;
_isActive = active;
SetManagementedObject();
}
public void SetActive(bool active)
{
if (_isActive == active)
return;
_isActive = active;
SetState();
SetInfluence();
SetLayer();
}
public void SetLayer()
{
if (_isActive)
gameObject.layer = LayerMask.NameToLayer("Resources");
else
gameObject.layer = LayerMask.NameToLayer("Default");
}
public void SetInfluence()
{
if (_isActive)
_island.Influence += 0.1f;
else
_island.Influence -= 0.1f;
}
public void SetState()
{
if (_animateCoroutine != null)
StopCoroutine(_animateCoroutine);
if (_isActive)
_animateCoroutine = StartCoroutine(Animate(transform.position, _activePos));
else
_animateCoroutine = StartCoroutine(Animate(transform.position, _originPos));
}
private void SetManagementedObject()
{
var manage = gameObject.GetOrAddComponent<ManagementedObject>();
manage.Add(GetComponent<Collider>(), typeof(Collider));
manage.Add(GetComponent<Renderer>(), typeof(Renderer));
}
public void Interact(Player player)
{
SetActive(false);
}
private IEnumerator Animate(Vector3 fromPos, Vector3 toPos)
{
for (float t = 0f; t < 0.5f; t += Time.deltaTime)
{
transform.position = Vector3.Lerp(fromPos, toPos, t / 0.5f);
yield return null;
}
transform.position = toPos;
_animateCoroutine = null;
}
}
아티팩트의 저장/불러오기 시 주의할 점을 알아냈으니 이를 반영해서 저장/불러오기 코드를 작성했다.
가장 먼저, 세이브데이터를 직렬화하기위한 구조체를 선언했다.
[System.Serializable]
public struct ArtifactSaveData
{
public Vector3 pos;
public bool isActive;
public string islandName;
}
public struct ArtifactSaveDataList
{
public List<ArtifactSaveData> list;
}
public ArtifactSaveDataList saveData;
그 다음은, Create 메서드를 오버로딩해서 게임 로직 진행 중 계절에 의해 생성된 아티팩트와 로드된 데이터로 생성된 아티팩트가 다르게 생성될 수 있도록 했다.
즉, 로드된 아티팩트가 중복으로 섬의 영향력을 높이지 않도록 했다.
public void Create(Vector3 spawnPosition, Island island)
{
var obj = GameObject.CreatePrimitive(PrimitiveType.Cube).AddComponent<Artifact>();
obj.SetInfo(island, false, spawnPosition, _root);
obj.SetActive(true);
Artifacts.Add(obj);
}
public void Create(ArtifactSaveData loadData)
{
Island island;
if (loadData.islandName == "IceIsland")
island = _manager.IceIsland;
else if (loadData.islandName == "FireIsland")
island = _manager.FireIsland;
else
return;
var obj = GameObject.CreatePrimitive(PrimitiveType.Cube).AddComponent<Artifact>();
obj.SetInfo(island, loadData.isActive, loadData.pos, _root);
obj.SetState();
obj.SetLayer();
Artifacts.Add(obj);
}
그 다음은 Save/Load 메서드를 작성했다.
public void Save()
{
saveData.list = new();
foreach (var artifact in Artifacts)
{
ArtifactSaveData data = new()
{
pos = artifact.OriginPos,
isActive = artifact.IsActive,
islandName = artifact.IslandName,
};
saveData.list.Add(data);
}
string json = JsonUtility.ToJson(saveData);
SaveGame.CreateJsonFile("Artifacts", json, SaveGame.SaveType.Runtime);
saveData.list = null;
}
public void Load()
{
SaveGame.TryLoadJsonFile(SaveGame.SaveType.Runtime, "Artifacts", out saveData);
foreach (var data in saveData.list)
Create(data);
}
마지막으로, 생성자에서 Save/Load를 할 수 있도록 해줬다.
public ArtifactCreator(GameManager manager)
{
...
manager.OnSaveCallback += Save;
Load();
}