캐릭터 스테이드 설정

  • 플레이어 캐릭터와 AI 캐릭터의 기능을 체계적으로 관리하기 위해 캐릭터에도 스테이트 머신 모델을 구현한다
  • PRINT 스테이트 : 캐릭터 생성 전의 스테이트, 기본 캐릭터 애셋이 설정돼 있지만, 캐리겉와 UI를 숨겨둔다. 해당 스테이트는 대미지를 입지 않음
  • LOADING 스테이트 : 선택한 캐릭터 애셋을 로딩하는 스테이트, 이때는 게임이 시작된 시점이므로 현재 조종하는 컨트롤러가 AI인지 플레이어인지 구분할 수 있다. 플레이어 컨트롤러인 경우 애셋 로딩이 완료될 떄까지 캐릭터를 조종하지 못하도록 입력을 비활성화 한다
  • READY 스테이트 : 캐릭터 애셋 로딩이 완료된 스테이트. 숨겨둔 캐릭터와 UI를 보여주며, 이떄부터는 공격을 받으면 대미지를 입는다. 플레이어 컨트롤러는 비로소 캐릭터를 조종할 수 있으며, AI 컨트롤러는 행도트리 로직을 구동해 캐릭터를 동작 시킨다
  • DEAD 스테이트 : 캐릭터가 HP를 소진해 사망할 떄 발생하는 스테이트, 죽는 애니메이션을 재생하고 UI 끄는 한편, 충돌 기능을 없애고, 대미지를 입지 않도록 설정한다. 컨트롤러가 플레이어인 경우 입력을 비활성화하고 AI인 경우 행동트리 로직을 중지한다. 일정시간이 지난 후에는 플레이어의 경우 레벨을 재시작하고 AI는 레벨에서 퇴장한다.
  • 학습을 위해 블루프린트와 호환되는 열거형 선언
/////////ArenaBattle.h
UENUM(BlueprintType)
{
    PREINIT,
    LOADING,
    READY,
    DEAD
};

////////ABCharacter.h

class ARENABATTLE_API AABCharacter : public ACharacter
{
    GENERATED_BODY()
public:
    AABCharacter();
    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;
}
///AABCharacter.cpp

AABCharacter::AABCharacter()
{
    AssetIndex =4;
    SetActorHiddenInGame(true);
    HPBarWidget->SetHiddenInGame(true);
    bCanBeDamaged = false;
}

void AABCharacter::BeginPlay()
{
    Super::BeginPlay();


    // Changed in 4.21. copied from PostInitializeComponents()
    auto CharacterWidget = Cast<UABCharacterWidget>(HPBarWidget->GetUserWidgetObject());
    if (nullptr != CharacterWidget)
    {
        CharacterWidget->BindCharacterStat(CharacterStat);
    }

    bIsPlayer = IsPlayerControlled();

    if (bIsPlayer)
    {
        ABPlayerController = Cast<AABPlayerController>(GetController());
        ABCHECK(nullptr != ABPlayerController);
    }
    else
    {
        ABAIController = Cast<AABAIController>(GetController());
        ABCHECK(nullptr != ABAIController);
    }


    auto DefaultSetting = GetDefault<UABCharacterSetting>();

    if (bIsPlayer)
    {
        AssetIndex = 4;
    }
    else
    {
        AssetIndex = FMath::RandRange(0, DefaultSetting->CharacterAssets.Num()-1);
    }

    CharacterAssetToLoad = DefaultSetting->CharacterAssets[AssetIndex];
    auto ABGameInstance = Cast<UABGameInstance>(GetGameInstance());
    ABCHECK(nullptr != ABGameInstance)

    AssetStreamingHandle = ABGameInstance->StreamableManager.RequestAsyncLoad(CharacterAssetToLoad, FStreamableDelegate::CreateUObject(this, &AABCharacter::OnAssetLoadCompleted));
    SetCharacterState(ECharacterState::LOADING);
}


void AABCharacter::SetCharacterState(ECharacterState NewState)
{
    ABCHECK(CurrentState != NewState);
    CurrentState = NewState;

    switch (NewState)
    {
    case ECharacterState::LOADING :
        SetActorHiddenInGame(true);
        HPBarWidget->SetHiddenInGame(true);
        bCanBeDamaged = false;
        break;

    case ECharacterState::READY:
        SetActorHiddenInGame(false);
        HPBarWidget->SetHiddenInGame(false);
        bCanBeDamaged = true;

        CharacterStat->OnHPIsZero.AddLambda([this]()->void {SetCharacterState(ECharacterState::DEAD);});


        auto CharacterWidget = Cast<UABCharacterWidget>(HPBarWidget->GetUserWidgetObject());
        ABCHECK(nullptr != CharacterWidget);
        CharacterWidget->BindCharacterStat(CharacterStat);
        break;

    case ECharacterState::DEAD:

        SetActorEnableCollision(false);
        GetMesh()->SetHiddenInGame(false);
        HPBarWidget->SetHiddenInGame(true);
        ABAnim->SetDeadAnim();
        bCanBeDamaged = false;

        break;
    }
}

