PART 12 - 강의 6/8

Co-op 멀티플레이어

원신 스타일 4인 협동 플레이 시스템 구현

01

Listen Server 아키텍처

호스트 기반 Co-op 세션 관리

C++
// CoopGameMode.h - Co-op 게임 모드 #pragma once #include "CoreMinimal.h" #include "GameFramework/GameModeBase.h" #include "CoopGameMode.generated.h" /** * 입장 권한 타입 */ UENUM(BlueprintType) enum class EJoinPermission : uint8 { Closed, // 입장 금지 FriendsOnly, // 친구만 WorldLevelOnly, // 같은 레벨 이상 Open // 모두에게 열림 }; /** * 호스트 월드 상태 */ USTRUCT(BlueprintType) struct FCoopWorldState { GENERATED_BODY() UPROPERTY(BlueprintReadWrite) int32 WorldLevel = 1; UPROPERTY(BlueprintReadWrite) TArray<FName> UnlockedRegions; UPROPERTY(BlueprintReadWrite) TArray<FName> CompletedQuests; UPROPERTY(BlueprintReadWrite) TMap<FName, bool> OpenedChests; }; DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam( FOnPlayerJoined, APlayerController*, NewPlayer ); DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam( FOnPlayerLeft, APlayerController*, LeavingPlayer ); UCLASS() class OPENWORLDRPG_API ACoopGameMode : public AGameModeBase { GENERATED_BODY() public: ACoopGameMode(); // 최대 플레이어 수 static constexpr int32 MAX_PLAYERS = 4; // 이벤트 UPROPERTY(BlueprintAssignable) FOnPlayerJoined OnPlayerJoined; UPROPERTY(BlueprintAssignable) FOnPlayerLeft OnPlayerLeft; // 호스트 월드 상태 (복제됨) UPROPERTY(Replicated, BlueprintReadOnly) FCoopWorldState WorldState; // 입장 권한 UPROPERTY(Replicated, BlueprintReadWrite) EJoinPermission JoinPermission = EJoinPermission::FriendsOnly; // 권한 변경 UFUNCTION(BlueprintCallable, Category = "Co-op") void SetJoinPermission(EJoinPermission NewPermission); // 플레이어 강퇴 UFUNCTION(BlueprintCallable, Category = "Co-op") void KickPlayer(APlayerController* PlayerToKick, const FText& Reason); // 현재 플레이어 수 UFUNCTION(BlueprintPure, Category = "Co-op") int32 GetCurrentPlayerCount() const; protected: virtual void PreLogin(const FString& Options, const FString& Address, const FUniqueNetIdRepl& UniqueId, FString& ErrorMessage) override; virtual void PostLogin(APlayerController* NewPlayer) override; virtual void Logout(AController* Exiting) override; virtual void GetLifetimeReplicatedProps( TArray<FLifetimeProperty>& OutLifetimeProps) const override; private: // 친구 확인 (플랫폼별 구현) bool IsFriend(const FUniqueNetIdRepl& PlayerId) const; // 월드 레벨 확인 bool MeetsWorldLevelRequirement(const FUniqueNetIdRepl& PlayerId) const; }; // CoopGameMode.cpp #include "CoopGameMode.h" #include "Net/UnrealNetwork.h" #include "CoopPlayerController.h" ACoopGameMode::ACoopGameMode() { bUseSeamlessTravel = true; PlayerControllerClass = ACoopPlayerController::StaticClass(); } void ACoopGameMode::GetLifetimeReplicatedProps( TArray<FLifetimeProperty>& OutLifetimeProps) const { Super::GetLifetimeReplicatedProps(OutLifetimeProps); DOREPLIFETIME(ACoopGameMode, WorldState); DOREPLIFETIME(ACoopGameMode, JoinPermission); } void ACoopGameMode::PreLogin(const FString& Options, const FString& Address, const FUniqueNetIdRepl& UniqueId, FString& ErrorMessage) { Super::PreLogin(Options, Address, UniqueId, ErrorMessage); // 기존 에러가 있으면 바로 반환 if (!ErrorMessage.IsEmpty()) { return; } // 인원 제한 체크 if (GetCurrentPlayerCount() >= MAX_PLAYERS) { ErrorMessage = TEXT("Session is full"); return; } // 입장 권한 체크 switch (JoinPermission) { case EJoinPermission::Closed: ErrorMessage = TEXT("Session is closed"); break; case EJoinPermission::FriendsOnly: if (!IsFriend(UniqueId)) { ErrorMessage = TEXT("Friends only session"); } break; case EJoinPermission::WorldLevelOnly: if (!MeetsWorldLevelRequirement(UniqueId)) { ErrorMessage = TEXT("World level too low"); } break; case EJoinPermission::Open: // 제한 없음 break; } } void ACoopGameMode::PostLogin(APlayerController* NewPlayer) { Super::PostLogin(NewPlayer); // 게스트에게 월드 상태 전송 if (ACoopPlayerController* CoopPC = Cast<ACoopPlayerController>(NewPlayer)) { if (!CoopPC->IsLocalController()) { CoopPC->Client_InitializeWorldState(WorldState); } } OnPlayerJoined.Broadcast(NewPlayer); // 호스트에게 알림 UI 표시 if (APlayerController* HostPC = GetWorld()->GetFirstPlayerController()) { if (ACoopPlayerController* CoopHostPC = Cast<ACoopPlayerController>(HostPC)) { CoopHostPC->ShowPlayerJoinedNotification(NewPlayer); } } } void ACoopGameMode::Logout(AController* Exiting) { if (APlayerController* PC = Cast<APlayerController>(Exiting)) { // 호스트 퇴장 시 if (PC->IsLocalController() && PC->HasAuthority()) { // 모든 클라이언트에게 세션 종료 알림 for (FConstPlayerControllerIterator It = GetWorld()->GetPlayerControllerIterator(); It; ++It) { if (ACoopPlayerController* CoopPC = Cast<ACoopPlayerController>(It->Get())) { if (!CoopPC->IsLocalController()) { CoopPC->Client_HostDisconnected(); } } } } OnPlayerLeft.Broadcast(PC); } Super::Logout(Exiting); }
02

