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

Wooooo·2024년 1월 26일
0

내일배움캠프Unity

목록 보기
67/94

[오늘의 키워드]

  1. 프로그래머스 - 다리를 지나는 트럭 (Lv.2)
  2. 자원 오브젝트 리팩토링
  3. 아이템 드랍 테이블 클래스

1. [프로그래머스 - 다리를 지나는 트럭 (Lv.2)]

옛날에 한 번 C++로 풀어봤던 문제다.

근데 다시 보니까, 큐를 써놓고 다 꺼냈다가 다시 집어넣고,, O(n^2)로 풀어놨다.

푸는 방법이야 여러 방법이 있겠지만, 내가 생각한 가장 적은 연산횟수로 푸는 방법은 다음과 같다.

  1. 트럭이 다리를 빠져나간 시간이 아니라, 트럭이 다리 위에 올라탄 시간을 기록한다.
  2. 제일 마지막 트럭이 다리 위에 올라탄 시간 + 다리의 길이를 하면 정답이 나온다.

코드

using System;
using System.Collections.Generic;

public class Solution
{
    public int solution(int bridge_length, int weight, int[] truck_weights)
    {
        Queue<int> queue = new Queue<int>();
        int[] start = new int[truck_weights.Length];
        int time = 0;
        int currentWeight = 0;
        int count = 0;
        while (count < truck_weights.Length)
        {
            if (queue.Count == 0 || weight - currentWeight >= truck_weights[count])
            {
                time++;
                start[count] = time;
                queue.Enqueue(count);
                currentWeight += truck_weights[count];
                count++;
            }
            else
            {
                int front = queue.Dequeue();
                time = Math.Max(start[front] + bridge_length - 1, time);
                currentWeight -= truck_weights[front];
            }
        }
        return start[truck_weights.Length - 1] + bridge_length;
    }
}

다리에서 트럭이 빠져나갈 때, 그 시간은 트럭이 다리에 올라온 시간 + 다리의 길이다.

문제는 다리에 트럭이 줄줄이 올라온 경우에는 큐의 제일 앞에 있는 트럭이 빠져나간 시간이 큐에 제일 마지막으로 트럭이 들어온 시간보다 빨라서 시간이 과거로 돌아갈 수도 있다.

따라서 제일 앞 트럭이 빠져나간 시간과 현재 시간 중 최대값으로 지정해줘야한다.


2. [자원 오브젝트 리팩토링]

자원 오브젝트를 Save/Load 해야한다.
새로 생성된 오브젝트가 있다면 어떤 오브젝트인지, 어떤 위치에 있는지, 어떤 상태인지를 데이터로 뽑을 수 있어야한다.

하지만 내가 작업해둔 현재 구조는 데이터가 따로 놀고 있어서, 데이터를 하나의 객체로 몰빵할 필요가 있었다. (MVC)

저장할 때 필요한 값은 다음과 같다.

  1. 오브젝트의 이름이나 ID
  2. 오브젝트의 위치
  3. 자원 재생성까지 남은 시간
  4. 자원의 현재 상태

이 4가지 값을 ResourceObjectParent에서 받아올 수 있는 구조로 리팩토링했다.

기존에는 ResourceObjectDebris 클래스에서 재생성까지 남은 시간을 가지고 있었는데, 이 값을 ResourceObjectParent에서 관리할 수 있도록 변경했다.

public class ResourceObjectDebris : ResourceObjectBase
{
    [SerializeField] private float _respawnTime;
    public float RespawnTime => _respawnTime;


    public void Respawn()
    {
        _parent.SwitchState(_toObjectID);
    }
}
public class ResourceObjectParent : MonoBehaviour
{
    private Dictionary<int, ResourceObjectGathering> _gatherings = new();
    private Dictionary<int, ResourceObjectDebris> _debris = new();
    private Dictionary<int, GameObject> _objects = new();

    public int CurrentState { get; private set; }
    public float RemainingTime { get; private set; }

    private bool _isInitialized = false;

    private void Start()
    {
        Initialize();
    }

    public void Update()
    {
        if (_debris.TryGetValue(CurrentState, out var debris))
        {
            RemainingTime -= Time.deltaTime;
            if (RemainingTime <= 0)
                debris.Respawn();
        }
    }

    private void Initialize()
    {
        if (_isInitialized) return;

        for (int i = 0; i < transform.childCount; i++)
        {
            var child = transform.GetChild(i);
            if (child.TryGetComponent<ResourceObjectGathering>(out var gathering))
            {
                _gatherings.TryAdd(i, gathering);
                gathering.Initialize();
                _objects.TryAdd(i, child.gameObject);
            }
            if (child.TryGetComponent<ResourceObjectDebris>(out var debris))
            {
                _debris.TryAdd(i, debris);
                debris.Initialize();
                _objects.TryAdd(i, child.gameObject);
            }
        }
        CurrentState = 0;
        RemainingTime = GetCurrentDebrisRespawnTime();

        _isInitialized = true;
    }