ECharacterState AABCharacter::GetCharacterState() const
{
    return CurrentState;
}
void AABCharacter::OnAssetLoadCompleted()
{
    AssetStreamingHandle->ReleaseHandle();
    TSoftObjectPtr<USkeletalMesh> LoadedAssetPath(CharacterAssetToLoad);
    ABCHECK(LoadedAssetPath.IsValid());

    GetMesh()->SetSkeletalMesh(LoadedAssetPath.Get());
    SetCharacterState(ECharacterState::READY);
}
  • 스테이트 구성이 끝나면, 스테이트에 맞게 행동트리 로직을 수동으로 구동하고 중지할 수 있게, AI 컨트롤러의 구조를 변경한다.
//h
UCLASS()
class ARENABATTLE_API AABAIController : public AAIController
{
public:
    void RunAI();
    void StopAI();
}

//cpp
void AABAIController::OnPossess(APawn* InPawn)
{
    Super::OnPossess(InPawn);

}

void AABAIController::RunAI()
{
    if (UseBlackboard(BBAsset, Blackboard))
    {
        Blackboard->SetValueAsVector(HomePosKey, GetPawn()->GetActorLocation());
        if (!RunBehaviorTree(BTAsset))
        {
            ABLOG(Error, TEXT("AIController couldn't run behavior tree!"));
        }
    }
}

void AABAIController::StopAI()
{
    auto BehaviorTreeComponent = Cast<UBehaviorTreeComponent>(BrainComponent);
    if (nullptr != BehaviorTreeComponent)
    {
        BehaviorTreeComponent->StopTree(EBTStopMode::Safe);
    }
}

플레이어가 빙의할 떄 발생하는 캐릭터의 PossessedBy 함수는 제거하고 대신 캐릭터의 READY 스테이트에서 이를 구현한다.

플레이어인 경우 입력을 활성화하고 AI의 경우 행동트리 시스템을 구동하는 로직을 추가한다. 그리고 DEAD 스테이트에는 플레이어의 경우 입력 처리를 중단하고, AI 경우에느 행동트리 시스템을 종료한다.

그리고 타이머를 발동시켜 사망한 이후에 처리할 로직도 구현한다.

UCLASS()
class ARENABATTLE_API AABCharacter : public ACharacter
{

private:


	UPROPERTY(EditAnyWhere, BlueprintReadWrite, Category = State, Meta = (AllowPrivateAccess = true))
		float DeadTimer;

	FTimerHandle DeadTimerHandle = {};

};
void AABCharacter::SetCharacterState(ECharacterState NewState)
{
	ABCHECK(CurrentState != NewState);
	CurrentState = NewState;

	switch (NewState)
	{
	case ECharacterState::LOADING :
	{
		if (bIsPlayer)
		{
			DisableInput(ABPlayerController);
		}

		SetActorHiddenInGame(true);
		HPBarWidget->SetHiddenInGame(true);
		bCanBeDamaged = false;
		break;
	}
	case ECharacterState::READY:
	{
		SetActorHiddenInGame(false);
		HPBarWidget->SetHiddenInGame(false);
		bCanBeDamaged = true;

		CharacterStat->OnHPIsZero.AddLambda([this]()->void {SetCharacterState(ECharacterState::DEAD); });


		auto CharacterWidget = Cast<UABCharacterWidget>(HPBarWidget->GetUserWidgetObject());
		ABCHECK(nullptr != CharacterWidget);
		CharacterWidget->BindCharacterStat(CharacterStat);

		if (bIsPlayer)
		{
			SetControlMode(EControlMode::DIABLO);
			GetCharacterMovement()->MaxWalkSpeed = 600.0f;
			EnableInput(ABPlayerController);
		}
		else
		{
			SetControlMode(EControlMode::NPC);
			GetCharacterMovement()->MaxWalkSpeed = 400.0f;
			ABAIController->RunAI();
		}


		break;
	}
	case ECharacterState::DEAD:
	{
		SetActorEnableCollision(false);
		GetMesh()->SetHiddenInGame(false);
		HPBarWidget->SetHiddenInGame(true);
		ABAnim->SetDeadAnim();
		bCanBeDamaged = false;

		if (bIsPlayer)
		{
			DisableInput(ABPlayerController);
		}
		else
		{
			ABAIController->StopAI();
		}

		GetWorld()->GetTimerManager().SetTimer(DeadTimerHandle, FTimerDelegate::CreateLambda([this]() -> void {
			if (bIsPlayer)
			{
				ABPlayerController->RestartLevel();
			}
			else
			{
				Destroy();
			}
		}),DeadTimer,false);

		break;
	}
	}
}

 

 