네트워크 복제 설정

캐릭터 상태 동기화

C++
// CoopCharacter.h - 네트워크 동기화 캐릭터 #pragma once #include "CoreMinimal.h" #include "GameFramework/Character.h" #include "CoopCharacter.generated.h" UCLASS() class OPENWORLDRPG_API ACoopCharacter : public ACharacter { GENERATED_BODY() public: ACoopCharacter(); // ===== Server RPC - 클라이언트 -> 서버 ===== // 스킬 사용 요청 UFUNCTION(Server, Reliable, WithValidation) void Server_UseElementalSkill(); UFUNCTION(Server, Reliable, WithValidation) void Server_UseElementalBurst(); // 공격 요청 UFUNCTION(Server, Reliable, WithValidation) void Server_PerformAttack(int32 ComboIndex); // ===== Client RPC - 서버 -> 해당 클라이언트 ===== UFUNCTION(Client, Reliable) void Client_ShowDamageNumber(float Damage, FVector Location, bool bCritical); UFUNCTION(Client, Reliable) void Client_NotifySkillCooldown(float RemainingTime); // ===== Multicast RPC - 서버 -> 모든 클라이언트 ===== UFUNCTION(NetMulticast, Unreliable) void Multicast_PlaySkillEffect(UParticleSystem* Effect, FVector Location); UFUNCTION(NetMulticast, Unreliable) void Multicast_PlayAttackMontage(UAnimMontage* Montage); UFUNCTION(NetMulticast, Reliable) void Multicast_OnDeath(); protected: // 복제 속성 UPROPERTY(ReplicatedUsing = OnRep_Health) float Health = 100.0f; UPROPERTY(ReplicatedUsing = OnRep_Energy) float Energy = 0.0f; UPROPERTY(Replicated) float MaxHealth = 100.0f; UPROPERTY(Replicated) float MaxEnergy = 100.0f; UPROPERTY(Replicated) bool bIsAlive = true; // RepNotify 콜백 UFUNCTION() void OnRep_Health(); UFUNCTION() void OnRep_Energy(); virtual void GetLifetimeReplicatedProps( TArray<FLifetimeProperty>& OutLifetimeProps) const override; private: // 스킬 쿨다운 (서버에서만 관리) float ElementalSkillCooldown = 0.0f; float ElementalBurstCooldown = 0.0f; static constexpr float SKILL_COOLDOWN = 6.0f; static constexpr float BURST_ENERGY_COST = 80.0f; }; // CoopCharacter.cpp #include "CoopCharacter.h" #include "Net/UnrealNetwork.h" ACoopCharacter::ACoopCharacter() { // 복제 활성화 bReplicates = true; SetReplicateMovement(true); // 네트워크 업데이트 빈도 NetUpdateFrequency = 66.0f; MinNetUpdateFrequency = 33.0f; } void ACoopCharacter::GetLifetimeReplicatedProps( TArray<FLifetimeProperty>& OutLifetimeProps) const { Super::GetLifetimeReplicatedProps(OutLifetimeProps); // 모든 클라이언트에 복제 DOREPLIFETIME(ACoopCharacter, Health); DOREPLIFETIME(ACoopCharacter, Energy); DOREPLIFETIME(ACoopCharacter, MaxHealth); DOREPLIFETIME(ACoopCharacter, MaxEnergy); DOREPLIFETIME(ACoopCharacter, bIsAlive); } // ===== Server RPC 구현 ===== bool ACoopCharacter::Server_UseElementalSkill_Validate() { // 기본 유효성 검사 return bIsAlive; } void ACoopCharacter::Server_UseElementalSkill_Implementation() { // 쿨다운 체크 if (ElementalSkillCooldown > 0.0f) { // 클라이언트에게 쿨다운 알림 Client_NotifySkillCooldown(ElementalSkillCooldown); return; } // 스킬 실행 (서버에서) FVector SkillLocation = GetActorLocation() + GetActorForwardVector() * 300.0f; // 데미지 영역 생성 SpawnDamageArea(SkillLocation); // 에너지 충전 Energy = FMath::Min(Energy + 20.0f, MaxEnergy); // 쿨다운 설정 ElementalSkillCooldown = SKILL_COOLDOWN; // 모든 클라이언트에 이펙트 재생 Multicast_PlaySkillEffect(ElementalSkillParticle, SkillLocation); } bool ACoopCharacter::Server_UseElementalBurst_Validate() { return bIsAlive && Energy >= BURST_ENERGY_COST; } void ACoopCharacter::Server_UseElementalBurst_Implementation() { // 에너지 소비 Energy -= BURST_ENERGY_COST; // 궁극기 실행 PerformElementalBurst(); // 모든 클라이언트에 이펙트 Multicast_PlaySkillEffect(ElementalBurstParticle, GetActorLocation()); } // ===== Multicast RPC 구현 ===== void ACoopCharacter::Multicast_PlaySkillEffect_Implementation( UParticleSystem* Effect, FVector Location) { if (Effect) { UGameplayStatics::SpawnEmitterAtLocation( GetWorld(), Effect, Location, FRotator::ZeroRotator, FVector(1.0f), true ); } } void ACoopCharacter::Multicast_OnDeath_Implementation() { // 모든 클라이언트에서 사망 처리 bIsAlive = false; // 래그돌 활성화 GetMesh()->SetSimulatePhysics(true); GetMesh()->SetCollisionProfileName(TEXT("Ragdoll")); // 사망 이펙트 UGameplayStatics::SpawnEmitterAtLocation( GetWorld(), DeathParticle, GetActorLocation() ); } // RepNotify 구현 void ACoopCharacter::OnRep_Health() { // UI 업데이트 UpdateHealthUI(); // 사망 체크 if (Health <= 0.0f && bIsAlive) { // 클라이언트 측 사망 처리는 Multicast에서 } }
03

