내일배움캠프 Unity 76일차 TIL - 팀 9와 4분의 3 - 개발일지

Wooooo·2024년 2월 14일
0

내일배움캠프Unity

목록 보기
78/94

[오늘의 키워드]

  1. 계절 클래스 작성
  2. 아티팩트 생성기 클래스 작성
  3. 아티팩트 컴포넌트
  4. 아티팩트 저장/불러오기

[계절 클래스 작성]

온도 시스템을 만들었으니 섬들의 온도를 바꾸기 위한 계절 클래스를 만들기로했다.

계절 클래스에 필요한 기능을 정리해봤다.

  • DayCycle 클래스와 통신하여 특정 날짜마다 계절이 바뀐다.
  • GameManager는 저녁마다 섬에 속성을 가진 몬스터를 소환하는데, 계절과 관련된 속성의 몬스터를 소환해야한다. 따라서 외부에서 현재 계절의 진행정도를 판단할 수 있는 메서드나 프로퍼티가 필요하다.
  • ArtifactCreator 역시 저녁마다 아티팩트를 생성하는데, 현재 계절에 따라 활성화된 섬을 찾아와야한다. 따라서 현재 계절이 어떤 계절일지 판단할 수 있는 메서드나 프로퍼티가 필요하다.

요약을 해보자면, 날짜가 변경될 때마다 계절의 상태를 갱신하고, 다른 객체가 계절의 상태를 알 수 있도록 해주는 클래스다.

Season.cs

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라는 싱글톤 클래스를 이용해서 코루틴을 실행해줬다.

ArtifactCreator.cs

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;
    }
}

[아티팩트 저장/불러오기]

아티팩트의 저장/불러오기 시 주의할 점을 알아냈으니 이를 반영해서 저장/불러오기 코드를 작성했다.

가장 먼저, 세이브데이터를 직렬화하기위한 구조체를 선언했다.

ArtifactSaveData

    [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 메서드를 오버로딩해서 게임 로직 진행 중 계절에 의해 생성된 아티팩트와 로드된 데이터로 생성된 아티팩트가 다르게 생성될 수 있도록 했다.

즉, 로드된 아티팩트가 중복으로 섬의 영향력을 높이지 않도록 했다.

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 메서드를 작성했다.

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();
    }

profile
game developer

0개의 댓글