네비게이션 메시 시스템 설정

  • 섹션에서 NPC와 아이템 상자를 생성하는 기능을 추가
  • NPC와 아이템 상자가 생성될 시간을 지정할 속성을 추가
  • 타이머 기능(FTimerHandle 사용)을 사용해 일정 시간 이후에 생성한다
//h
class KGAME_API AABSection : public AActor
{
private:

    void OnNPCSpawn();

    UPROPERTY(EditAnywhere, Category = Spawn, Meta = (AllowPrivateAccess = true))
    float EnemySpawnTime;

    UPROPERTY(EditAnywhere, Category = Spawn, Meta = (AllowPrivateAccess = true))
    float ItemBoxSpawnTime;

    FTimerHandle SpawnNPCTimerHandle = { };
    FTimerHandle SpawnItemBoxTimerHandle = { };
  }

//cpp
AABSection::AABSection()
{
...
    EnemySpawnTime = 2.0f;
    ItemBoxSpawnTime = 5.0f;
}


void AABSection::SetState(ESectionState NewState)
{

.....
    case ESectionState::BATTLE:
    {
        Trigger->SetCollisionProfileName(TEXT("NoCollision"));
        for (UBoxComponent* GateTrigger : GateTriggers)
        {
            GateTrigger->SetCollisionProfileName(TEXT("NoCollision"));
        }

        OperateGates(false);


        GetWorld()->GetTimerManager().SetTimer(
            SpawnNPCTimerHandle,
            FTimerDelegate::CreateUObject(this, &AABSection::OnNPCSpawn),
            EnemySpawnTime,false);

        GetWorld()->GetTimerManager().SetTimer(SpawnItemBoxTimerHandle,
            FTimerDelegate::CreateLambda([this]()-> void 
        {
            FVector2D RandXY = FMath::RandPointInCircle(600.0f);
            GetWorld()->SpawnActor<AABItemBox>(GetActorLocation() + FVector(RandXY, 30.0f), FRotator::ZeroRotator);
        }), ItemBoxSpawnTime, false);

        break;
    }
.....
}

void AABSection::OnNPCSpawn()
{
    GetWorld()->SpawnActor<AABCharacter>(GetActorLocation() + FVector::UpVector * 88.0f, FRotator::ZeroRotator);
}

실행하면 플레이어 진입하고 2초 후에 가운데 위치에서 NPC 생성되고, 5초후에 NPC 반경 6미터 내의 랜덤한 위치에 아이템 상자가 생성된다
그러나 생성된 NPC 가만히 있는다. 그래서 새로 생성된 섹션 영역에도 내비게이션 메시가 만들어져야 NPC가 이를 활용해 플레이어로 이동할 수 있다.
이를 위해 게임 실행 중에 동적으로 내비 게이션 메시를 생성하도록 프로젝트에서 추가설정을 해줘야 한다. 프로젝트 세팅의 내비게이션 메시설정에서 RunTime Generation 속성의 값을 Dynamic으로 변경하면, 새롭게 생성된 섹션에도 내비게이션 메시가 실시간으로 만들어져 적용된다.

이 설정면 변경하면 새롭게 생성한 섹션 액터에도 내비게이션 시스템이 만들어져서 NPC는 플레이어를 쫓아온다

레벨을 섹션이라는 단위로 나누고, 하나의 섹션을 클리어 하면 새로운 섹션이 등장하는 무한 맵 스테이지 제작하기


섹션 액터가 해야 할일

  • 섹션의 배경과 네방향으로 캐릭터 입장을 통제하는 문 제공
  • 플레이어가 섹션에 진입하면 모든 문을 닫는다
  • 문을 닫고 일정 시간 후에 섹션 중앙에서 NPC를 생성한다
  • 문을 닫고 일정시간 후에 아이템 상자를 섹션 내 랜덤한 위치에 생성한다
  • 생성한 NPC가 죽으면 모든 문을 개방한다
  • 통과한 문으로 이어지는 새로운 섹션을 생성한다

섹션 액터 제작을 위해 Actor를 부모클래스로 하는 ABSection이라는 클래스를 KGame모듈에 생성한다.

//h
#include "KGame.h"
#include "GameFramework/Actor.h"
#include "ABSection.generated.h"

UCLASS()
class KGAME_API AABSection : public AActor
{
    GENERATED_BODY()
public: 
    AABSection();

protected:
    virtual void BeginPlay() override;

public: 
    virtual void Tick(float DeltaTime) override;

private:
    UPROPERTY(VisibleAnywhere,Category=Mesh,Meta=(AllowPrtivateAccess=true))
        UStaticMeshComponent* Mesh;
};

//cpp
#include "ABSection.h"