세션 관리 시스템

Online Subsystem을 활용한 매칭

C++
// CoopSessionManager.h #pragma once #include "CoreMinimal.h" #include "Subsystems/GameInstanceSubsystem.h" #include "Interfaces/OnlineSessionInterface.h" #include "CoopSessionManager.generated.h" DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam( FOnSessionCreated, bool, bSuccess); DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam( FOnSessionsFound, const TArray<FBlueprintSessionResult>&, Sessions); DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams( FOnJoinSessionComplete, bool, bSuccess, const FString&, ErrorMessage); UCLASS() class OPENWORLDRPG_API UCoopSessionManager : public UGameInstanceSubsystem { GENERATED_BODY() public: virtual void Initialize(FSubsystemCollectionBase& Collection) override; virtual void Deinitialize() override; // 이벤트 UPROPERTY(BlueprintAssignable) FOnSessionCreated OnSessionCreated; UPROPERTY(BlueprintAssignable) FOnSessionsFound OnSessionsFound; UPROPERTY(BlueprintAssignable) FOnJoinSessionComplete OnJoinSessionComplete; // 세션 생성 (호스트) UFUNCTION(BlueprintCallable, Category = "Session") void CreateCoopSession(int32 MaxPlayers = 4); // 세션 찾기 UFUNCTION(BlueprintCallable, Category = "Session") void FindCoopSessions(); // 세션 참가 UFUNCTION(BlueprintCallable, Category = "Session") void JoinCoopSession(const FBlueprintSessionResult& Session); // 세션 나가기 UFUNCTION(BlueprintCallable, Category = "Session") void LeaveCoopSession(); // 현재 세션 상태 UFUNCTION(BlueprintPure, Category = "Session") bool IsInSession() const; UFUNCTION(BlueprintPure, Category = "Session") bool IsSessionHost() const; protected: // Online Subsystem 인터페이스 IOnlineSessionPtr SessionInterface; // 검색 결과 TSharedPtr<FOnlineSessionSearch> SessionSearch; // 콜백 void OnCreateSessionComplete(FName SessionName, bool bSuccess); void OnFindSessionsComplete(bool bSuccess); void OnJoinSessionComplete(FName SessionName, EOnJoinSessionCompleteResult::Type Result); void OnDestroySessionComplete(FName SessionName, bool bSuccess); private: FDelegateHandle CreateSessionHandle; FDelegateHandle FindSessionsHandle; FDelegateHandle JoinSessionHandle; FDelegateHandle DestroySessionHandle; bool bIsHost = false; }; // CoopSessionManager.cpp void UCoopSessionManager::CreateCoopSession(int32 MaxPlayers) { if (!SessionInterface.IsValid()) { OnSessionCreated.Broadcast(false); return; } // 기존 세션 정리 if (SessionInterface->GetNamedSession(NAME_GameSession)) { SessionInterface->DestroySession(NAME_GameSession); } // 세션 설정 FOnlineSessionSettings Settings; Settings.bIsLANMatch = false; Settings.NumPublicConnections = MaxPlayers; Settings.bAllowJoinInProgress = true; Settings.bAllowJoinViaPresence = true; Settings.bShouldAdvertise = true; Settings.bUsesPresence = true; Settings.bUseLobbiesIfAvailable = true; // 커스텀 설정 Settings.Set(SETTING_MAPNAME, FString("OpenWorld"), EOnlineDataAdvertisementType::ViaOnlineService); Settings.Set(FName("GameType"), FString("CoOp"), EOnlineDataAdvertisementType::ViaOnlineService); // 콜백 바인딩 CreateSessionHandle = SessionInterface->AddOnCreateSessionCompleteDelegate_Handle( FOnCreateSessionCompleteDelegate::CreateUObject( this, &UCoopSessionManager::OnCreateSessionComplete)); // 세션 생성 const ULocalPlayer* LocalPlayer = GetWorld()->GetFirstLocalPlayerFromController(); if (LocalPlayer) { SessionInterface->CreateSession( *LocalPlayer->GetPreferredUniqueNetId(), NAME_GameSession, Settings); } } void UCoopSessionManager::OnCreateSessionComplete( FName SessionName, bool bSuccess) { SessionInterface->ClearOnCreateSessionCompleteDelegate_Handle( CreateSessionHandle); if (bSuccess) { bIsHost = true; // Listen 서버로 맵 로드 GetWorld()->ServerTravel("/Game/Maps/OpenWorld?listen"); } OnSessionCreated.Broadcast(bSuccess); } void UCoopSessionManager::JoinCoopSession( const FBlueprintSessionResult& Session) { if (!SessionInterface.IsValid()) { OnJoinSessionComplete.Broadcast(false, "No session interface"); return; } // 콜백 바인딩 JoinSessionHandle = SessionInterface->AddOnJoinSessionCompleteDelegate_Handle( FOnJoinSessionCompleteDelegate::CreateUObject( this, &UCoopSessionManager::OnJoinSessionComplete)); const ULocalPlayer* LocalPlayer = GetWorld()->GetFirstLocalPlayerFromController(); if (LocalPlayer) { SessionInterface->JoinSession( *LocalPlayer->GetPreferredUniqueNetId(), NAME_GameSession, Session.OnlineResult); } } void UCoopSessionManager::OnJoinSessionComplete( FName SessionName, EOnJoinSessionCompleteResult::Type Result) { SessionInterface->ClearOnJoinSessionCompleteDelegate_Handle(JoinSessionHandle); if (Result == EOnJoinSessionCompleteResult::Success) { // 서버 주소 가져오기 FString ConnectString; if (SessionInterface->GetResolvedConnectString(SessionName, ConnectString)) { bIsHost = false; // 서버에 연결 APlayerController* PC = GetWorld()->GetFirstPlayerController(); if (PC) { PC->ClientTravel(ConnectString, TRAVEL_Absolute); } OnJoinSessionComplete.Broadcast(true, FString()); return; } } OnJoinSessionComplete.Broadcast(false, "Failed to join session"); }
04

