futurelabunseen / A-SeunghyunHong

0 stars 1 forks source link

[Bug] RPC 파라미터가 nullptr가 되는 문제 #2

Open strurao opened 2 months ago

strurao commented 2 months ago

리슨서버 멀티플레이 게임에서 ClientRPC 또는 MulticastRPC의 파라미터로 액터의 포인터를 전달할 때 nullptr가 되어서, 아래 영상처럼 애니메이션 동기화가 제대로 되지 않고 있습니다.

스킬 발동 처리 관련 함수

void AGOPlayerCharacter::ActivateSkill(ASkillSlot* InCurrentSkillSlot)
{
    if (!HasAuthority())
    {
        // 클라이언트의 애니메이션이 잘 실행됩니다.
        PlaySkillAnim(InCurrentSkillSlot); 
    }

    // 서버에게 명령을 보내는 ServerRPC의 호출에서는 
    // InCurrenSkillSlot이 null이 되지 않고 잘 전달됩니다.
    ServerRPCAttackNew(GetWorld()->GetGameState()->GetServerWorldTimeSeconds(), InCurrentSkillSlot);
}

ServerRPC

void AGOPlayerCharacter::ServerRPCAttackNew_Implementation(float AttackStartTime, ASkillSlot* InSkillSlot)
{
    GO_LOG(LogGONetwork, Log, TEXT("%s"), TEXT("Begin"));

    ...

    // 애니메이션이 잘 실행됩니다.
    PlaySkillAnim(InSkillSlot);

    // 아래 for 문 대신 MulticastRPC를 실행해도 nullptr로 넘어가고 있습니다.
    // MulticastRPCAttackNew(InSkillSlot);

    for (APlayerController* PlayerController : TActorRange<APlayerController>(GetWorld()))
    {
        if (PlayerController && GetController() != PlayerController)
        {
            if (!PlayerController->IsLocalController())
            {
                // 이 조건문을 통과한 컨트롤러는 simulated proxy로,
                // 캐릭터를 재생하는 다른 플레이어 컨트롤러입니다.
                AGOPlayerCharacter* OtherPlayer = Cast<AGOPlayerCharacter>(PlayerController->GetPawn());
                if (OtherPlayer)
                {
                    if(InSkillSlot == nullptr)
                    {
                        // 아직 이 조건으로 들어오지는 않습니다. 
                        UE_LOG(LogTemp, Log, TEXT("InSkillSlot: %s "), *InSkillSlot->GetSkillInstance()->GetName());
                    }
                    OtherPlayer->ClientRPCPlaySkillAnimation(this, InSkillSlot);
                }
            }
        }
    }
}

MulticastRPC

void AGOPlayerCharacter::MulticastRPCAttackNew_Implementation(ASkillSlot* InSkillSlot)
{
    if (!IsLocallyControlled())
    {
        if (InSkillSlot == nullptr)
        {
            // 이 조건으로만 들어오게 됩니다.
            UE_LOG(LogTemp, Log, TEXT("InSkillSlot is null"));
        }

        // 현재 클라이언트는 이미 모션을 재생했으므로
        // 다른 클라이언트의 Proxy로써 동작하는 캐릭터에 대해서만 모션을 재생시킵니다.
        PlaySkillAnim(InSkillSlot); 
    }
}

ClientRPC

void AGOPlayerCharacter::ClientRPCPlaySkillAnimation_Implementation(AGOPlayerCharacter* CharacterToPlay, ASkillSlot* InSkillSlot)
{
    if (CharacterToPlay)
    {
        if (InSkillSlot == nullptr)
        {
            // 이 조건으로만 들어오게 됩니다.
            UE_LOG(LogTemp, Warning, TEXT("InSkillSlot is null."));
            return;
        }

        UE_LOG(LogTemp, Log, TEXT("InSkillSlot name: %s "), *InSkillSlot->GetSkillInstance()->GetTotalSkillData().SkillAnim->GetName());
        CharacterToPlay->PlaySkillAnim(InSkillSlot);
    }
}

만약 위 ClientRPC 함수에서 nullptr 시 return 하는 코드를 제외하면 UE_LOG에서 크래시가 나옵니다

strurao commented 2 months ago

참고를 위한 플레이 영상입니다. (위: 서버, 아래 둘: 클라이언트) https://github.com/futurelabunseen/A-SeunghyunHong/assets/126440235/d3da02b1-1be6-40e7-915f-4a08dfab6a9a

strurao commented 2 months ago

참고 중인 자료입니다.

strurao commented 2 months ago

언리얼 포럼에서 찾아보니 Unreal Engine에서 흔히 볼 수 있는 네트워크 복제와 관련된 문제인 것 같습니다. 리플리케이션 문제와 관련이 있는 것 같고, RPC를 사용하여 네트워크를 통해 액터의 포인터를 전달할 때, 해당 액터가 모든 클라이언트에 제대로 복제되지 않았거나 네트워크 복제 준비가 완료되지 않은 경우 발생하는 이슈를 찾아볼 수 있었습니다.

strurao commented 2 months ago

문제에 대한 답:

런타임에 생성한 액터는 파라미터로 못건내줍니다. 왜냐하면 인스턴스는 메모리에 올라와있는거니까, 각자의 컴퓨터는 동일한 대상에 대하여, 다른 메모리에 인스턴스를 가지고 있습니다. 그런데 네트워크로 건네주는건 인스턴스가 아니라 "데이터"죠,

즉, 인스턴스를 넘겨주려고 할게 아니라 SkillSlot1 은 Id 1이라고 하고, 2번슬롯은 id가 2라고할때 통신과정에선, 1번슬롯눌렷대! 하고 1을 건네주면 그건 클라나 서버나 동일하게 작동할 것입니다.

즉, Enum이나 int 처럼 데이터인 애를 파라미터로 던져줘야한다는 것입니다. 클라1 -> 서버에게 (ServerRPC) : 나 1번스킬 사용할거야! 서버 -> 클라1, 클라2 에게 : 클라1이 1번스킬눌렀대!!! 클라1의 대답: 클라1(나)가 조종하는 캐릭터의 1번스킬을 사용하자. 클라2의 대답: 클라1(다른클라)가 조종하는 캐릭터의 1번스킬을 사용하자.

사전적으로, 서버가 클라가 입장할때마다, 1번스킬에 스킬을 끼워줘야겠죠. 클라1입장시, 서버 -> 클라1 : 너 1번스킬은 이거야 클라2입장시, 서버->클라2 : 클라1의 1번스킬은 이거래 저기서 동일한 스킬을 사전에 넣어줬으면 문제없이 동작하겟죠

strurao commented 2 months ago

문제에 대한 답2:

RPC 파라미터로 포인터는 적합하지 않습니다. 포인터는 객체가 할당된 메모리의 주소값일 뿐, 해당 값을 전달한다고 해도, 전달 받은 다른 클라이언트들은 각각 다르게 할당하였기 때문에 객체를 찾을 수 없어요.

퀵슬롯 1 ~ 5번까지 객체의 정보를 리플리케이트되고 있는지 모르겠는데 리플리케이트되고 있다면, 0 ~ 4 퀵슬롯 배열의 인덱스를 전달하는 방식으로… 정보가 없다면, 스킬자체의 정보를 직접 축약해서 넘기는 방식으로 처리하면 될 것 같습니다. 스킬테이블이 있다면 스킬아이디와 다이나믹한 정보를 구조체로 전달하면 괜찮을 것 같아요.

strurao commented 2 months ago

TMap, Subsytem, DataTable, Enum 을 사용해보려고 합니다!