// Sets default values
AABSection::AABSection()
{
    // Set this actor to call Tick() every frame.  You can turn this off to improve performance if you don't need it.
    PrimaryActorTick.bCanEverTick = false;


    Mesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("MESH"));
    RootComponent = Mesh;

    FString AssetPath = TEXT("/Game/Book/StaticMesh/SM_SQUARE.SM_SQUARE");
    static ConstructorHelpers::FObjectFinder<UStaticMesh> SM_SQUARE(*AssetPath);
    if (SM_SQUARE.Succeeded())
    {
        Mesh->SetStaticMesh(SM_SQUARE.Object);
    }
    else
    {
        ABLOG(Error, TEXT("Failed to load staticMesh : %s"), *AssetPath);
    }
}

// Called when the game starts or when spawned
void AABSection::BeginPlay()
{
    Super::BeginPlay();

}

// Called every frame
void AABSection::Tick(float DeltaTime)
{
    Super::Tick(DeltaTime);

}

해당 스태틱 메시에 각 방향별로 출입문과 섹션을 이어붙일 수 있게 여덟 개의 소켓이 부착돼 있다.
배경의 각 출입구에 철문을 부착한다. 철문마다 스태틱 메시 컴포넌트를 제작하고 이를 소켓에 부착한다. 제공하는 철문애셋은 부착되는 최종위치로 부터 Y축으로 -80.5 이동ㅇ한 지점
소켓목록을 제작하고 이를 사용해 철문을 각각 부착, 동일한 기능이므로 TArray로 묶어 관리

 

//h
UCLASS()
class KGAME_API AABSection : public AActor
{
    UPROPERTY(VisibleAnywhere,Category=Mesh,Meta=(AllowPrtivateAccess=true))
        UStaticMeshComponent* Mesh;
};
//cpp
AABSection::AABSection()
{
    FString GateAssetPath = TEXT("/Game/Book/StaticMesh/SM_GATE.SM_GATE");
    static ConstructorHelpers::FObjectFinder<UStaticMesh> SM_GATE(*GateAssetPath);
    if (!SM_GATE.Succeeded())
    {
        ABLOG(Error, TEXT("Failed to load staticMesh : %s"), *GateAssetPath);
    }

    static FName GateSockets[] = {
        {TEXT("+XGate")},
        {TEXT("-XGate")},
        {TEXT("+YGate")},
        {TEXT("-YGate")}
    };

    for (FName GateSocket : GateSockets)
    {
        ABCHECK(Mesh->DoesSocketExist(GateSocket));
        UStaticMeshComponent* NewGate = CreateDefaultSubobject<UStaticMeshComponent>(*GateSocket.ToString());
        NewGate->SetStaticMesh(SM_GATE.Object);
        NewGate->SetupAttachment(RootComponent, GateSocket);
        NewGate->SetRelativeLocation(FVector(0.0f, -80.5f, 0.0f));
        GeteMeshes.Add(NewGate);
    }

이번에는 ABCharacter만을 감지하는 ABTrigger라는 이름의 특별한 콜리전 프리셋을 추가한다.
이 콜리전 프리셋은 플레이어의 입장을 감지하고 섹션을 클리어 한 후 출구를 선택할 떄 사용한다.

해당 프리셋을 사용하는 Box 컴포넌트를 생성하고 섹션의 중앙과 각 철문 영역에 부착한다.

//h
class KGAME_API AABSection : public AActor
{
    UPROPERTY(VisibleAnywhere, Category = Trigger, Meta = (AllowPrtivateAccess = true))
        TArray<UBoxComponent*> GateTriggers;

    UPROPERTY(VisibleAnywhere, Category = Trigger, Meta = (AllowPrtivateAccess = true))
        UBoxComponent* Trigger;
};
}

//cpp
AABSection::AABSection()
{
    PrimaryActorTick.bCanEverTick = false;


    Mesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("MESH"));
    RootComponent = Mesh;

    FString AssetPath = TEXT("/Game/Book/StaticMesh/SM_SQUARE.SM_SQUARE");
    static ConstructorHelpers::FObjectFinder<UStaticMesh> SM_SQUARE(*AssetPath);
    if (SM_SQUARE.Succeeded())
    {
        Mesh->SetStaticMesh(SM_SQUARE.Object);
    }
    else
    {
        ABLOG(Error, TEXT("Failed to load staticMesh : %s"), *AssetPath);
    }

    Trigger = CreateDefaultSubobject<UBoxComponent>(TEXT("TRIGGER"));
    Trigger->SetBoxExtent(FVector(775.0f, 775.0f, 300.0f));
    Trigger->SetupAttachment(RootComponent);
    Trigger->SetRelativeLocation(FVector(0.0f, 0.0f, 250.0f));
    Trigger->SetCollisionProfileName(TEXT("ABTrigger"));

    FString GateAssetPath = TEXT("/Game/Book/StaticMesh/SM_GATE.SM_GATE");
    static ConstructorHelpers::FObjectFinder<UStaticMesh> SM_GATE(*GateAssetPath);
    if (!SM_GATE.Succeeded())
    {
        ABLOG(Error, TEXT("Failed to load staticMesh : %s"), *GateAssetPath);
    }


    static FName GateSockets[] = {
        {TEXT("+XGate")},
        {TEXT("-XGate")},
        {TEXT("+YGate")},
        {TEXT("-YGate")}
    };

    for (FName GateSocket : GateSockets)
    {
.....

        UBoxComponent* NewGateTrigger = CreateDefaultSubobject<UBoxComponent>(*GateSocket.ToString().Append(TEXT("Trigger")));

        NewGateTrigger->SetBoxExtent(FVector(100.0f,100.0f,300.0f));
        NewGateTrigger->SetupAttachment(RootComponent,GateSocket);
        NewGateTrigger->SetRelativeLocation(FVector(70.0f,0.0f,250.0f));
        NewGateTrigger->SetCollisionProfileName(TEXT("ABTrigger"));
        GateTriggers.Add(NewGateTrigger);
    }

}

