20241220 / Unity_6차 15주차 금요일
이번에는 방벽 생성/파괴 시 베이스 영역을 변경해 주기 위해 SortedDictionary를 사용하였다.
사용 이유는 가장 바깥쪽의 방벽이 생성/파괴 되었는지 확인하기 위해 정렬 기능이 필요했기 때문이다.
SortedDictionary<tkey, tvalue>
특징
- 키를 정렬된 순서로 유지하며, 빠른 검색, 삽입, 삭제를 제공하는 C#의 컬렉션 클래스
- 내부적으로 이진 탐색 트리(Balanced Binary Search Tree)를 기반으로 동작
대표적인 메서드
메서드 | 설명 |
Add(TKey key, TValue value) | 지정된 키와 값을 추가. |
Remove(TKey key) | 지정된 키를 삭제. |
TryGetValue(TKey key, out value) | 키가 존재하면 값을 반환하고, 없으면 false를 반환. |
ContainsKey(TKey key) | 지정된 키가 있는지 확인. |
ContainsValue(TValue value) | 지정된 값이 있는지 확인. (효율적이지 않음, O(n)) |
Clear() | 모든 키-값 쌍을 삭제. |
Keys | 정렬된 키의 컬렉션을 반환. |
Values | 정렬된 값의 컬렉션을 반환. |
Count | 딕셔너리에 있는 키-값 쌍의 개수를 반환. |
사용 예시를 베이스 좌측 벽으로 들면
SortedDictionary<float, BuildableArchitecture> wallPositionLeft
private void WallConstructed(BuildableArchitecture wall)
{
float position = wall.transform.position.x;
// 벽이 좌측에 있는 경우
if (position <= allyBase.left)
{
// 기존의 가장 왼쪽 벽과 비교
if (wallPositionLeft.Count > 0)
{
if (position < wallPositionLeft.Keys.First())
{
allyBase.left = position;
OnAllyBaseChanged?.Invoke();
}
}
else
{
allyBase.left = position;
OnAllyBaseChanged?.Invoke();
}
wallPositionLeft.Add(position, wall);
}
}
기존 가장 좌측 벽의 좌표를 wallPositionLeft.Keys.First() 으로 가져올 수 있다.
가장 왼쪽 벽이 부서졌을 때 베이스 좌측 경계를 기존에 왼쪽에서 2번째였던 벽의 좌표로 갱신하기 위해
SortedDictionary의 2번째 Key값을 알아낼 필요가 있었다.
찾아낸 방법은 3가지다.
// Enumerator 순회
flot result1;
var enumerator = dictionary.GetEnumerator();
enumerator.MoveNext(); // 첫 번째
enumerator.MoveNext(); // 두 번째
result1 = enumerator.Current.Key;
// List 변환 후 인덱스 접근
flot result2;
var keys = new List<TKey>(dictionary.Keys);
result2 = keys[1];
// Keys.ElementAt
flot result3;
result3 = dictionary.Keys.ElementAt(1);
각 방식의 장단점을 비교해 보았다.
방법 | 장점 | 단점 |
Enumerator 순회 |
- 메모리 효율적. - 추가 변환 없이 뒤에서부터 탐색 가능. |
- 두 번째 요소까지 O(2) 순회. - 다소 번거로운 코드. |
List 변환 후 인덱스 접근 | - 인덱스 기반으로 빠르게 접근 가능(O(1)). | - 키를 List로 변환하는 데 추가 메모리 사용. - 변환 자체에 O(n) 시간 소요. |
Keys.ElementAt | - 직관적이고 간단한 코드. - 추가 메모리 사용 없음. - 중간 값 접근에 효율적(O(log n)). |
- 트리 구조에서 인덱스를 찾기 위해 log n 연산이 필요. - 느린 인덱스 접근. |
List 변환만 피하면 될 것 같아서 그냥 코드가 단순한 ElementAt 메서드를 사용했다.
float position = wall.transform.position.x;
if (position <= allyBase.left)
{
if (wallPositionLeft.Count > 0)
{
if (wall == wallPositionLeft.First().Value)
{
if (wallPositionLeft.Count >= 2)
{
allyBase.left = wallPositionLeft.Keys.ElementAt(1);
}
else
{
allyBase.left = initialBaseLeft;
}
OnAllyBaseChanged?.Invoke();
}
wallPositionLeft.Remove(position);
}
}
그런데 다시 생각해 보니 요소 삭제부터 하고 First값을 받아오면 될 것 같다는 느낌이 들었다.
bool isChanged = false;
float position = wall.transform.position.x;
if (position <= allyBase.left)
{
if (wallPositionLeft.Count > 0)
{
if (wall == wallPositionLeft.First().Value) isChanged = true;
wallPositionLeft.Remove(position);
if (isChanged == true)
{
if (wallPositionLeft.Count > 0)
{
allyBase.left = wallPositionLeft.Keys.First();
}
else
{
allyBase.left = initialBaseLeft;
}
OnAllyBaseChanged?.Invoke();
}
}
}
두 코드를 비교하면 성능과 가독성 측면에서 모두 바뀐 이후의 코드가 더 좋아 보인다.