옛날에 한 번 C++로 풀어봤던 문제다.
근데 다시 보니까, 큐를 써놓고 다 꺼냈다가 다시 집어넣고,, O(n^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;
}
}
다리에서 트럭이 빠져나갈 때, 그 시간은 트럭이 다리에 올라온 시간 + 다리의 길이다.
문제는 다리에 트럭이 줄줄이 올라온 경우에는 큐의 제일 앞에 있는 트럭이 빠져나간 시간이 큐에 제일 마지막으로 트럭이 들어온 시간보다 빨라서 시간이 과거로 돌아갈 수도 있다.
따라서 제일 앞 트럭이 빠져나간 시간과 현재 시간 중 최대값으로 지정해줘야한다.
자원 오브젝트를 Save/Load 해야한다.
새로 생성된 오브젝트가 있다면 어떤 오브젝트인지, 어떤 위치에 있는지, 어떤 상태인지를 데이터로 뽑을 수 있어야한다.
하지만 내가 작업해둔 현재 구조는 데이터가 따로 놀고 있어서, 데이터를 하나의 객체로 몰빵할 필요가 있었다. (MVC)
저장할 때 필요한 값은 다음과 같다.
이 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에 접근할 수 있도록 딕셔너리를 추가했다.
기존에 아이템 드랍은 몬스터나 자원 객체가 ItemData
를 필드로 갖거나, 배열로 가지고 있거나 해서 그 값을 플레이어 인벤토리 추가하는 방식으로 돼있었다.
즉, 확률에 드랍 여부나 개수의 차이가 없이 고정된 아이템만 드랍할 수 있었다.
ItemLooting
, ItemDropTable
클래스를 추가해서 어떤 아이템을 드랍할 건지, 어떤 확률을 가질건지, 몇 개나 줄 건지의 데이터를 갖고 있다가, 요청을 받으면 확률 계산을 해서 ItemData
와 개수를 반환해주도록 작성했다.
[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
은 가중치로, 나중에 플레이어가 드랍률을 올려주는 효과를 갖게 됐을 때 사용하면 좋을 것 같아서 추가해봤다.
가중치는 더하는 방식이기 때문에, 분포표는 오른쪽으로 갈 수록 더 좋도록 작성해야한다.
[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개 랜덤.
희귀한 아이템의 경우엔 이렇게도 할 수 있다.