네트워크 동기화 디버깅

Net Emulation, 네트워크 프로파일링, 동기화 이슈 해결

멀티플레이어 게임에서 네트워크 동기화 문제는 가장 디버깅하기 어려운 버그입니다. UE5는 Net Emulation, Network Profiler, 그리고 다양한 콘솔 명령어를 통해 네트워크 문제를 진단하고 해결할 수 있는 도구를 제공합니다.

Net Emulation 설정

C++
// CoopNetworkDebugger.h #pragma once #include "CoreMinimal.h" #include "Subsystems/GameInstanceSubsystem.h" #include "CoopNetworkDebugger.generated.h" /** * 네트워크 에뮬레이션 프로파일 * 다양한 네트워크 환경을 시뮬레이션 */ USTRUCT(BlueprintType) struct FNetEmulationProfile { GENERATED_BODY() UPROPERTY(EditAnywhere, BlueprintReadWrite) FString ProfileName; // 패킷 지연 (ms) UPROPERTY(EditAnywhere, BlueprintReadWrite) int32 InPacketLatency = 0; UPROPERTY(EditAnywhere, BlueprintReadWrite) int32 OutPacketLatency = 0; // 패킷 손실률 (%) UPROPERTY(EditAnywhere, BlueprintReadWrite) int32 InPacketLoss = 0; UPROPERTY(EditAnywhere, BlueprintReadWrite) int32 OutPacketLoss = 0; // 지터 (ms) UPROPERTY(EditAnywhere, BlueprintReadWrite) int32 Jitter = 0; }; UCLASS() class OPENWORLDRPG_API UCoopNetworkDebugger : public UGameInstanceSubsystem { GENERATED_BODY() public: virtual void Initialize(FSubsystemCollectionBase& Collection) override; // Net Emulation 프로파일 적용 UFUNCTION(BlueprintCallable, Category = "Network|Debug") void ApplyNetEmulationProfile(const FNetEmulationProfile& Profile); // Net Emulation 비활성화 UFUNCTION(BlueprintCallable, Category = "Network|Debug") void DisableNetEmulation(); // 네트워크 통계 수집 UFUNCTION(BlueprintCallable, Category = "Network|Debug") void StartNetworkProfiling(); UFUNCTION(BlueprintCallable, Category = "Network|Debug") void StopNetworkProfiling(); // 복제 상태 덤프 UFUNCTION(BlueprintCallable, Category = "Network|Debug") void DumpReplicationState(AActor* TargetActor); protected: // 사전 정의 프로파일 TMap<FString, FNetEmulationProfile> PresetProfiles; void SetupPresetProfiles(); };
C++
// CoopNetworkDebugger.cpp #include "CoopNetworkDebugger.h" #include "Engine/NetDriver.h" void UCoopNetworkDebugger::Initialize( FSubsystemCollectionBase& Collection) { Super::Initialize(Collection); SetupPresetProfiles(); } void UCoopNetworkDebugger::SetupPresetProfiles() { // 양호한 네트워크 (광대역) FNetEmulationProfile GoodProfile; GoodProfile.ProfileName = "Good"; GoodProfile.InPacketLatency = 20; GoodProfile.OutPacketLatency = 20; GoodProfile.InPacketLoss = 0; PresetProfiles.Add("Good", GoodProfile); // 보통 (모바일 네트워크) FNetEmulationProfile AverageProfile; AverageProfile.ProfileName = "Average"; AverageProfile.InPacketLatency = 80; AverageProfile.OutPacketLatency = 80; AverageProfile.InPacketLoss = 2; AverageProfile.Jitter = 20; PresetProfiles.Add("Average", AverageProfile); // 나쁜 환경 (고지연, 패킷 손실) FNetEmulationProfile BadProfile; BadProfile.ProfileName = "Bad"; BadProfile.InPacketLatency = 200; BadProfile.OutPacketLatency = 200; BadProfile.InPacketLoss = 5; BadProfile.OutPacketLoss = 3; BadProfile.Jitter = 50; PresetProfiles.Add("Bad", BadProfile); } void UCoopNetworkDebugger::ApplyNetEmulationProfile( const FNetEmulationProfile& Profile) { UWorld* World = GetWorld(); if (!World) return; // 콘솔 명령으로 Net Emulation 설정 World->Exec(World, *FString::Printf( TEXT("Net PktLag=%d"), Profile.InPacketLatency)); World->Exec(World, *FString::Printf( TEXT("Net PktLagVariance=%d"), Profile.Jitter)); World->Exec(World, *FString::Printf( TEXT("Net PktLoss=%d"), Profile.InPacketLoss)); UE_LOG(LogCoopNetwork, Log, TEXT("Net Emulation 적용: %s (Latency=%dms, Loss=%d%%)"), *Profile.ProfileName, Profile.InPacketLatency, Profile.InPacketLoss); } void UCoopNetworkDebugger::DumpReplicationState( AActor* TargetActor) { if (!TargetActor) return; UE_LOG(LogCoopNetwork, Log, TEXT("=== Replication State: %s ==="), *TargetActor->GetName()); UE_LOG(LogCoopNetwork, Log, TEXT(" bReplicates: %s"), TargetActor->bReplicates ? TEXT("true") : TEXT("false")); UE_LOG(LogCoopNetwork, Log, TEXT(" Role: %s, RemoteRole: %s"), *UEnum::GetValueAsString(TargetActor->GetLocalRole()), *UEnum::GetValueAsString(TargetActor->GetRemoteRole())); UE_LOG(LogCoopNetwork, Log, TEXT(" NetUpdateFrequency: %.1f"), TargetActor->NetUpdateFrequency); UE_LOG(LogCoopNetwork, Log, TEXT(" Owner: %s"), TargetActor->GetOwner() ? *TargetActor->GetOwner()->GetName() : TEXT("None")); }