액터 설정을 완료하고 액터의 로직을 스테이트 머신으로 설계한다
다음과 같은 스테이트들을 가진다.
열거형을 사용하여 구현

  • 준비스테이트 : 액터의 시작 스테이트, 문을 열어놓고 대기하다가 중앙의 박스트리거로 플레이어의 진입을 감지하면 전투스테이트로 이동한다.
  • 전투스테이트 : 문을 닫고 일정 시간이 지마녀 NPC를 소환한다. 그리고 일정시간이 지나면 랜덤한 위치에 아이템 상자도 생성한다. NPC가 죽으면 완료 스테이트로 이동한다.
  • 완료스테이트 : 닫힌 문을 연다. 각 문마다 배치한 트리거 게이트로 플레이를 감지하면 이동한 문의 방향으로 새로운 섹션을 소환한다
  • bNobattle : 에디터에 전투가 없으므로 true로 설정한다
//h
class KGAME_API AABSection : public AActor
{
private:
    enum class ESectionState : uint8
    {
        READY = 0,
        BATTLE,
        COMPLETE
    };

    void SetState(ESectionState NewState);
    ESectionState CurrentState = ESectionState::READY;

    void OperateGates(bool bOpen = true);

private:
    UPROPERTY(EditAnywhere, Category = State, Meta = (AllowPrtivateAccess = true))
        bool bNoBattle;
};

//cpp
AABSection::AABSection()
{
..........
    bNoBattle = false;
}


void AABSection::BeginPlay()
{
    Super::BeginPlay();

    SetState(bNoBattle ? ESectionState::COMPLETE : ESectionState::READY);
}



void AABSection::SetState(ESectionState NewState)
{
    switch (NewState)
    {
    case ESectionState::READY:
        Trigger->SetCollisionProfileName(TEXT("ABTrigger"));
        for (UBoxComponent* GateTrigger : GateTriggers)
        {
            GateTrigger->SetCollisionProfileName(TEXT("NoCollision"));
        }
        OperateGates(true);

        break;

    case ESectionState::BATTLE :
        Trigger->SetCollisionProfileName(TEXT("NoCollision"));
        for (UBoxComponent* GateTrigger : GateTriggers)
        {
            GateTrigger->SetCollisionProfileName(TEXT("NoCollision"));
        }
        OperateGates(false);

        break;

    case ESectionState::COMPLETE:
        Trigger->SetCollisionProfileName(TEXT("NoCollision"));
        for (UBoxComponent* GateTrigger : GateTriggers)
        {
            GateTrigger->SetCollisionProfileName(TEXT("ABTrigger"));
        }
        OperateGates(true);

        break;
    default:
        break;
    }


}

void AABSection::OperateGates(bool bOpen)
{
    for (UStaticMeshComponent* Gate : GeteMeshes)
    {
        Gate->SetRelativeRotation(bOpen ? FRotator(0.0f,-90.0f,0.0f) : FRotator::ZeroRotator);
    }
}

터에는 어디터와 연동되는 OnConstruction이라는 특별한 함수가 있다.
에디터 작업에서 선택한 액터의 속성이나 트랜스폼 정보가 변경 될떄 이 OnConstruction 함수가 실행된다.

 

