[C++과 언리얼로 만드는 MMORPG 게임 개발 시리즈] Part5: UE5 & IOCP 서버 연동
안녕하세요 루키스님 루키스님의 강의를 보면서 나날히 성장하고 있는 예비 언리얼 프로그래머입니다. 다름이 아니고 온라인 서버의 웹서버를 유니티에서만 보고 있습니다. 혹시나 해서 궁금한데 데디케이트 서버라는게 효율이 좋을수 있지만 구현자체가 어렵다는걸 알기에 혹시 언리얼과 웹서버를 구현하고 연결할수 있으신가여? 가능하다면 혹시 강의도 만들어주실수 있나요?
안녕하세요 먼저 좋은 강의 감사드립니다. ABCharacterStatComponent.cpp의 SetLevelStat함수에서 CurrentLevel에 대한 Clamp의 MinValue가 1, MaxValue가 CharacterMaxLevel로 설정되어있는데 인덱스값이므로 MinValue = 1, MaxValue = CharacterMaxLevel - 1로 수정이 필요할 것 같습니다.
텔레포트를 구현할 때 네트워크 동기화 문제 때문에 CharacterMovementComponent를 확장하는 방식으로 구현해야 한다고 했는데 그러면 타임라인 함수를 사용하여 캐릭터를 특정 위치로 서서히 이동 시키는 기능도 CharacterMovementComponent를 확장하는 방식으로 만들어야 할까요?
안녕하세요, GA, GE 등은 그래도 객체 정보를 담을 수 있게 C++에서 기본적인 작업을 하고 BP로 만든 객체를 세팅해주고 구현이 되는데, GC는 BP로 만든 객체를 세팅 없이 Tag로 바로 발동 되는 거 같습니다. AbilitySystem에서 GamePlayCue로 설정된 Tag의 Asset들을 Tracking하여 따로 관리하는 것일까요?
HUDWidget 같은 경우 CreateWidget함수로 위젯을 만들잖아요,. 근데 HpBarWidget같은 경우 CreateWidget함수가 안 보이더라고요. 어디서 만들어지는거죠? 제가 이해한 바로는 다음과 같습니다. Base클래스 생성자에서 HpBar라는 위젯컴포넌트에 주소 참조해서 가져온 HpBarWidget클래스를 HpBar->SetWidgetClass함수에 넣으면 CreateWidget함수처럼 위젯을 생성한다. 이렇게 이해하는게 맞을까요? 만약 맞다면 언리얼에서 Widget 생성하는 방법은 이렇게 2가지 있을까요? 1. CreateWidget 사용 (HUD 만들 때 사용) 2. 위젯 클래스를 주소참조해 가져와 위젯컴포넌트에 담기 (Actor를 따라다닐 UI 만들 때 사용 )
[C++과 언리얼로 만드는 MMORPG 게임 개발 시리즈] Part5: UE5 & IOCP 서버 연동
selected viewport나 new editor window로 실행 했을 때 Stop 버튼을 눌러도 연결이 끊어지지 않고 언리얼 에디터까지 꼭 종료해줘야 끊어지더라구요. stop버튼을 눌렀을 때 종료되는 것처럼 하려면 어떻게 해야 하나요?? standalone game으로 해봤을 때 제가 원하는 대로 동작해주긴 했는데 위에 두가지 경우에도 방법이 있을까 해서 질문드립니다.
SetupCharacterWidget 인터페이스 핵심 사용 이유가 액터와 위젯 생성 주기를 고려해 원하는 시점에 초기화 해주기 위함이 맞을까요? 인터페이스를 사용하지 않고, BeginPlay에 다음과 같이 코드를 작성해도 오류는 안 나더라고요. 대신 처음에 플레이어 HP바가 다 채워지지 않은 상태로 시작하는 버그는 있지만요. 아무래도 InitWidget , NativeConstruct 함수가 초기화되지 않았기 때문이겠죠? void AABCharacterBase::BeginPlay() { Super::BeginPlay(); UABHpBarWidget* HpBarWidget = Cast<UABHpBarWidget>(HpBar->GetWidget()); ensure(HpBarWidget); HpBarWidget->UpdateStat(Stat->GetBaseStat(), Stat->GetModifierStat()); HpBarWidget->UpdateHpBar(Stat->GetCurrentHp()); Stat->OnHpChanged.AddUObject(HpBarWidget, &UABHpBarWidget::UpdateHpBar); Stat->OnStatChanged.AddUObject(HpBarWidget, &UABHpBarWidget::UpdateStat); } 호출해도
아래와 같은 지점에서 저는 문제가 발생을 계속 하고 있는데 혹시 이유를 알수 있을까요?? 모듈러 로그를 활성화 하여 ContinueInitStateChain 쪽 로그 입니다. LogModularGameplay: Verbose: ContinueInitStateChain: Cannot transition B_SimpleHeroPawn_C_0:Hero (role 3) from InitState.DataAvailable to InitState.DataInitialized LogModularGameplay: Verbose: ContinueInitStateChain: Transitioning B_SimpleHeroPawn_C_0:PawnExtension (role 3) from InitState.DataAvailable to InitState.DataInitialized LogModularGameplay: Verbose: ContinueInitStateChain: Transitioning B_SimpleHeroPawn_C_0:PawnExtension (role 3) from InitState.DataInitialized to InitState.GameplayReady LogModularGameplay: Verbose: ContinueInitStateChain: Cannot transition B_SimpleHeroPawn_C_0:Hero (role 3) from InitState.DataAvailable to InitState.DataInitialized LogModularGameplay: Verbose: ContinueInitStateChain: Cannot transition B_SimpleHeroPawn_C_0:Hero (role 3) from InitState.DataAvailable to InitState.DataInitialized Cannot transition 이 발상한 조건문 체크는 // DataAvailable -> DataInitialized if (CurrentState == InitTags.InitState_DataAvailable && DesiredState == InitTags.InitState_DataInitialized) { // PawnExtensionComponent가 DataInitialized될 때까지 기다림 (== 모든 Feature Component가 DataAvailable인 상태) //return CLMPS && Manager->HasFeatureReachedInitState(Pawn, UCLMPawnExtensionComponent::NAME_ActorFeatureName, InitTags.InitState_GameplayReady); return CLMPS && Manager->HasFeatureReachedInitState(Pawn, UCLMPawnExtensionComponent::NAME_ActorFeatureName, InitTags.InitState_DataInitialized); } 라이라 원본으로 정상 작동을 진행하는데 코드를 몇번을 수정을 하여도 진척도가 없는데 혹시 원인을 알수 있을까요? InitState_GameplayReady 태그를 사용할 경우에는 정상 동작 합니다..
[C++과 언리얼로 만드는 MMORPG 게임 개발 시리즈] Part5: UE5 & IOCP 서버 연동
혹시 만약에 인벤토리을 관리할떄 서버가 아닌 클라에서 아이템 관리를 위해 아이템객체들을 전부 스마트포인터를 써야할지... 포션같은 소모아이템 떄문에 써야되지 않을까 하는데 너무 고민입니다. 현업에서도 GC를 적극적으로 사용을 하는지 서버가 있는데 AI 인공지능을 쓰는 경우가 있다면 어느 경우인지 이렇게 질문 3개입니다...
항상 강의 잘 보고있습니다. 감사합니다. 프로젝트 만들기 강의 영상 중 13:26초 부분에 문제가 있어 질문 남깁니다. 영상과 같이 저도 더블 클릭을 해서 The following modules are missing or built with a different engine version: R1 Would you like to rebuild them now? 해당 메시지를 yes를 눌렀지만 Binaries폴더가 생성되는 것이 아닌 아래와 같은 Error 메세지가 뜨면서 생성되지 않습니다. 해당 문제를 어떻게 해결하면 되는지 답변 주시면 감사하겠습니다. R1 could not be compiled. Try rebuilding from source manually.
part2에서 만든 모든 캐릭터들은 CharacterBase 클래스를 상속받아서 동작하고 있는데, 그럼 만약에 적 캐릭터들을 좀 더 플레이어와는 다르게, 적군 만의 어떤 공통적인 기능들이나 스탯 설정같은걸 추가하려면 플레이어용 PlayerCharacterBase와 EnemyCharacterBase 뭐 이런식으로 따로 베이스 클래스를 만들어서 작성하는게 나을까요, 아니면 그냥 기본 바탕은 CharacterBase로 통일시키되 override를 하거나 해서 만드는 것이 나을까요? 막상 베이스 클래스를 2개 만들어서 하려다보니 많이 비효율적인 방식인지 궁금해져서 질문드립니다
[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를 확인해보면 이미 삭제된 플레이어가 한번 더 들어오는 경우가 생기네요. 혹시 어떤 부분 때문일까요??
8강 아이템 시스템 마지막에 강참조된 아이템들을 약참조로 바꾸는 부분을 공부하다가 질문드립니다 인터넷에서 강참조와 약참조에 대해 좀 더 자세히 공부하다보니 강참조 = TStrongObjectPtr / 약참조 = TSoftObjectPtr 를 사용하며, TObjectPtr과 강참조가 관련된 정보는 없길래 TObjectPtr에 대해 궁금증이 생겼습니다 TObjectPtr에 대해 검색해본 바로는 TObjectPtr은 기존에 사용하던 원시 포인터를 대체하는 포인터를 이용한 단순한 참조에 불과하며, 그러므로 댕글링 포인터 문제 또한 발생할 수 있다. 보통 강참조라고 하는 것은, 객체의 소유권을 가지고 Ref Count를 올리는 역할을 수행해야 하는데, TObjectPtr은 단순히 객체를 참조하는 역할만 하므로 강참조라고 볼 수는 없다 라고 나와 있던데 강의에서 TObjectPtr이 강참조라고 하셨던 것은 어떤 의미인지 궁금합니다 그리고 만약에 TObjectPtr이 강참조가 아니라면, 이는 어떠한 정도의 참조인건지요...? 중참조? 이런 단어는 없는거같던데.. TObjectPtr은 TStrongObjectPtr(강참조) 와 TSoftObjectPtr(약참조) 사이의 어딘가에 있는 애매한 정도의 참조인것인지요?? 추가질문) 약참조를 공부하는데 약참조에도 또 TSoftObjectPtr과 TWeakObjectPtr의 2가지가 존재하더라구요... TSoftObjectPtr 리소스의 주소는 갖고 있되, 로딩은 시켜놓지 않는 경우에 사용 아직 현재 월드에 인스턴스화되지 않은 객체를 참조할 때 사용 TSoftObjectPtr은 FSoftObjectPath를 감싸는 TWeakObjectPtr이다 TWeakObjectPtr Ref Count를 증가시키지 않아야 하는 경우 / 소유권이 없지만 참조는 필요한 경우에 사용 참조하고 있는 오브젝트가 GC에 넘어간 경우, nullptr로 바뀌며 validation 체크를 가능하게 함 이렇게 알고있는데 제가 학습한 내용이 맞는건지요??
먼저, 강의 흐름대로 위젯 블루프린트를 하나 만든 후 'PbHpBar' f라는 ProgressBar를 추가하였으며 그 블루프린트의 부모를 ABHpBarWidget으로 설정해 둔 상태입니다. 여기서 궁금한 점은 ABHpBarWidget은 부모클래스이며, 따라서 자식클래스에서 생성한 'PbHpBar' 라는 위젯의 존재를 알지 못할텐데, 어떻게 HpProgressBar 에 'PbHpBar' 라는 위젯의 존재를 찾아서 주소를 반환할 수 있는것인지 궁금합니다. (만약 부모자식 관계가 바뀌어있다면, 자식에서 부모의 PbHpBar 라는 녀석의 존재를 찾아 포인트를 반환하는게 논리적으로 뭔가 납득이 가는데, 강의의 내용은 마치 상속관계가 반대의 상황인데도 불구하고 가능한것 처럼 보여집니다.) NativeConstruct() 라는 생성자는 제가 모르는 뭔가 특별한 기능을 가진 생성자이며, 따라서 부모클래스임에도 불구하고 자식클래스인 블루프린트 클래스에 있는 위젯의 정보들까지 미리 전부 탐색할 수 있기 때문에 자식에 있는 PbHpBar까지도 찾아서 포인터를 반환할 수 있는 것인가요?
player를 잘 쫓아 오긴하는데 방향이 뚝 뚝 뚝 끊기면서 전환되며 따라옵니다.. 제가 이해한 바로는 FindTargetService에서 Tick 마다 Target을 갱신해주어 매끄럽게 방향전환하며 따라와야 한다고 생각하는데 기본 제공해주는 태스크인 Move To의 문제일까요?? 강의 영상에서도 AI가 쫓아올 때 방향이 뚝 뚝 끊기는 것 같아서 저 만의 문제는 아닌듯 한데 개인적으로 궁금해서 여쭤봅니다! 태스크를 새로 만들어야하는지..아니면 다른 방법이 있는지 궁금합니다 !
5.1 버전의 강의를 다 듣고, 5.4 깃허브 업데이트 되어서 다시 복습하다가 제목과 같이 스킬 사용할 때 범위 시각화(배포용) 기능을 구현해보고 싶더라구요. 일단, 캐스캐이드가 Legacy 기능이다보니 나이아가라로 다른 스킬 구현은 완료했고, 스킬의 범위를 바닥에 표현하고 싶은데 GAS 프레임워크에서 스킬 범위를 표기할 때 좋은 class가 따로 구현되어 있을까요? 현재 나이아가라 데칼을 써서 해볼까 실험은 하고있는데, 정석적인 방법(?)이 있다면 먼저 그런 방법으로 해보고 싶어서 질문드려요.