캐릭터와 AI 캐릭터를 스테이트 머신 모델로 구현
스테이트 종류
PREINIT 스테이트 : 캐릭터 생성 전의 스테이트, 캐릭터와 UI 숨김
LOADING 스테이트 : 선택한 캐릭터 애셋 로딩 스테이트
로딩 완료 전까지 플레이어 컨트롤러 입력 부분 비활성화
READY 스테이트 : 캐릭터 애셋 로딩 완료, 캐릭터 및 UI 활성화,
플레이어 컨트롤러 동작, AI 컨트롤러 비헤이비어 트리 로직 구동
DEAD 스테이트 : 캐릭터 HP 소진해 사망 스테이트
플레이어 컨트롤러는 입력 중지, UI 비활성화, 충돌 비활성화
AI 컨트롤러는 비헤이비어 트리 로직 중단
일정 시간 후 플레이어의 경우 재시작, AI 는 퇴장
ArenaBattle.h
캐릭터 스테이스 열거형 정의
C++, 블루 프린트에서 사용 할 수 있게 정의
UENUM(BlueprintType)
enum class ECharacterState : uint8
{
PREINIT,
LOADING,
READY,
DEAD
};
DECLARE_LOG_CATEGORY_EXTERN(ArenaBattle, Log, All);
ABCharacter.h
캐릭터 상태 변수 추가 및 함수 셋팅
UCLASS()
class ARENABATTLE_API AABCharacter : public ACharacter
{
GENERATED_BODY()
public:
//캐릭터 상태 Set, get
void SetCharacterState(ECharacterState NewState);
ECharacterState GetCharacterState() const;
private:
//캐릭터 생성 에셋 인덱스
int32 AssetIndex = 0;
//현재 캐릭터 상태
UPROPERTY(Transient, VisibleInstanceOnly, BlueprintReadOnly, Category = State, Meta = (AllowPrivateAccess = true))
ECharacterState CurrentState;
//플레이어 인지 판단
UPROPERTY(Transient, VisibleInstanceOnly, BlueprintReadOnly, Category = State, Meta = (AllowPrivateAccess = true))
bool bIsPlayer;
UPROPERTY()
class AABAIController* ABAIController;
UPROPERTY()
class AABPlayerController* ABPlayerController;
};
ABCharacter.cpp
PREINIT 스테이트
캐릭터는 플레이어 인지 판단 하는 부분을 BeginPlay 로 사용
현재 구성은 PossessedBy 함수는 2번 호출 (플레이어에 AI 컨트롤러 자동 부착)
BeginPlay 호출 하면 플레이어 또는 NPC 인지 판단 해 IsPlayer 변수에 지정
캐릭터 애셋 로딩을 시작 하고 LOADING로 변경
LOADING 스테이트에서 캐릭터 애셋 로딩이 완료 되면
READY 스테이트로 변경 플레이어 동작 및 게임 진행
게임 진행 도중 HP 0 되면 DEAD 스테이트로 변경
#include "ABPlayerController.h"
AABCharacter::AABCharacter()
{
AssetIndex = 4;
//캐릭터, hp bar 인게임 비활성화
SetActorHiddenInGame(true);
HPBarWidget->SetHiddenInGame(true);
bCanBeDamaged = false;
}
void AABCharacter::BeginPlay()
{
Super::BeginPlay();
//플레이어 컨트롤러인지 판단
bIsPlayer = IsPlayerControlled();
if (bIsPlayer)
{
ABPlayerController = Cast<AABPlayerController>(GetController());
ABCHECK(nullptr != ABPlayerController);
}
else
{
ABAIController = Cast<AABAIController>(GetController());
ABCHECK(nullptr != ABAIController);
}
//INI 파일에서 데이터 로드된 UABCharacterSetting 참조
auto DefaultSetting = GetDefault<UABCharacterSetting>();
if (bIsPlayer)
{
AssetIndex = 4;
}
else
{
AssetIndex = FMath::RandRange(0, DefaultSetting->CharacterAssets.Num() - 1);
}
//AssetIndex 값으로 캐릭터 애셋 지정
CharacterAssetToLoad = DefaultSetting->CharacterAssets[AssetIndex];
//GameInstance 객체 참조
auto ABGameInstance = Cast<UABGameInstance>(GetGameInstance());
ABCHECK(nullptr != ABGameInstance);
//비동기 로드 실행, 경로, 델리게이트를 사용해 오브젝트 생성후 OnAssetLoadCompleted() 실행
AssetStreamingHandle = ABGameInstance->StreamableManager.RequestAsyncLoad(CharacterAssetToLoad,
FStreamableDelegate::CreateUObject(this, &AABCharacter::OnAssetLoadCompleted));
//로드로 상태 변경
SetCharacterState(ECharacterState::LOADING);
}
void AABCharacter::SetCharacterState(ECharacterState NewState)
{
//현재 상태와 새로운 상태 비교
ABCHECK(CurrentState != NewState);
CurrentState = NewState;
switch (CurrentState)
{
case ECharacterState::LOADING:
{
//캐릭터 및 UI 숨김, 데미지 충돌 안함
SetActorHiddenInGame(true);
HPBarWidget->SetHiddenInGame(true);
bCanBeDamaged = false;
break;
}
case ECharacterState::READY:
{
//캐릭터 및 UI 활성화, 데미지 충돌 활성화
SetActorHiddenInGame(false);
HPBarWidget->SetHiddenInGame(false);
bCanBeDamaged = true;
//HP가 0경우 발생 하는 델리게이트에 DEAD 상태 지정 하게 등록
CharacterStat->OnHPIsZero.AddLambda([this]() -> void {
SetCharacterState(ECharacterState::DEAD);
});
auto CharacterWidget = Cast<UABCharacterWidget>(HPBarWidget->GetUserWidgetObject());
ABCHECK(nullptr != CharacterWidget);
//스탯이랑 HP bar 연결
CharacterWidget->BindCharacterStat(CharacterStat);
break;
}
case ECharacterState::DEAD:
{
//충돌 체크 비활성화, 메쉬는 활성화, HP bar 비활성화
SetActorEnableCollision(false);
GetMesh()->SetHiddenInGame(false);
HPBarWidget->SetHiddenInGame(true);
//죽는 애니메이션 실행, 데미지 충돌 체크 비활성화
ABAnim->SetDeadAnim();
bCanBeDamaged = false;
break;
}
}
}
ECharacterState AABCharacter::GetCharacterState() const
{
return CurrentState;
}
void AABCharacter::OnAssetLoadCompleted()
{
//로드 된 데이터를 스켈레톤 메쉬로 캐스트
USkeletalMesh* AssetLoaded = Cast<USkeletalMesh>(AssetStreamingHandle->GetLoadedAsset());
//핸들 초기화
AssetStreamingHandle.Reset();
ABCHECK(AssetLoaded != nullptr)
//현재 메쉬 스켈레톤으로 지정
GetMesh()->SetSkeletalMesh(AssetLoaded);
//캐릭터 애셋 로드 완료 했으니 READY 상태 변경
SetCharacterState(ECharacterState::READY);
}
ABAICharacter
스테이트에 맞게 비헤이비어 트리 로직을 수동으로 정지, 시작하게 구조 변경
void AABAIController::Possess(APawn* InPawn)
{
Super::Possess(InPawn);
}
void AABAIController::RunAI()
{
//Blackboard 맞는 지 확인
if (UseBlackboard(BBAsset, Blackboard))
{
//블랙 보드의 위치 값 갱신
Blackboard->SetValueAsVector(HomePosKey, GetPawn()->GetActorLocation());
//비헤어비어 트리 실행
if (!RunBehaviorTree(BTAsset))
{
ABLOG(Error, TEXT("AIController couldn't run behavior tree!"));
}
}
}
void AABAIController::StopAI()
{
//AAIController 의 매개변수를 이용해 UBehaviorTreeComponent 캐스팅
auto BehaviorTreeComponent = Cast<UBehaviorTreeComponent>(BrainComponent);
if (nullptr != BehaviorTreeComponent)
{
//정지
BehaviorTreeComponent->StopTree(EBTStopMode::Safe);
}
}
ABCharacter
기존의 PossessedBy 에서 처리하는 부분을 READY 스테이트에서 구현
READY 스테이트에서 플레이어 경우 입력 활성화, AI 경우 비헤이비어 트리 구동
DEAD 스테이트 플레이어 경우 입력 비 활성화, AI 경우 비헤어비어 트리 정지
사망한 이후 처리할 로직을 타이머로 구현
5초 후 플레이어 사망 시 레벨 리스타트, NCP 엑터 일 경우 삭제
UCLASS()
class ARENABATTLE_API AABCharacter : public ACharacter
{
GENERATED_BODY()
private:
//죽은 후 동작 하는 타이머 실행 시간
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = State, Meta = (AllowPrivateAccess = true))
float DeadTimer;
//죽은 후 동작 하는 타이머
FTimerHandle DeadTimerHandle = { };
}
AABCharacter::AABCharacter()
{
DeadTimer = 5.0f;
}
void AABCharacter::SetCharacterState(ECharacterState NewState)
{
//현재 상태와 새로운 상태 비교
ABCHECK(CurrentState != NewState);
CurrentState = NewState;
switch (CurrentState)
{
case ECharacterState::LOADING:
{
if (bIsPlayer)
{
//플레이어 컨트롤러 입력 비활성화
DisableInput(ABPlayerController);
}
//캐릭터 및 UI 숨김, 데미지 충돌 안함
SetActorHiddenInGame(true);
HPBarWidget->SetHiddenInGame(true);
bCanBeDamaged = false;
break;
}
case ECharacterState::READY:
{
//캐릭터 및 UI 활성화, 데미지 충돌 활성화
SetActorHiddenInGame(false);
HPBarWidget->SetHiddenInGame(false);
bCanBeDamaged = true;
//HP가 0경우 발생 하는 델리게이트에 DEAD 상태 지정 하게 등록
CharacterStat->OnHPIsZero.AddLambda([this]() -> void {
SetCharacterState(ECharacterState::DEAD);
});
auto CharacterWidget = Cast<UABCharacterWidget>(HPBarWidget->GetUserWidgetObject());
ABCHECK(nullptr != CharacterWidget);
//스탯이랑 HP bar 연결
CharacterWidget->BindCharacterStat(CharacterStat);
//플레이어 판단
if (bIsPlayer)
{
//컨트롤 모드 DIABLO, 속도 설정 후 입력 활성화
SetControlMode(EControlMode::DIABLO);
GetCharacterMovement()->MaxWalkSpeed = 600.0f;
EnableInput(ABPlayerController);
}
else
{
//컨트롤 모드 NPC, 속도 설정 후 AI 비헤어비어 실행
SetControlMode(EControlMode::NPC);
GetCharacterMovement()->MaxWalkSpeed = 400.0f;
ABAIController->RunAI();
}
break;
}
case ECharacterState::DEAD:
{
//충돌 체크 비활성화, 메쉬는 활성화, HP bar 비활성화
SetActorEnableCollision(false);
GetMesh()->SetHiddenInGame(false);
HPBarWidget->SetHiddenInGame(true);
//죽는 애니메이션 실행, 데미지 충돌 체크 비활성화
ABAnim->SetDeadAnim();
bCanBeDamaged = false;
if (bIsPlayer)
{
//플레이어 입력 비활성화
DisableInput(ABPlayerController);
}
else
{
//AI 비헤어비어 정지
ABAIController->StopAI();
}
//죽음 타이머 동작,
GetWorld()->GetTimerManager().SetTimer(DeadTimerHandle, FTimerDelegate::CreateLambda([this]() -> void {
if (bIsPlayer)
{
//5초 후 레벨 리스타트
ABPlayerController->RestartLevel();
}
else
{
//npc 엑터 파괴
Destroy();
}
}), DeadTimer, false);
break;
}
}
}
'언리얼 C++ 게임 개발의 정석 > 13. 게임 플레이 제작' 카테고리의 다른 글
13.3 게임 데이터의 관리 (0) | 2024.05.23 |
---|---|
13.2 플레이어 데이터와 UI 연동 (0) | 2024.05.21 |