3 분 소요

Unreal/C++ 개인 과제 - 경찰과 도둑

Git Page

과제 소개 및 목표

이번 프로젝트는 ‘개인 프로젝트’로서
이전 챕터의 복습을 위한 과제이다

  • 개인 프로젝트 주제
    • CH03 챕터에서 배운 언리얼 게임플레이 프레임워크 클래스를 활용하고,
      CH04 멀티플레이가 되게끔 로직을 작성합니다.
    • 레벨에 선량한 시민 AI NPC들이 돌아다닙니다.
      경찰과 도둑, 시민 AI NPC는 모두 같은 모습을 하고 돌아다닙니다.
    • 경찰은 도둑일거 같은 캐릭터를 때려잡고,
      도둑은 계속해서 도망다니거나 시민 AI NPC 사이에 숨어다닙니다.
    • 제한 시간 내에 도둑을 잡지 못하면 도둑 승리. 잡으면 경찰 승리입니다.
  • 개인 프로젝트 이유
    • 프로젝트 준비부터 프로젝트 마무리까지 혼자서 경험해보기 위함.
    • CH04 내용을 복습하기 위함.

구현 기능


구현요소

  • 이동 동기화
  • Match State 동기화
  • 공격/피격 동기화
  • 게임 결과 동기화
  • 아이템 생성 및 효과 적용 동기화

클래스 설명

TagNChase
- 로깅용 매크로를 넣어둔 파일

Animation
- BaseAnimInst : NPC가 사용하기 위하여 부모용 AnimInstance를 분리
- TaskAnimInstance : Player가 사용하는 AnimInstance (AimOffset)

Character
- BaseCharacter 
 : Player와 NPC가 같이 사용하기 위한 부모 캐릭터 클래스 (ACharacter)
   TakeDamage, OnDeath(Pure), StatusComponent를 가짐

- TaskCharacter
  : Player 용의 캐릭터 클래스, 입력 처리, 공격 등에 대한 처리 포함

- AICharacter
  : Ragdoll 용 함수가 포함된 NPC 클래스

UI
- UTNHPTextWidgetComponent 
  : 월드에 위젯을 띄우기 위한 클래스 
   (SetOnlyOwnerSee(true) / SetOwnerNoSee(false)) 설정을 통해
   자기 자신만이 볼 수 있도록 수정

- UTNStatusComponent
  : 체력 , 역할 등에 대한 데이터가 담긴 컴포넌트 클래스
    (변경에 따른 Delegate 포함)

Controller
- TaskTitlePlayerController : Title Level에서 활용하는 컨트롤러
- TaskPlayerController : UI 와 연동하는 기능을 추가한 PlayerController 클래스
- NPCController : AIController를 상속받은 클래스 (기초적인 BT 등에 대한 세팅 용도)

GameMode
- ATaskGameModeBase
  : 로그인 및 매치 흐름에 대한 관리용 게임모드 클래스
    로그인 한 플레이어들을 모으고, 역할 분리 및 게임 흐름 진행
    (다만, 게임 흐름 부분은 별도의 매니저를 두어도 좋았을 듯)

GameState
- ATaskGameStateBase
  : 매치 상태와 매치 시간을 가진 GameState 클래스

Item
- UItemSpawnSubsystem : 아이템을 Spawn 시키는 용도의 WorldSubsystem 클래스
- AItemSpawnVolume : 아이템을 Spawn 시킬 임의의 위치를 찾는 볼륨용 액터 클래스
- ATaskItem 
: 아이템 클래스 
  Player와 접촉 시, 사라지고 Role 에 따른 효과 부여
  (다만, 별도의 효과 클래스를 구현하지 않아 고정된 효과만 적용)

트러블 슈팅 1 - Enemy가 사망하였으니 Ragdoll이 발생하지 않는 상황

void AAICharacter::OnDeath()
{
    if (AAIController* AICon = Cast<AAIController>(GetController()))
    {
        AICon->StopMovement();

        if (UBrainComponent* Brain = AICon->GetBrainComponent())
        {
            Brain->StopLogic(TEXT("Dead"));
        }
    }

    if (UCharacterMovementComponent* MoveComp = GetCharacterMovement())
    {
        MoveComp->DisableMovement();
    }

    USkeletalMeshComponent* MeshComp = GetMesh();
    if (MeshComp)
    {
        GetCapsuleComponent()->SetCollisionEnabled(ECollisionEnabled::NoCollision);

        MeshComp->SetCollisionProfileName(TEXT("Ragdoll"));
        MeshComp->SetSimulatePhysics(true);
        MeshComp->SetAllBodiesSimulatePhysics(true);
        MeshComp->WakeAllRigidBodies();
    }

    DetachFromControllerPendingDestroy();

    SetLifeSpan(10.0f);

	ATaskGameModeBase* GameMode = Cast<ATaskGameModeBase>(UGameplayStatics::GetGameMode(this));
	if (HasAuthority() == true && IsValid(GameMode) == true)
	{
		GameMode->PaneltyPolice();
	}
}
  • NPC가 사용하는 코드로서
    경찰이 NPC를 공격할 시
    대미지를 받고 사망하여 호출되는 코드이다

  • 다만, ‘패널티’를 통한 경찰의 체력감소는 적용되었으나
    클라이언트에선 lagdoll 이 발생하지 않는 현상이 있었다

    • ‘동기화’ 문제!
    • 서버에선 처리되었으나, 클라이언트에선 처리되지 않은 문제!

