온도 시스템을 만들었으니 섬들의 온도를 바꾸기 위한 계절 클래스를 만들기로했다.
계절 클래스에 필요한 기능을 정리해봤다.
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();
}