//h
class KGAME_API AABSection : public AActor
{
public: 
    AABSection();
    virtual void OnConstruction(const FTransform& Transform) override;

//cpp
void AABSection::OnConstruction(const FTransform& Transform)
{
    Super::OnConstruction(Transform);
    SetState(bNoBattle ? ESectionState::COMPLETE : ESectionState::READY);
}

액터의 완료 스테이트에서는 각 철문에 있는 트리거가 활성화 되고, 플레이어가 감지되면, 해당 철문의 방향으로 새로운 섹션액터를 생성하는 로직이 들어가야 한다.
그런데 게임의 진행 상황에 따라 해당 위치에 이미 섹션 액터가 생성돼 있을 수 있으므로 이를 미리 확인하는 로직이 필요하다. 물리엔진 기능을 사용해 해당 위치에 액터가 있다면 생성을 건너뛴다.
새롭게 생성된 섹션 액터를 READY 스테이트에서 부터 시작하는데, 가운데 트리거 여영ㄱ을 활성화해서 플레이어가 들어오는지 감지해야 한다. 플레이어를 감지하면 바로 BATTLE 스텡트로 전환하고 플레이어가 빠져나가지 못하게 문을 닫는다.
박스컴포넌트 OnComponentBeginOveralp 델리게이트에 바인딩시클 함수를 생성하고 이를 연결한다. 해당 델리게이트 다이나믹 델리게이트이므로 함수 선언에 UFUNCTION을 지정한다.
총 4개의 문에 각각 4개의 박스 컴포넌트가 있지만, 이들 기능은 동일하다. 그래서 모든 문에 설치된 박스컴포넌트의 델리게이트에 하나의 멤버 함수를 연결한다. 이때 감지된 박스 컴포넌트가 어떤 문에 있는 컴포넌트인지 구분 할 수 있도록 컴포넌트에 소켓이름으로 태그(Tag)를 설정하고, 이를 사용해 해당 방향에 띄울 다음 섹션 액터까지 생성하는 기능을 구현한다.

/h
UCLASS()
class KGAME_API AABSection : public AActor
{
    GENERATED_BODY()
public:
    AABSection();
    virtual void OnConstruction(const FTransform& Transform) override;
protected:
private:
    UFUNCTION()
        void OnTriggerBeginOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult &SweepResult);

    UFUNCTION()
        void OnGateTriggerBeginOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult &SweepResult);

//cpp
AABSection::AABSection()
{

    Trigger->SetCollisionProfileName(TEXT("ABTrigger"));

    Trigger->OnComponentBeginOverlap.AddDynamic(this, &AABSection::OnTriggerBeginOverlap);



    for (FName GateSocket : GateSockets)
    {
        ABCHECK(Mesh->DoesSocketExist(GateSocket));
        UStaticMeshComponent* NewGate = CreateDefaultSubobject<UStaticMeshComponent>(*GateSocket.ToString());
        NewGate->SetStaticMesh(SM_GATE.Object);
        NewGate->SetupAttachment(RootComponent, GateSocket);
        NewGate->SetRelativeLocation(FVector(0.0f, -80.5f, 0.0f));
        GateMeshes.Add(NewGate);

        UBoxComponent* NewGateTrigger = CreateDefaultSubobject<UBoxComponent>(*GateSocket.ToString().Append(TEXT("Trigger")));
        NewGateTrigger->SetBoxExtent(FVector(100.0f, 100.0f, 300.0f));
        NewGateTrigger->SetupAttachment(RootComponent, GateSocket);
        NewGateTrigger->SetRelativeLocation(FVector(70.0f, 0.0f, 250.0f));
        NewGateTrigger->SetCollisionProfileName(TEXT("ABTrigger"));
        GateTriggers.Add(NewGateTrigger);

        NewGateTrigger->OnComponentBeginOverlap.AddDynamic(this, &AABSection::OnGateTriggerBeginOverlap);
        NewGateTrigger->ComponentTags.Add(GateSocket);
    }

    bNoBattle = false;
}



void AABSection::OnTriggerBeginOverlap(UPrimitiveComponent * OverlappedComponent, AActor * OtherActor, UPrimitiveComponent * OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult & SweepResult)
{
    if (CurrentState == ESectionState::READY)
    {
        SetState(ESectionState::BATTLE);
    }
}

void AABSection::OnGateTriggerBeginOverlap(UPrimitiveComponent * OverlappedComponent, AActor * OtherActor, UPrimitiveComponent * OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult & SweepResult)
{
    ABCHECK(OverlappedComponent->ComponentTags.Num() == 1);
    FName ComponentTag = OverlappedComponent->ComponentTags[0];
    FName SocketName = FName(*ComponentTag.ToString().Left(2));
    if (!Mesh->DoesSocketExist(SocketName))
        return;

    FVector NewLocation = Mesh->GetSocketLocation(SocketName);

    TArray<FOverlapResult> OverlapResults;
    FCollisionQueryParams CollisionQueryParam(NAME_None, false, this);
    FCollisionObjectQueryParams ObjectQueryParam(FCollisionObjectQueryParams::InitType::AllObjects);
    bool bResult = GetWorld()->OverlapMultiByObjectType(
        OverlapResults,
        NewLocation,
        FQuat::Identity,
        ObjectQueryParam,
        FCollisionShape::MakeSphere(775.0f),
        CollisionQueryParam
    );

    if (!bResult)
    {
        auto NewSection = GetWorld()->SpawnActor<AABSection>(NewLocation, FRotator::ZeroRotator);
    }
    else
    {
        ABLOG(Warning, TEXT("New section area is not empty."));
    }
}