해결방법

// AICharacter.h

protected:
	UPROPERTY(ReplicatedUsing = OnRep_IsDead)
	uint8 bIsDead : 1;

	UFUNCTION()
	void OnRep_IsDead();

// cpp
void AAICharacter::OnDeath()
{
    if (!HasAuthority())
        return;

    if (bIsDead)
        return;

    bIsDead = true;

    // 래그돌 관련 코드
    StartRagdoll();

    ATaskGameModeBase* TGM = Cast<ATaskGameModeBase>(UGameplayStatics::GetGameMode(this));
    if (IsValid(TGM))
    {
        TGM->PaneltyPolice();
    }
}

void AAICharacter::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
    Super::GetLifetimeReplicatedProps(OutLifetimeProps);

    DOREPLIFETIME(ThisClass, bIsDead);
}

void AAICharacter::OnRep_IsDead()
{
    if (bIsDead)
    {
        StartRagdoll();
    }
}
  • 죽음과 관련된 상태를
    저장하고, 해당 상태 변화에 따라
    OnRep 를 호출하여
    ‘클라이언트’에도 죽음의 상황을 공유하여 해결!

트러블 슈팅 2 - 설정한 모자가 보이지 않는 상황

//TaskCharacter.h

#pragma region Hats
	void ApplyRoleHat(ERoleType InRole);

protected:
	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Role|Hat")
	TObjectPtr<UStaticMeshComponent> RoleHatMesh;

	UPROPERTY(EditDefaultsOnly, Category = "Role|Hat")
	TObjectPtr<UStaticMesh> PoliceHatMesh;

	UPROPERTY(EditDefaultsOnly, Category = "Role|Hat")
	TObjectPtr<UStaticMesh> ThiefHatMesh;
#pragma endregion

//cpp

// BeginPlay
StatusComponent->OnRoleChanged.AddUObject(this, &ThisClass::ApplyRoleHat);

  • Role 에 따라 보여질 모자를 ‘다르게’ 수정하였으나
    모자 자체가 보이지 않는 상황!

  • 분명 BroadCast를 통해 Role 변화시 동적으로 호출되게 하였는데??
    -> Broadcast는 ‘해당 장치’ 내에서만 Broadcast 됨!
    • 어찌보면 당연하지만, BroadCast 되며 서버->클라 연동이 될것이라 생각…
      (Net Multicast 와 헷갈린듯…??)
  • 그렇기에 Status의 BroadCast를 클라에서도 적용되도록 수정하였다
// StatusComponent.h

UFUNCTION()
void OnRep_Role();

...

UPROPERTY(EditAnywhere, BlueprintReadOnly, ReplicatedUsing = OnRep_Role)
ERoleType Role;

// cpp
void UTNStatusComponent::OnRep_Role()
{
	OnRoleChanged.Broadcast(Role);
}
  • 이를 통해
    각 클라이언트에서 BroadCast를 호출하여
    모자에 대한 Mesh 설정을 제대로 할 수 있었다!

트러블 슈팅 3 - 스폰한 아이템이 보이지 않는 상황

  • TaskItem를 SpawnActor 로 생성하였으나
    보이지 않는 상황이었다

  • 처음에는 ‘생성’에 실패하였나?
    싶었으나 로그를 찍어보니 ‘서버’에서는 생성에 성공하였다

    • 그렇다… 또 클라이언트 동기화 문제였다
ATaskItem::ATaskItem()
{
...
	bReplicates = true;
	bAlwaysRelevant = true;
}
  • Replicate 를 true로 설정하며
    또한, 연관성이 있도록 설정
    • 아이템은 어떤 플레이어도 섭취할 수 있으므로
  • 아이템이 잘 보이게 되었다!

Image

  • 모자와 마찬가지로
    주어지는 역할에 따라
    메시와 효과적용을 다르게 하였다

댓글남기기