자주 발생하는 동기화 이슈

주요 콘솔 명령어
  • Net PktLag=100 - 100ms 인공 지연 추가
  • Net PktLoss=5 - 5% 패킷 손실 시뮬레이션
  • Net PktLagVariance=30 - 30ms 지터 추가
  • stat net - 실시간 네트워크 통계 표시
  • net.ListActorChannels - 활성 액터 채널 목록
  • LogNetTraffic Verbose - 네트워크 트래픽 상세 로깅
흔한 동기화 버그와 해결법
  • 클라이언트에서 속성이 업데이트되지 않음: GetLifetimeReplicatedPropsDOREPLIFETIME 등록 여부 확인. Replicated 지정자와 실제 등록이 모두 필요합니다.
  • RPC가 호출되지 않음: 액터의 bReplicates = true 확인. Server RPC는 소유 클라이언트만 호출 가능, NetMulticast는 서버에서만 호출해야 합니다.
  • 이동이 버벅거림 (러버밴딩): NetUpdateFrequency 값을 높이고, CharacterMovementComponentNetworkSmoothingMode를 확인하세요.
  • 스폰된 액터가 클라이언트에 없음: 서버에서만 스폰하고 bReplicates = true인지 확인. SpawnActor는 서버에서 호출해야 자동 복제됩니다.
  • 호스트 이탈 시 크래시: OnNetCleanupHandleDisconnect를 구현하여 연결 해제를 graceful하게 처리하세요.
SUMMARY

핵심 요약

  • Listen Server 아키텍처로 4인 Co-op 구현
  • 서버 권한 모델 - 중요 로직은 서버에서만
  • Server RPC + WithValidation으로 치트 방지
  • Multicast RPC로 이펙트 동기화
  • Online Subsystem으로 세션 관리
PRACTICE

도전 과제

배운 내용을 직접 실습해보세요

실습 1: Co-op 세션 구현

Listen Server 기반 2-4인 Co-op을 구현하세요. 세션 생성(CreateSession), 참가(JoinSession), 트래블(ServerTravel)의 전체 플로우를 완성하세요.

실습 2: 멀티플레이어 전투 동기화

GAS 기반 전투가 네트워크에서 올바르게 작동하도록 하세요. 데미지 계산은 서버에서, 이펙트/사운드는 Multicast로, 체력 UI는 OnRep으로 동기화하세요.

심화 과제: Co-op 종합 테스트

2인 이상으로 플레이하면서 파티 시스템, 전투 동기화, 퀘스트 공유, 인벤토리 거래를 테스트하세요. Net Emulation으로 지연을 추가하고, 에지 케이스(동시 공격, 접속 끊김, 호스트 이탈)를 검증하세요.