INI 설정과 애셋의 지연로딩


  • ABCharacterSetting 은 앞으로 사용할 캐릭터 애셋의 목록을 보관한다.
  • INI 파일을 사용해 ABCharacterSetting의 기본값을 설정하는 방법을 진행
  • 애셋은 경로 정보만 알면 프로그램에서 참조해 로딩 할수 있다. 이 애셋 경로 정보를 보관하기 위해 언리얼 엔진은 FSoftObjectPath라는 클래스를 제공한다.
  • 언리얼 오브젝트가 기본값을 INI 파일에서 불러들이려면 UCLASS 매크로에 config 키워드를 추가해 여기에 불러들일 INIㅍ 알의 이름을 지정하고, 불러들일 PROPERTY 속성에는 config 키워드를 선언해야 한다.
  • 이렇게 서언하면 언리얼 엔진은 언리얼 오브젝트를 초기화할떄 해당 속성의 값을 INI 파일에서 읽어 설정한다.
//h
UCLASS(config=KGame)
class KGAMESETTING_API UABCharacterSetting : public UObject
{
    GENERATED_BODY()

public:
    UABCharacterSetting();
    UPROPERTY(config)
        TArray<FSoftObjectPath> CharacterAssets;
};

/cpp
#include "ABCharacterSetting.h"

UABCharacterSetting::UABCharacterSetting()
{

}

UCLASS 매크로 내 config 키워드네 있는 KGame이라는 설정으로 인해, 언리얼 엔진은 초기화 단계에서, Config 폴더에 위치한 DefaultKGame.ini 파일을 읽어 들여 , KGameSetting dnk CharacterAssets 값을 설정한다.
책에 리소스 DefaultKGame.ini 복사에서 Config 폴더로


 

  • 언리얼 엔진이 초기화 되면 엔진 구동에 필요한 모듈이 순차적으로 로딩된다. 모듈이 로딩되면서 모듈은 자신에게 속한 모든 언리얼 오브젝트의 기본값을 지정해 생성해낸다. 이를 클래스 기본 객체라고 한다.
  • 엔진이 초기화 되면 모든 언리얼 오브젝트 클래스 기본 객체가 메모리에 올라간 상태가 된다. 이렇게 올라간 클래스 기본 객체는 GetDefault 함수를 사용해 가져 올 수 있다. 엔진이 종료될떄까지 상주하기 떄문에 언제든지 사용해도 된다.
  • KGame 모듈에서 ABCharacter, KGameSettiing 모듈의 ABCharacterSetting에서 캐릭터 애셋 목록을 얻어오도록 코드를 생성한다. 그러러면 모듈의 빌드 규칙을 지정하는 KGame.Build.cs 파일에서 KGameSetting 모듈을 사용하도록 참조할 모듈 목록에 추가해야 한다.
  • 구현 부가 모여있는 Private 폴더 에서만 KGameSetting 모듈을 사용할 예정이므로 PricateDependencyModule 항목에 이를 추가한다.
public class KGame : ModuleRules
{
    public KGame(ReadOnlyTargetRules Target) : base(Target)
    {
        PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;

        PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore", "UMG", "NavigationSystem", "AIModule", "GameplayTasks" });

        PrivateDependencyModuleNames.AddRange(new string[] { "KGameSetting" });

        // Uncomment if you are using Slate UI
        // PrivateDependencyModuleNames.AddRange(new string[] { "Slate", "SlateCore" });

        // Uncomment if you are using online features
        // PrivateDependencyModuleNames.Add("OnlineSubsystem");

        // To include OnlineSubsystemSteam, add it to the plugins section in your uproject file with the Enabled attribute set to true
    }
}

성공 메시지 출력

  • 경로가 확인되고 생성자 로직에서 해당 코드는 제거하고, NPC가 생성될떄 랜덤하게 하나 캐릭터 애셋을 로딩하도록 기능을 변경한다.
  • 언리얼은 게임 진행 중 비동기 방식으로 애셋을 로딩하도록 FStreamableManager라는 클래스를 제공한다.
  • 이 매니저 클래스는 프로젝트에서 하나만 활성화 하는 것이 좋기에, 유일한 인스턴스로 동작하는 ABGameInstance 클래스에서 이를 멤버 변수로 선언한다.
class KGAME_API UABGameInstance : public UGameInstance
{
public:
    FStreamableManager StreamableManager;
};

//.h
class KGAME_API AABCharacter : public ACharacter
{
private:
    FSoftObjectPath CharacterAssetToLoad = FSoftObjectPath(nullptr);
    TSharedPtr<struct FStreamableHandle> AssetStreamingHandle;
}

//cpp
#include "ABGameInstance.h"

