클라이언트간 p2p 통신으로 싱크?를 맞추는걸 공부해보고 싶습니다
미해결
[C++과 언리얼로 만드는 MMORPG 게임 개발 시리즈] Part4: 게임 서버
안녕하세요 루키스님 서버<-> 클라이언트 통신이 아닌, 클라이언트간 p2p 통신으로 싱크?를 맞추는걸 공부해보고 싶습니다. 혹시 이런 내용의 강의를 준비중이시거나 추천해주실만한 책이 있을까요?
- 네트워크
- MMORPG
- windows-server
173만명의 커뮤니티!! 함께 토론해봐요.
미해결
[C++과 언리얼로 만드는 MMORPG 게임 개발 시리즈] Part4: 게임 서버
안녕하세요 루키스님 서버<-> 클라이언트 통신이 아닌, 클라이언트간 p2p 통신으로 싱크?를 맞추는걸 공부해보고 싶습니다. 혹시 이런 내용의 강의를 준비중이시거나 추천해주실만한 책이 있을까요?
미해결
[C++과 언리얼로 만드는 MMORPG 게임 개발 시리즈] Part4: 게임 서버
안녕하세요! 빨간책(moden C++ design)이 온라인에 검색해보니까 해외 직구로 살 수 있긴 하더라고요. 지금 이직 준비 단계에선는 보기 어려울 거 같아 일을 시작하면 읽고 싶단 생각이 듭니다. 괜찮을까요?
미해결
[C++과 언리얼로 만드는 MMORPG 게임 개발 시리즈] Part4: 게임 서버
void Session::ProcessConnect() { _connectEvent.owner = nullptr; // (1) _connected.store(true); // (2) // 세션 등록 GetService()->AddSession(GetSessionRef()); // 컨텐츠 코드에서 재정의 OnConnected(); // 수신 등록 RegisterRecv(); } 5장 Session#3 강의를 듣고 따라해보다가 클라이언트 프로그램이 위 함수를 실행하다가 터지는 문제가 발생했습니다. 디버깅을 해보니 (1)에서 _connectEvent.owner 의 strong ref가 1에서 0으로 줄어들면서 세션(this)이 삭제되고 (2)에서 this를 참조하면서 문제가 생긴 것이었습니다. bool IocpCore::Dispatch(uint32 timeoutMs) { DWORD numOfBytes = 0; ULONG_PTR key = 0; IocpEvent* iocpEvent = nullptr; if (::GetQueuedCompletionStatus(_iocpHandle, OUT &numOfBytes, OUT &key, OUT reinterpret_cast<LPOVERLAPPED*>(&iocpEvent), timeoutMs)) { // (A): 강사님 코드 // strong ref: 1 IocpObjectRef iocpObject = iocpEvent->owner; // strong ref: 2 iocpObject->Dispatch(iocpEvent, numOfBytes); // (B): 제 코드 // strong ref: 1 //iocpEvent->owner->Dispatch(iocpEvent, numOfBytes); } else { int32 errCode = ::WSAGetLastError(); switch (errCode) { case WAIT_TIMEOUT: return false; default: // TODO : 로그 찍기 IocpObjectRef iocpObject = iocpEvent->owner; iocpObject->Dispatch(iocpEvent, numOfBytes); break; } } return true; } 강사님 코드와 제 코드에서 어떤 부분이 다른지 확인해 본 결과 위와 같은 차이가 있었습니다. 강사님은 지역 변수로 스마트 포인터를 하나 만들어서 레퍼런스 카운트가 2인 상태로 세션->Dispatch 함수를 호출하는 반면, 저는 레퍼런스 카운트가 1인 상태로 세션->Dispatch 함수를 호출하는 차이가 있었습니다. [질문 1] 혹시 강사님은 코드를 짜실 때 (B)와 같이 코드를 짜면 문제가 생긴다는 걸 인지하고 (A)와 같이 짜신 걸까요? 레퍼런스 카운트를 어느 정도까지 고려하면서 프로그래밍을 하는 것이 좋은지 궁금합니다. [질문 2] 아래와 같이 코드를 수정해서 문제를 해결해도 별 문제가 없을까요? // 원본 코드 void Session::ProcessConnect() { // RELEASE_REF _connectEvent.owner = nullptr; _connected.store(true); // 세션 등록 GetService()->AddSession(GetSessionRef()); // 컨텐츠 코드에서 재정의 OnConnected(); // 수신 등록 RegisterRecv(); } // 수정된 코드 void Session::ProcessConnect() { _connected.store(true); // 세션 등록 GetService()->AddSession(GetSessionRef()); // RELEASE_REF _connectEvent.owner = nullptr; // 컨텐츠 코드에서 재정의 OnConnected(); // 수신 등록 RegisterRecv(); }
미해결
[C++과 언리얼로 만드는 MMORPG 게임 개발 시리즈] Part4: 게임 서버
안녕하세요! 메모리 풀의 단점 중 하나가 메모리가 오염됐을 때 계속 재사용되다가 시간이 지나서 문제가 되니까 원인 찾기가 어렵다고 말씀하셨는데요. 그게 정확히 어떤 식의 오염인지 감이 안 와서 잘 와닿지 않았습니다. 요구하는 사이즈와 같거나 큰 크기의 메모리를 할당받아서 사용하고 반납하는데, 어떤 오염이 발생할 수 있는 건지 알고 싶습니다! 심지어 다른 애가 그 오염된 메모리를 사용하는 게 문제라고 하셨는데 그 오염된 상태가 어째서 계속 유지될 수 있는지 궁금합니다.
미해결
[C++과 언리얼로 만드는 MMORPG 게임 개발 시리즈] Part4: 게임 서버
안녕하세요! 개인적으로 메모리 관련한 수업..굉장히 잘 듣고 있습니다. 감사합니다!! 다름이 아니라 우리가 수업에서 제작한 코드는 47:33에서 언급하셨듯 Use-After-Free 문제가 남아 있는데요. 이 부분이 마소 성님들이 만든 SLIST 시리즈를 사용하면 해소가 되는 부분일까요? 아니면 SLIST와는 관련 없는 설계/구조적인 문제일까요? 관련해서 설명하실 때 직접 만들어서 사용하지 말라고 하신 부분에서, 마소의 SLIST는 이 문제를 어떻게 다루었는지 궁금증이 생겼습니다.
미해결
[C++과 언리얼로 만드는 MMORPG 게임 개발 시리즈] Part4: 게임 서버
안녕하세요! 마소 성님이 만들어준 SLIST_HEADER, SLIST_ENTRY, alligned_malloc..등을 이용한 메모리 풀 방식을 실무에서 사용한 적 있으실까요? 직접 사용은 안 하셨다면 사용된 프로젝트를 보신 적 있을지 궁금합니다!
미해결
[C++과 언리얼로 만드는 MMORPG 게임 개발 시리즈] Part4: 게임 서버
다음처럼 하면 의도대로 동작합니다! int32 size = 0; // _poolTable이 4097 크기인 이유는 요구 사이즈가 1부터 시작해서 4096까지 가야 하므로 int32 table_index = 1; for (size = 32; size < 1024; size += 32) { MemoryPool* pool = new MemoryPool(size); _pools.push_back(pool); while (table_index <= size) { _pool_table[table_index] = pool; ++table_index; } } for (; size < 2048; size += 128) { MemoryPool* pool = new MemoryPool(size); _pools.push_back(pool); while (table_index <= size) { _pool_table[table_index] = pool; ++table_index; } } for (; size <= 4098; size += 256) { MemoryPool* pool = new MemoryPool(size); _pools.push_back(pool); while (table_index <= size) { _pool_table[table_index] = pool; ++table_index; } } 반복문의 조건문에서 등호(=)를 빼면 됩니다. 경계의 값은 다음 반복문에서 처리할 수 있으니까요. 마지막 반복문은 다음 반복문에서 처리할 게 없으므로 끝까지 가야 해서 등호를 살려놓습니다. 예를 들어 두 번째 반복문에서, table_size는 이전 반복문에서 1024가 되고, 두 번째 반복문의 size는 1024부터 시작하니까 아귀가 딱딱 맞게 돼요. 경계값에 문제 없는 것도 확인했습니다. 약간 갸우뚱하셨던 분들은 참고하면 좋을 것 같습니다!
미해결
[C++과 언리얼로 만드는 MMORPG 게임 개발 시리즈] Part4: 게임 서버
특정 lock L1이 pop되는 상황이 있다고 할 때 L1으로의 순방향 간선 정보가 담긴 다른 Lock들의 history에 대해서는 별도로 L1값을 제거해야 할 필요는 없나요??
미해결
[C++과 언리얼로 만드는 MMORPG 게임 개발 시리즈] Part4: 게임 서버
Missile 클래스에서 WraithRef _target = nullptr; 코드를 넣어주면, GameServer.cpp에서 테스트할 때 MissileRef missile(new Missile()); 이 코드를 타고 들어가면 TSharedPtr(T* ptr) { Set(ptr); } 동작할 때 ptr에 nullptr이 들어옵니다. Missile 클래스에서 WraithRef _target = nullptr;을 하지 않고 Missile 클래스에서 WraithRef _target; 으로만 작성하면 정상 동작하고요. 저와 같은 증상 겪는 분 없나요? vs 버전은 17.10.4 입니다.
미해결
[C++과 언리얼로 만드는 MMORPG 게임 개발 시리즈] Part4: 게임 서버
안녕하세요. 나중에 프로젝트 진행하실 땐 lock stack을 TLS로 관리해서 스레드마다 스레드 id가 관리되도록 하신다 말씀하셨습니다. 여기에서 생긴 궁금증입니다. 스레드 id가 부여될 때, 감소하지 않고 증가하는 nameToId().size()로 만들어지는 이상 모든 id가 고유할 거라고 생각합니다. id를 통해서 lockHistory에서 value(set객체)를 꺼냅니다. lockHistory는 TLS에서 관리되지 않는 전역 객체입니다(애초에 그러면 안 되겠죠. 전역적으로 사이클 여부를 탐지해야 하니까요!). 스레드 id가 유일한 이상 lockStack을 TLS에서 관리하나 전역으로 관리하는 것이 차이가 없을 것 같은데, 강의 후반에 새로 영상을 추가하시어 '스레드마다 잡고 있는 락이 다르기 때문에 TLS로 관리해야 한다'고 말씀하신 이유가 궁금합니다! id가 전역적으로 1,2,3,4,5,6..이렇게 증가하거나 스레드 별 공간에서 {1,2..} {1,2...} {1,2...} 의 차이 정도로만 생각되어 어떤 본질적인 차이가 있는지 알고 싶습니다! p.s 수업에서 진행한 테스트 코드에서도 문제가 없었던 이유도 궁금증에 한몫을 했습니다.
미해결
[C++과 언리얼로 만드는 MMORPG 게임 개발 시리즈] Part4: 게임 서버
안녕하세요! 어제, 강의도 다 안 듣고 질문을 드린 바보 같은 저를 질책하며 강의를 다 듣고 제가 이해한 게 맞는지 확인을 좀 받고 싶습니다! 락 획득 정책을 말씀하실 때 살짝 이해가 안 가는 부분이 있었는데 동일 스레드일 때란 걸 알게 되고 나서는 이해에 큰 어려움이 없었습니다. 그래도 확인이 필요하다는 생각이 들었습니다! 각 항목을 번호로 구분해보겠습니다! 동일 스레드일 때의 정책이란 게 락을 획득 후 실행될 때는 사실상 싱글 스레드인 것처럼 생각해도 되겠더라구요 그렇다고 한다면 GameServer.cpp에서 테스트 코드 작성하실 때 ThreadWrite()일 때 TestPush(), TestPop()의 동작이 한 스레드에서 동작하는 건 당연히 문제가 안 되니까 W->W 혹은 W->R을 허용하는 게 이해가 됐습니다. 한 처리가 완료되고 나서 다른 처리로 넘어가니까요. 그렇다면 같은 논리가 적용될 것 같은데 R->W은 왜 안 되느냐에 대해서도 생각해봤는데요. 논리적으로 접근했을 때, 하나의 함수 내에서 읽기 연산 중에 쓰기 연산을 하는 건 단일 책임 원칙에도 위반될 뿐더러 이상한 코드가 나오더라고요. 가령 테스트 코드로 작성하신 TestRead() 안에 TestPush() 혹은 TestPop()을 넣어버리면 안 되니까요. 또한 데드락이 생기지 않기 위해서는 락을 거는 순서가 중요한데, w->r을 허용한 이상 r->w를 허용하면 데드락이 생기기 딱 좋다. ReadLock에 대해서는, WriteLock을 잡고 있지 않은 다른 스레드는 ReadLock을 획득할 수 있다! ReadLock의 주석에 "아무도 소유하고 있지 않을 때"라고 되어 있어서, 조금 헷갈렸는데요. 코드를 보면 마스킹해서 read_count만 확인하는 걸 보고 역시 ReadLock은 WriteLock을 잡고 있지 않은 스레드도 잡을 수 있구나, 카운트 올리는 것만 경합하는 거구나, 로 이해했습니다. 2년 전에 처음 강의를 들을 때 제대로 이해 안 하고 넘어갔다가 나중에 큰 화를 당했어서, 이번에는 확실히 이해하려고 하다 보니 코드에서 참 얻을 게 많다 느낍니다. 틀린 게 있으면 지적 부탁드립니다. 감사합니다!
미해결
[C++과 언리얼로 만드는 MMORPG 게임 개발 시리즈] Part4: 게임 서버
안녕하세요! 아직 끝까진 들은 건 아니지만 중간에 큰 의문점이 생겨서 질문드립니다. 32형 변수에 상위 비트, 하위 비트를 사용하려면 | 연산을 이용해야 하지 않나요~? 가령 상위 비트에 새로운 값을 쓰는 걸 의사 코드로 표현하면, int flag = _lockFlag.load(); desired = (flag & 0x0000FFFF) | ((LThreadId << 16) & WRITE_THREAD_MASK); 이렇게 하지 않고 다음 코드를 돌리면 while (true) { for (int spin_count = 0; spin_count < MAX_SPIN_COUNT; ++spin_count) { uint32 expected = EMPTY_FLAG; if (_lock_flag.compare_exchange_strong(OUT expected, desired)) { ++_write_count; return; } } } write flag를 쓸 때마다 read flag가 날아가는 것 같아서요!
미해결
[C++과 언리얼로 만드는 MMORPG 게임 개발 시리즈] Part4: 게임 서버
안녕하세요! 동시에 TryPop() 을 할 경우, oldHead 를 참조하는 스레드가 있을 수 있고 이 때 무작정 해제해버리면 use after delete 오류가 발생할 수 있다고 하셨습니다. TryDelete() 함수를 구현하실 때, 1 == popCount 안에서 delete old_head 를 할 때는 이미 분리했으니까 괜찮다고 하셨는데요. 첫 문단에서 언급한 부분과 반대되는 말을 하신 것 같다는 느낌을 받았습니다. 제가 생각했을 때는, 저희 수업 때 사용하신 예제 코드에서는, cas 연산을 통해 oldHead 를 분리했기 때문에 use after delete 오류가 안 날 것 같단 생각이 들더라고요. 궁금해서 테스트를 해봤습니다. TryDelete() 함수를 호출하지 않고 바로 delete old_head; 를 호출하는 코드를 넣었고요, 지금 이 질문을 올리고 있는 와중에 push() 는 sleep_for(1ms) 를 주고 pop() 을 하는 스레드는 5개를 돌려서, 진단 도구에서 확인되는 프로세스 메모리가 1mb이 유지되게 설정해서 30분 넘게 돌려보고 있습니다. 오류가 안 나고 있는 광경을 목격 중입니다. 그냥 운이 좋은 걸까요? 오류가 발생하지 않는 이유는 다음과 같다 생각합니다. atomic 변수인 head 에서 cas 연산을 하기 때문에 간발의 차이로 먼저 다음 코드를 통과한 스레드가 획득한 oldHead 는 다른 스레드가 이후에 될 oldHead 와 같지 않다. while 조건에서 true를 반환하는 순간 head 는 head 의 next 를 가리키고 있게 되기 때문이다. compare_exchange_weak() 함수가 true를 반환하면 head 는 원자적으로 head->next 를 가리키는 상태가 된다. while (old && false == _head.compare_exchange_weak(old, old->next)) return false; 고견 부탁드립니다. 멋진 강의 감사합니다.
미해결
[C++과 언리얼로 만드는 MMORPG 게임 개발 시리즈] Part4: 게임 서버
Reader-Writer Lock 구현중에 Atomic 변수를 사용하고 있는데, Atomic 변수자체가 lock이 포함되어 있어서 대규모 Read가 병렬적으로 처리되는 상황이 아니라면 일반 lock을 사용하는 것이 더 효율이 좋을 것 같습니다. 하지만 Reader-Writer Lock이 의미가 있을 정도로 대규모 Read가 빈번하게 일어나는 경우에는 WriteLock쪽에서 기아 현상이 발생할 것 같습니다. 그렇다면 Reader-Writer Lock은 피크 타임에 대규모 Read가 발생하는 경우에만 사용하는 건가요? (LThreadId << 16) & WRITE_THREAD_MASK 부분에서 굳이 & 연산을 추가로 하는 이유가 뭔가요?
미해결
[C++과 언리얼로 만드는 MMORPG 게임 개발 시리즈] Part4: 게임 서버
2022버전인데 정의는 되어있으나 특정 tcp/ip함수들을 식별하지 못하는 현상이 있습니다.(헤더가드 다 하였음) 제 경우에는 inet_ntop 함수 단 하나만 컴파일러가 식별하지못하는 에러가있어서 stackoverflow나 reddit을 찾아도 원하는 상황에 및 해결법은 나오지않아 pch.h환경에서 쓰기때문에 GlobalPch.h 에서 #include <WinSock2.h> #include <mswsock.h> #include <ws2tcpip.h> #include <windows.h> 4가지 헤더를 이 순서대로 배치하고 프로젝트 별 헤더에서는 위의 4가지를 선언하지않으니 해결되었습니다. 혹시나 특정함수 단 하나만 식별되지않거나하면 시도 해보시기 바랍니다. 저는 시간날렸어도 누군가는 금방 해결해서 다음step으로 빠르게 가길 빕니다.
미해결
[C++과 언리얼로 만드는 MMORPG 게임 개발 시리즈] Part4: 게임 서버
안녕하세요! 스핀이 계속 되면서 CPU 점유율이 치솟는 문제에 대한 대안으로 sleep_for(1ms)를 호출하는 건 어떻게 생각하실지 궁금합니다. 질문을 다 하고 나니, 커널의 도움을 굳이 받지 않고 오래 대기하지 않아도 되는 상황이 보장될 때 사용하면 좋은 락이 스핀 락인데 sleep_for()을 하면 컨텍스트 스위칭이 발생하겠네요. 이렇게 되면 스핀 락을 쓰는 이유가 퇴색될 것 같다는 생각이 듭니다. 고견 부탁드립니다 🙏
미해결
[C++과 언리얼로 만드는 MMORPG 게임 개발 시리즈] Part5: UE5 & IOCP 서버 연동
selected viewport나 new editor window로 실행 했을 때 Stop 버튼을 눌러도 연결이 끊어지지 않고 언리얼 에디터까지 꼭 종료해줘야 끊어지더라구요. stop버튼을 눌렀을 때 종료되는 것처럼 하려면 어떻게 해야 하나요?? standalone game으로 해봤을 때 제가 원하는 대로 동작해주긴 했는데 위에 두가지 경우에도 방법이 있을까 해서 질문드립니다.
미해결
[C++과 언리얼로 만드는 MMORPG 게임 개발 시리즈] Part5: UE5 & IOCP 서버 연동
혹시 만약에 인벤토리을 관리할떄 서버가 아닌 클라에서 아이템 관리를 위해 아이템객체들을 전부 스마트포인터를 써야할지... 포션같은 소모아이템 떄문에 써야되지 않을까 하는데 너무 고민입니다. 현업에서도 GC를 적극적으로 사용을 하는지 서버가 있는데 AI 인공지능을 쓰는 경우가 있다면 어느 경우인지 이렇게 질문 3개입니다...
미해결
[C++과 언리얼로 만드는 MMORPG 게임 개발 시리즈] Part5: UE5 & IOCP 서버 연동
안녕하세요 루키스님 part4 강의로 만들어진 서버와 part5에서 만들어진 클라이언트를 조금 수정해가며 추가 기능을 만들던 중 문제가 생겼는데 해결이 잘 안되어서 질문드립니다. 우선 추가하려는 기능은 강의에서 배운 Q를 눌러서 종료하는 기능 외에 플레이어(3명)들이 접속해있는 상태에서 언리얼 편집창을 닫아버렸을 때 3명 모두 접속이 끊어지며 플레이어들을 서버에서 제거해주는 기능을 추가하려고 합니다.. 이 때, 발생하는 문제는 player가 3명이고 objectId가 각각 1번부터 3번까지 있다고 했을 때, 어쩔 땐 player들 모두 제거되고, 어쩔 땐 player(2번), player(1번), player(1번)처럼 objectId가 1번인 플레이어를 제거하는 처리가 중복 호출되며 2번과 1번에 대한 삭제만 이루어지고, 3번에 대한 삭제가 이루어지지 않는 문제가 발생합니다. 아래는 작성한 코드입니다. 먼저 GameSession::OnDisconnected()의 맨 마지막 줄에 GRoom->DoAsync()로 Room::HandleLeavePlayer를 호출해주는 코드를 추가했고, 이 때 들어오는 player들의 objectId들을 확인해봤을 때 1, 2, 3 모두 중복 없이 들어오는 것을 확인했습니다. void GameSession::OnDisconnected() { if (player.load() == nullptr) { return; } cout << "objectId: " << player.load().get()->objectInfo->object_id() << endl; RoomRef room = player.load()->room.load().lock(); if (room == nullptr) { return; } GRoom->DoAsync(&Room::HandleLeavePlayer, player.load()); } 다음은 Room::LeaveRoom()에서 위에서 말한 상황일 때 파라미터로 들어오는 object가 중복으로 들어오는 걸 확인했습니다. if(!success) { return success; }는 이미 삭제된 object를 중복으로 RemoveObject(objectId)를 할 경우 false를 돌려주는데 밑에서 패킷 보내는 작업을 넘겨버리기 위해 추가했습니다. 그리고 코드 거의 마지막 부분에 GSessionManager.Remove(session)로 세션을 제거하는 코드를 추가했습니다. bool Room::LeaveRoom(ObjectRef object) { if (object == nullptr) { return false; } const uint64 objectId = object->objectInfo->object_id(); bool success = RemoveObject(objectId); if (!success) { return success; } // 퇴장할 플레이어에게 퇴장 패킷 전송 if (object->IsPlayer()) { PlayerRef player = static_pointer_cast<Player>(object); Protocol::S_LEAVE_GAME leavePkt; SendBufferRef sendBuffer = ClientPacketHandler::MakeSendBuffer(leavePkt); if (GameSessionRef session = player->session.lock()) { session->Send(sendBuffer); } } // 남아있는 플레이어들에게 퇴장 플레이어의 디스폰 패킷 전송 { Protocol::S_DESPAWN despawnPkt; despawnPkt.add_object_ids(objectId); SendBufferRef sendBuffer = ClientPacketHandler::MakeSendBuffer(despawnPkt); Broadcast(sendBuffer, objectId); // 퇴장 플레이어는 이미 room에서 빠져나간 상태라 Broadcast 불가 if (object->IsPlayer()) { PlayerRef player = static_pointer_cast<Player>(object); if (GameSessionRef session = player->session.lock()) { session->Send(sendBuffer); GSessionManager.Remove(session); } } } return success; } GameSession 소멸자 virtual ~GameSession() override { cout << "~GameSession" << endl; player.store(shared_ptr<Player>()); } Player 소멸자 Player::~Player() { cout << "~Player" << endl; session.reset(); } 다음은 첫번째 실행 화면입니다. (실행 화면에 출력되는 줄이 뒤죽박죽이라 서로 다른 색으로 구별 해봤습니다..ㅠ) 연두색으로 묶은 범위는 언리얼 에디터를 1번 실행했다가 닫았을 때의 단위로 묶었습니다. 빨간색으로 밑줄 그어진 objectId: 숫자로 출력되는 부분은 GameSession::OnDisconnected()에서 출력하고 있고, 노란색으로 밑줄 그어진 Remove Objedct숫자로 출력되는 부분은 Room::RemoveObject()에서 objectId를 출력하고 있습니다. 첫번째 실행 화면에서는 처음 실행하고 바로 해당 문제가 발생했습니다. objectId: 3, objectId: 2, objectId: 1 순으로 중복 없이 잘 들어왔는데 Remove Object3, Remove Object1, Remove Object1 처럼 출력되었고, 2번 플레이어에 대한 제거가 되지 않았습니다. 때문에 두번째, 세번째 실행에서는 접속한 3명의 플레이어가 모두 잘 제거됨에도 처음에 제거되지 않은 플레이어 하나가 남아서 언리얼 에디터 실행 화면에 보이는 플레이어가 총 4명이었습니다. 다음은 두번째 출력 화면입니다. 두번째 실행 화면에서는 5번째가 되어서야 해당 문제가 발생했습니다. 그 전까지는 문제 없이 3명의 플레이어 모두 잘 제거되었습니다. 마찬가지로 objectId: 13, objectId: 14, objectId: 15로 들어왔고 Remove Object13, Remove Object15, Remove Object15로 실행되면서 13, 15번의 플레이어만 제거되고 14번 플레이어는 제거되지 않았습니다. 실제로 문제가 발생할 경우의 호출 스택을 따라가 봤을 때 OnDisconnected()에서 GRoom->DoAsync(&Room::HandleLeavePlayer, player.load())로 삭제 작업 예약 거는 것 까지는 플레이어가 중복 없이 잘 들어가는데 예약했던 Room::HandleLeavePlayer()가 호출되고 파라미터로 들어오는 플레이어의 objectId를 확인해보면 이미 삭제된 플레이어가 한번 더 들어오는 경우가 생기네요. 혹시 어떤 부분 때문일까요??
미해결
[C++과 언리얼로 만드는 MMORPG 게임 개발 시리즈] Part4: 게임 서버
이번 강의는 순환 참조일 때 한 객체가 소멸될 시 참조하고있는 객체에 관해 refCount가 줄지않아 문제가 생기는 것에 관해서 여러 방법을 알려주시는것 같았습니다. 그렇다면 소멸자 부분에서 자신이 참조하고 있는 객체들을 release해주는것은 어떤가요? 참조하는 객체가 많으면 유지보수하기 어려울것 같은데 그래도 이것 또한 해결책이 될 수 있나요?