Code KATA
오늘 치의 알고리즘 코드카타를 풀이하고 정리하였다
https://iiblueblue.tistory.com/211
[2025.02.11] 달리기 경주
문제 설명안에서 매년 달리기 경주가 열립니다. 해설진들은 선수들이 자기 바로 앞의 선수를 추월했을 때 추월한 선수의 이름을 부릅니다. 예를 들어 1등부터 3등까지 "mumu", "soe", "poe" 선수들이
iiblueblue.tistory.com
최근에 챌린지반에서 map에 대해서 배웠다. 이렇게 바로 써먹을 곳이 있을지 몰랐는데 덕분에 금방 문제를 해결하였다.
분명 vector만으로도 충분히 해결할 수 있는 문제였다. callings에서 나온 string형 값을 find를 이용해서 인덱스를 찾고 그 인덱스와 바로 앞에 있는 인덱스의 사람 둘이 자리만 바꿔주면 되는 문제였다. 자리 바꾸기야 C++을 처음 시작하면 하도 많이 시키는 예제라 temp라는 빈 변수를 만들어 놓고 정석대로 구현하였다.
너무 쉽다 생각하긴 했지만 역시 시간 초과 오류가 발생하였다. 이 경우를 생각하지 못했던 것이 아니기 때문에 map을 사용하는 방향으로 바로 방향을 틀었다. callings에서 받을 수 있는 값이 sting 타입의 선수 이름이어서 map에 선수 이름을 키로 하면 빠르게 찾을 수 있을 것 같다는 생각을 하긴 했었다.
하지만 여기서 한 가지 막혔던 부분이 map만 사용 하려고 했었다는 점이다. vector로 구현했을 때 시간초과 오류가 발생하였으니 map만 이용해서 구현해보자 생각했었다. 결론적으로는 이전과 다를 것이 없어질 것 같았다. callings에서 불린 사람을 찾는 것이야 순식간에 할 수 있지만 그 사람 앞에 있는 사람을 찾으려고 하니 막막해졌다. map에서는 index로 앞에 있음을 알 수도 없고 value로 사람을 찾으려고 하면 결국 find와 다를 것이 없을 것 같았다. 결론적으로는 vector와 map을 함께 사용하면 된다는 것이 가장 효율적이라는 것을 알게 되었다.
* 자세한 구현과 오답 수정 과정은 링크에 정리되어 있음
저번 챌린지반 과제부터 나는 같은 데이터를 컨테이너 두 개를 사용하여 관리하는 것이 익숙하지 않고 심지어는 이상하게 느끼기도 하였다. 그래서 그렇게 해결할 생각을 하지 못하거나 다른 방법을 찾아 돌아가곤 한다. 저번 과제에서도 컨테이너 2개를 사용해 데이터를 관리했는데 이게 맞나라는 생각을 아직도 떨칠 수가 없다. 하지만 이번 코딩 테스트 문제를 풀어보니 두 개의 컨테이너를 사용해야 가장 효율적인 동작이 가능한 상황도 있음을 깨달았다.
Unreal
4-1, 4-2, 4-3, 4-4 강의를 모두 수강하고 실습을 완료하였다.
오류 발견
실습을 모두 완료하고 테스트를 진행하다 오류를 발견하였다. 주로 한번 게임을 완료하고 Restart를 누른 후 게임을 하다가 발생하는 오류인데 아이템을 먹다가 갑자기 오류가 발생했다며 Visual Studio 화면이 등장한다.
예외 발생(0x00007FFC07F2B358(UnrealEditor-CH3_LearningProject.dll), UnrealEditor.exe): 0xC0000005: 0x00000000000005D0 위치를 읽는 동안 액세스 위반이 발생했습니다..
BaseItem에서 아이템을 먹었을 때 Particle을 생성한 후 Particle을 삭제하는 부분에서 발새한 오류이다. 오류 원인을 알기 위해예외 발생 메시지에 대해서 Chat GPT에게 물어봤다.
예외 발생 코드는 프로그램이 잘못된 메모리 주소를 읽거나 쓰려고 할 때 발생하는 오류라고 한다. 저 부근에서 사용하고 있는 PickupParticle, PickupSound, GetWorld(), 타이머에서 사용되는 Particle이 nullptr인 경우 발생할 수 있는 오류이다. 후보군이 너무 많아 좀 더 정확히 오류의 위치를 파악하기 위해 호출 스택을 열어 살펴보았다.
> [인라인 프레임] UnrealEditor-CH3_LearningProject.dll!ABaseItem::ActivateItem::__l11::<lambda_1>::operator()() 줄 84 C++
호출 스택은 ABaseItem::ActivateItem() 함수 내의 람다 함수 내부에서 문제가 발생한 것이다. Chat GPT는 아래가 문제 상황이라고 설명하였다.
SetTimer()가 실행될 때까지 Particle이 유효한지 보장되지 않습니다. 즉, SetTimer()가 실행되기 전에 Particle이 파괴되었을 경우, nullptr을 참조하려고 하면 액세스 위반 오류가 발생할 수 있습니다.
지금으로써 내가 의심할 수 있는 것은 Restart에서 무언가 꼬였음이다. 도대체 SetTimer가 실행되기 전 Particle이 파괴될 수 있는 경우는 무엇일까.
- ActivateItem()에서 Particle을 스폰하고 타이머를 설정함
- 하지만 곧바로 레벨이 바뀌면서 ABaseItem이 삭제됨.
- ABaseItem이 삭제될 때, Particle도 자동으로 삭제됨.
- 문제는, SetTimer()가 등록된 시점에서는 여전히 Particle을 참조하고 있음.
- 타이머 시간이 지나고 실행되었을 때, 이미 삭제된 Particle을 참조하면서 충돌 발생.
결론적으로 이미 사라져 버린 Prticle 멱살을 붙잡고 내놓으라고 하고 있는 것이다. 저 앞에서 분명 Particle이 있다고 분명 확인을 받았는데도 말이다. 인증을 받고 사용을 할 때까지 바로 그 사이에 Particle이 사라지는 사건이 발생한 것이다. 그 사건이라는 것은 Level 이 변경되는 사건을 말한다.
사실 이 결론을 얻고 해결할 때까지 많은 시행착오가 있었지만 생각보다 간단하게 해결할 수 있는 것이었다. 바로 사용하기 직전에 다시 확인하는 것이다. 정말 Particle이 있는지 집요하게 물어보는 것이다. 그 역할을 해주는 것이 TWeakObjectPtr이다.
if (Particle)
{
UE_LOG(LogTemp, Warning, TEXT("DestroyParticleTimerHandle-BaseItem"));
FTimerHandle DestroyParticleTimerHandle;
TWeakObjectPtr<UParticleSystemComponent> WeakParticle = Particle;
GetWorld()->GetTimerManager().SetTimer(
DestroyParticleTimerHandle,
[WeakParticle]()
{
if (WeakParticle.IsValid())
{
WeakParticle->DestroyComponent();
}
},
2.0f,
false
);
}
우선 이전에 사용하던 Particle을 안전하게 TWeakObjectPtr로 감싸준다. 그리고 Particle을 사용하던 부분을 모두 WeakParticle로 바꾸어주면 된다. 그 다음 WeakParticle을 삭제할 때 조건문을 이용하여 WeakParticle이 유효한지 확인한 후 진행하도록 한다.
TWeakObjectPtr은 언리얼 엔진에서 제공하는 약한 참조 타입이다. 약한 참조라 분명 어디서 들어본 이름이다.
https://iiblueblue.tistory.com/131
스마트 포인터(Smart Pointer)
⊙ 스마트 포인터의 원리와 필요성에 대해 이해한다. ⊙ 스마트 포인터의 종류가 무엇이 있는지 안다. ⊙ unique_ptr, shared_ptr, weak_ptr의 차이와 특징을 이해한다. 스마트 포인터란?동적으로 할당된
iiblueblue.tistory.com
하지만 이 때도 다른 참조들 보다 특별히 다른 특징이 보이거나, 자주 사용해보지 못하여서 제대로 개념을 이해하지 못하고 넘어갔었다. 정리를 하면서도 조력자의 역할처럼 정리가 되어 많이 신경쓰지 않았다.
TWeakObjectPtr은 객체를 소유하지 않는다. 참조는 하고 있지만, 객체의 생명주기에 영향을 주지는 않는다. 그리고 지금부터 할 얘기가 중요한데, 객체가 삭제되면 자동으로 감지가 가능하다. 지금처럼 nullptr을 참조하는 실수를 방지할 수 있다. 또 위에서 사용한 기능 중 하나인 Isvalid()의 사용이다. IsValid()를 이용하면 안전한 참조 확인이 가능하다. 여전히 객체가 잘 살아있는지 확인하는 것이다.
이런 방법으로 있지도 않은 Particle을 제거하다가 생기는 오류 없앨 수 있게 되었다. 앞으로 위와 같은 구조에서 Timer을 사용하며 Timer 사용 중 갑자기 레벨이 바뀌거나 오브젝트가 삭제될 위험이 있다면 바로 TWeakObjectPtr<>을 사용하는 것이 좋을 것 같다.
Quest
- [8번 과제] 게임 루프 및 UI 재설계하기
'TIL' 카테고리의 다른 글
2025.02.19(수) (0) | 2025.02.19 |
---|---|
2025.02.13(목) (0) | 2025.02.13 |
2025.02.10(월) (0) | 2025.02.10 |
2025.02.07(금) (0) | 2025.02.07 |
2025.02.06(목) (0) | 2025.02.06 |