void AABCharacter::BeginPlay()
{

    if (!IsPlayerControlled())
    {
        auto DefaultSetting = GetDefault<UABCharacterSetting>();
        int32 RandIndex = FMath::RandRange(0,DefaultSetting->CharacterAssets.Num()-1 );
        CharacterAssetToLoad = DefaultSetting->CharacterAssets[RandIndex];

        auto ABGameInstance = Cast<UABGameInstance>(GetGameInstance());
        if (nullptr != ABGameInstance) {
            AssetStreamingHandle = ABGameInstance->StreamableManager.RequestAsyncLoad( 
                CharacterAssetToLoad, FStreamableDelegate::CreateUObject(this, &AABCharacter::OnAssetLoadCompleted));     //완료가 되면 등록한 함수가 호출 된다.
        }
    }
}

void AABCharacter::OnAssetLoadCompleted()
{
    AssetStreamingHandle->ReleaseHandle();
    TSoftObjectPtr<USkeletalMesh> LoadedAssetPath(CharacterAssetToLoad);
    if (LoadedAssetPath.IsValid())
    {
        GetMesh()->SetSkeletalMesh(LoadedAssetPath.Get());
    }
}

FStreamableDelegate::CreateUObject () : 즉석 델리게이트 생성 
FStreamableHandle : A handle to a synchronous or async load. As long as the handle is Active, loaded assets will stay in memory
TSoftObjectPtr : TSoftObjectPtr is templatized wrapper of the generic FSoftObjectPtr , it can be used in UProperties

소스를 효과적으로 관리 할 수 있도록 프로젝트 구조를 변경하고 게임 설정에 관련된 데이터를 별도의 모듈로 분리한다.
언리얼 엔진의 설정 시스템을 이요ㅕㅇ해 게임의 기본데이터를 INI 파일로 관리 
레벨의 요소를 섹션이라는 단위로 개편하고 무한으로 증가흔 레벨을 설계


모듈 폴더와 빌더 설정파일 : 모듈명으로 괸 Bulid.cs 파일 모듈의 정의 파일 : 모듈명으로 된 .cpp파일

//KGameSetting 모듈 추가하기
//KGameSetting.Bulid.cs, KGameSetting.cpp,KGameSetting.h

KGameSetting.cpp
#include "KGameSetting.h"
#include "Modules/ModuleManager.h"

IMPLEMENT_MODULE( FDefaultModuleImpl, KGameSetting);



///////////////KGameSetting.Build.cs
using UnrealBuildTool;
public class KGameSetting : ModuleRules
{
    public KGameSetting(ReadOnlyTargetRules Target) : base(Target)
    {
        PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;

        PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine" });

        PrivateDependencyModuleNames.AddRange(new string[] {  });

        // Uncomment if you are using Slate UI
        // PrivateDependencyModuleNames.AddRange(new string[] { "Slate", "SlateCore" });

        // Uncomment if you are using online features
        // PrivateDependencyModuleNames.Add("OnlineSubsystem");

        // To include OnlineSubsystemSteam, add it to the plugins section in your uproject file with the Enabled attribute set to true
    }
}


//////////////////KGame.Target.cs
using UnrealBuildTool;
using System.Collections.Generic;

public class KGameTarget : TargetRules
{
    public KGameTarget(TargetInfo Target) : base(Target)
    {
        Type = TargetType.Game;

        ExtraModuleNames.AddRange( new string[] { "KGame",
            "KGameSetting" } );
    }
}


//////////////////KGameEditor.Target.cs
using UnrealBuildTool;
using System.Collections.Generic;

public class KGameEditorTarget : TargetRules
{
    public KGameEditorTarget(TargetInfo Target) : base(Target)
    {
        Type = TargetType.Editor;

        ExtraModuleNames.AddRange( new string[] { "KGame",
            "KGameSetting" } );
    }
}

위까지 빌드를 하면 Ninaries 폴더에 새로운 파일이 생성 된다.

새로운 DLL 파일이 생성됐다면 DLL 파일을 로딩하도록 명령을 해야 한다. 명령을 하기 위해선 uproject 파일에 기입해야 한다.
KGameSetting모듈을 먼저 로딩하도록 PreDefault로 ㅓㄹ정하고 KGame 모듈이 KGameSetting 모듈에 대해 의존성을 가지도록 설정한다.
그러면 KGameSetting 모듈은 KGame모듈보다 먼저 언리얼 프로세스에 올라가게 된다.

{
    "FileVersion": 3,
    "EngineAssociation": "4.22",
    "Category": "",
    "Description": "",
  "Modules": [
    {
      "Name": "KGameSetting",
      "Type": "Runtime",
      "LoadingPhase": "PreDefault",
      "AdditionalDependencies": [
        "CoreUObject"
      ]
    },
    {
      "Name": "KGame",
      "Type": "Runtime",
      "LoadingPhase": "Default",
      "AdditionalDependencies": [
        "Engine",
        "UMG",
        //"KGameSetting"
      ]
    }
  ]
}

이제 신규 클래스를 만들떄 에디터를 학인 할수 있다

+ Recent posts