    public void SwitchState(int stateID)
    {
        // -1을 전달받을 경우 삭제
        if (stateID == -1)
        {
            Destroy(gameObject);
            return;
        }

        foreach (var obj in _objects.Values)
            obj.SetActive(false);

        CurrentState = stateID;
        _objects[CurrentState].SetActive(true);
        RemainingTime = GetCurrentDebrisRespawnTime();
    }

    public float GetCurrentDebrisRespawnTime()
    {
        if (_debris.TryGetValue(CurrentState, out var debris))
            return debris.RespawnTime;
        else return 0;
    }

    public void SetInfo(int stateID, float remainingTime)
    {
        Initialize();
        SwitchState(stateID);
        RemainingTime = remainingTime;
    }
}

저장된 데이터로 오브젝트의 상태를 지정할 수 있도록 SetInfo 메서드도 함께 작성했다.

또, 로직을 Parent에서 관리하게 됨에 따라 현재 상태의 ID로 자식 객체인 Gathering, Debris에 접근할 수 있도록 딕셔너리를 추가했다.


3. [아이템 드랍 테이블 클래스]

기존에 아이템 드랍은 몬스터나 자원 객체가 ItemData를 필드로 갖거나, 배열로 가지고 있거나 해서 그 값을 플레이어 인벤토리 추가하는 방식으로 돼있었다.

즉, 확률에 드랍 여부나 개수의 차이가 없이 고정된 아이템만 드랍할 수 있었다.

ItemLooting, ItemDropTable 클래스를 추가해서 어떤 아이템을 드랍할 건지, 어떤 확률을 가질건지, 몇 개나 줄 건지의 데이터를 갖고 있다가, 요청을 받으면 확률 계산을 해서 ItemData와 개수를 반환해주도록 작성했다.

ItemLooting class

[System.Serializable]
public class ItemLooting
{
    [SerializeField] private ItemData _item;
    [SerializeField] private AnimationCurve _distribution;

    public (ItemData item, int quantity) Looting(float weight = 0)
    {
        float value = Mathf.Clamp01(Random.value + weight);
        int quantity = Mathf.RoundToInt(_distribution.Evaluate(value));
        return (_item, quantity);
    }

    public void AddInventory(InventorySystem inventory, float weight = 0)
    {
        (var item, var quantity) = Looting(weight);
        if (item != null && quantity != 0)
            inventory.AddItem(item, quantity);
    }
}

아이템 개수와 확률을 어떻게 계산할까 고민하다가, Animation Curve를 쓰면 좋을 것 같아서 사용해봤다.
Curve로 아이템의 확률 분포 그래프를 그려두고 0 ~ 1 사이의 랜덤값으로 개수를 찾는거다.

weight은 가중치로, 나중에 플레이어가 드랍률을 올려주는 효과를 갖게 됐을 때 사용하면 좋을 것 같아서 추가해봤다.
가중치는 더하는 방식이기 때문에, 분포표는 오른쪽으로 갈 수록 더 좋도록 작성해야한다.

ItemDropTable class

[System.Serializable]
public class ItemDropTable
{
    [SerializeField] private ItemLooting[] _lootings;

    public void AddInventory(InventorySystem inventory, float weight = 0)
    {
        foreach (var loot in _lootings)
            loot.AddInventory(inventory, weight);
    }
}

큰 기능은 없고, ItemLooting 클래스를 배열로 갖고 있다가 인벤토리에 추가 요청을 한 번에 할 수 있도록 추가로 작성해봤다.

사용 예시

public class ResourceObjectGathering : ResourceObjectBase, IInteractable
{
    [SerializeField] private ItemDropTable _itemTable;

    public void Interact(Player player)
    {
        if (!gameObject.activeSelf)
            return;

        _itemTable?.AddInventory(player.Inventory); // ★
        _parent.SwitchState(_toObjectID);
    }
}

채집 가능한 자원 클래스에서 자신의 드랍 테이블에 있는 아이템들을 플레이어의 인벤토리에 추가하도록 요청하는 코드.

인스펙터에선 이렇게 생겼다.

커브는 이렇게 적용해봤다. 1 ~ 3개 랜덤.

희귀한 아이템의 경우엔 이렇게도 할 수 있다.


profile
game developer

0개의 댓글