블랙보드와 비헤이비어 트리 둘다 필요하므로 만들어준다.

 

블랙보드는 애님 인스턴스를 먼저 만들고 애니메이션 재생과 관련된 데이터를 몰아넣고 매 프레임마다 해당 데이터들을 이용해서 애니메이션을 재생하는데 사용하던 개념과 비슷하다.

 

저번까지는 빙의 시 타이머를 등록하여 랜덤한 이동을 테스트 해봤는데 비헤이비어 트리를 이용하면 이런 부분이 필요가 없어진다.

 

// MyAIController.h
private:
    UPROPERTY()
    class UBehaviorTree* BehaviorTree;

    UPROPERTY()
    class UBlackboardData* BlackboardData;
    
// MyAIController.cpp
#include "BehaviorTree/BehaviorTree.h"
#include "BehaviorTree/BlackboardData.h"
#include "BehaviorTree/BlackboardComponent.h"

AMyAIController::AMyAIController()
{
    static ConstructorHelpers::FObjectFinder<UBehaviorTree> BT(TEXT("BehaviorTree'/Game/AI/BT_MyCharacter.BT_MyCharacter'"));
    if (BT.Succeeded())
    {
        BehaviorTree = BT.Object;
    }

    static ConstructorHelpers::FObjectFinder<UBlackboardData> BD(TEXT("BlackboardData'/Game/AI/BB_MyCharacter.BB_MyCharacter'"));
    if (BD.Succeeded())
    {
        BlackboardData = BD.Object;
    }
}

void AMyAIController::OnPossess(APawn* InPawn)
{
    // 빙의 시
    Super::OnPossess(InPawn);

    // 블랙보드 사용
    if (UseBlackboard(BlackboardData, Blackboard))
    {
        // 비헤이비어 트리를 실행
        if (RunBehaviorTree(BehaviorTree))
        {
            // Log
        }
    }
}

 

아직까지는 비헤이비어 트리를 작성한게 딱히 없기때문에 아무런 행동도 하지 않지만 실제로는 적용되고 있다.

 

 

Task는 저것들이 전부가 아니라 기본적으로 제공되는 것들이고 당연히 커스텀으로 만들어서 사용할 수도 있다.

최상단의 Find Patrol Pos는 커스텀으로 만든 것이다.

 

BTTaskNode를 상속받은 클래스를 만들어서 필요한 기능을 안에 구현하면 된다.

 

// BTTask_FindPatrolPos.h
UCLASS()
class TESTUNREALENGINE_API UBTTask_FindPatrolPos : public UBTTaskNode
{
    GENERATED_BODY()

public:
    UBTTask_FindPatrolPos();

    virtual EBTNodeResult::Type ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) override;
};

// BTTask_FindPatrolPos.cpp
#include "NavigationSystem.h"
#include "Blueprint/AIBlueprintHelperLibrary.h"
#include "BehaviorTree/BehaviorTree.h"
#include "BehaviorTree/BlackboardData.h"
#include "BehaviorTree/BlackboardComponent.h"
#include "AIController.h"

UBTTask_FindPatrolPos::UBTTask_FindPatrolPos()
{
    // 해당 이름으로 노드가 표시된다
    NodeName = TEXT("FindPatrolPos");
}

EBTNodeResult::Type UBTTask_FindPatrolPos::ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory)
{
    EBTNodeResult::Type Result = Super::ExecuteTask(OwnerComp, NodeMemory);

    auto CurrentPawn = OwnerComp.GetAIOwner()->GetPawn();

    if (CurrentPawn == nullptr)
        return EBTNodeResult::Failed;

    // 월드의 내비게이션 시스템을 가져온다
    UNavigationSystemV1* NavSystem = UNavigationSystemV1::GetNavigationSystem(GetWorld());

    if (NavSystem == nullptr)
        return EBTNodeResult::Failed;

    FNavLocation RandomLocation;

    // 월드의 내비게이션 범위중 원점(첫번째 매개변수)을 기준으로 반지름 만큼의 범위를 한정지어
    // 랜덤한 좌표를 가져온다(RandomLocation에 저장됨)
    if (NavSystem->GetRandomPointInNavigableRadius(FVector::ZeroVector, 500.f, OUT RandomLocation))
    {
        // 해당 액터를 RandomLocation으로 이동
        OwnerComp.GetBlackboardComponent()->SetValueAsVector(FName(TEXT("PatrolPos")), RandomLocation.Location);
        return EBTNodeResult::Succeeded;
    }

    return EBTNodeResult::Failed;
}

 

ExecuteTask는 딱 보기에도 태스크의 실행이다.

 

3초간 랜덤한 위치로 이동하는 구현부를 이쪽으로 옮겨오면서 코드 수정이 이루어졌다.

원래 직접 이동시키는 함수가 있었으나 시퀀스를 이용할 것이기 때문에 랜덤한 좌표를 계산하는것만 가져오도록 한다.

 

비헤이비어 트리에서 시퀀스에 등록된 노드들은 각 노드별로 성공시 순차적으로 실행되지만 중간에 실패하면 이후의 노드들의 실행이 중단된다.

그렇기 때문에 매번 중요한 곳에서 체크할 때마다 Failed를 반환해 주어야 한다.

 

블랙보드에 등록된 키에 값을 넣어주려면 SetValue~~ 함수를 이용해서 블랙보드에 만들어 준 키 이름과 동일한 것을 입력하고 어떤 값을 넘겨줄지를 두번째 인자로 넘겨주면 된다.

 

블랙보드에 키를 만들어주고

 

트리를 위와 같이 구성하면 5초 대기 후 좌표를 구해서 해당 좌표로 이동하는 것을 반복하게 된다.

 

[정리]

 

  • 컴포짓 : 셀렉터, 시퀀스, 심플 페러렐 노드가 있다.
  • 셀렉터 : 자식 노드중 하나라도 성공하면 나머지 자식 노드의 실행을 중단한다. 실패하면 다음 자식 노드를 실행한다.
  • 시퀀스 : 자식 노드중 하나라도 실패하면 나머지 자식 노드의 실행을 중단한다. 성공하면 다음 자식 노드를 실행한다.
  • 심플 페러렐 : 전체 노드 트리와 동시에 하나의 태스크를 실행할 수 있다.
  • BTTaskNode : 비헤이비어 트리에 커스텀 노드를 추가할 수 있다.

'언리얼 엔진 > 언리얼 엔진4 입문' 카테고리의 다른 글

[UE4 입문] 샘플 분석  (0) 2022.09.12
[UE4 입문] Behavior Tree #2  (0) 2022.09.12
[UE4 입문] AI Controller  (0) 2022.09.11
[UE4 입문] UI 실습  (0) 2022.09.03
[UE4 입문] 스탯 매니저  (0) 2022.09.02

유니티의 경우 AI를 제어하려면 AIController같은 클래스를 만들어서 컴포넌트로 붙여서 제어하게 되는데 언리얼도 유사하게 되어있다.

언리얼은 이미 구조가 어느정도 잡혀있다는점이 조금 다르다.

AIController는 AI에 빙의 되었다가 풀렸다 하면서 제어가 된다.

 

AIController를 상속받는 클래스를 하나 만들어주도록 하자.

 

언리얼에서는 Behavior Tree라는것을 제공해주는데 항상 사용해야 되는것은 아니고 매우 간단한 AI같은 경우는 상태기계를 이용하는게 더 쉬울 수 있다.

Behavior Tree는 보통 서버쪽에서 만들어진다.

 

우선 모듈을 추가로 사용해야 하기 때문에 cs파일에 모듈을 추가해주자.

 

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

 

NavigationSytstem은 단어 그대로 내비게이션 시스템을 사용하기 위해 필요하고 AIModule은 AIController를 사용하기 위해서 필요하다. GameplayTasks는 이번시간은 아니지만 차후 Behavior Tree를 사용할 때 필요하므로 미리 넣어준다.

 

 

// MyAIController.h
UCLASS()
class TESTUNREALENGINE_API AMyAIController : public AAIController
{
    GENERATED_BODY()

public:
    AMyAIController();

    virtual void OnPossess(APawn* InPawn) override;
    virtual void OnUnPossess() override;

private:
    void RandomMove();

private:
    // 타이머 매니저에 콜백을 등록하기 위해 필요한 핸들
    FTimerHandle TimerHandle;
};

// MyAIController.cpp
#include "NavigationSystem.h"
#include "Blueprint/AIBlueprintHelperLibrary.h"

AMyAIController::AMyAIController()
{
}

void AMyAIController::OnPossess(APawn* InPawn)
{
    // 빙의 시
    Super::OnPossess(InPawn);

    // 월드의 타이머 매니저에 콜백을 등록한다
    GetWorld()->GetTimerManager().SetTimer(TimerHandle, this, &AMyAIController::RandomMove, 3.f, true);
}

void AMyAIController::OnUnPossess()
{
    // 빙의 해제 시
    Super::OnUnPossess();

    // 등록된 콜백을 해제한다
    GetWorld()->GetTimerManager().ClearTimer(TimerHandle);
}

void AMyAIController::RandomMove()
{
    auto CurrentPawn = GetPawn();

    // 월드의 내비게이션 시스템을 가져온다
    UNavigationSystemV1* NavSystem = UNavigationSystemV1::GetNavigationSystem(GetWorld());

    if (NavSystem == nullptr)
        return;

    FNavLocation RandomLocation;

    // 월드의 내비게이션 범위중 원점(첫번째 매개변수)을 기준으로 반지름 만큼의 범위를 한정지어
    // 랜덤한 좌표를 가져온다(RandomLocation에 저장됨)
    if (NavSystem->GetRandomPointInNavigableRadius(FVector::ZeroVector, 500.f, RandomLocation))
    {
        UAIBlueprintHelperLibrary::SimpleMoveToLocation(this, RandomLocation);
    }
}

 

OnPossess와 OnUnPossess는 빙의 시, 빙의 해제 시 실행되는 함수이다. OnOverlapBegin/End같은 트리거 이벤트라고 보면 될 듯 하다.

 

빙의시 타이머 매니저에 의해 콜백이 등록되고 해제되는데 이와 같은 패턴이 서버를 만들때도 비슷하게 만들어진다.

 

 

AMyCharacter::AMyCharacter()
{
    AIControllerClass = AMyAIController::StaticClass();
    AutoPossessAI = EAutoPossessAI::PlacedInWorldOrSpawned;
}

 

AIControllerClass는 Pawn에 기본적으로 생성되어있는 AIController이다. 이것을 방금 만든 AIController로 바꿔준다.

AutoPossessAI는 자동 빙의 규칙인데 기본적으로 플레이어가 빙의된 경우는 무시된다.

월드에 배치되거나 스폰된 경우 AIController가 자동으로 빙의되도록 한다.

 

월드에 내비 메시 바운드 볼륨을 깔고 실행하면 플레이어가 아닌 맵에 배치된 캐릭터가 3초마다 랜덤한 곳으로 이동한다.

 

[정리]

 

  • AIController는 AI에 빙의와 해제를 반복하여 AI를 제어한다.
  • 비헤이비어 트리가 강력한 툴인것은 맞지만 구현이 복잡하기 때문에 매우 간단한 AI로직의 경우 꼭 비헤이비어 트리를 사용할 필요는 없다.

'언리얼 엔진 > 언리얼 엔진4 입문' 카테고리의 다른 글

[UE4 입문] Behavior Tree #2  (0) 2022.09.12
[UE4 입문] Behavior Tree #1  (0) 2022.09.12
[UE4 입문] UI 실습  (0) 2022.09.03
[UE4 입문] 스탯 매니저  (0) 2022.09.02
[UE4 입문] 아이템 줍기  (0) 2022.09.02

위젯 블루프린트를 생성해서 작업한다.

 

 

 

실습할 환경. 너비는 200, 하이트는 50으로 설정한다.

 

팔레트에서 Progress Bar를 가져와서 계층구조에 넣는데, 차후 코드에서 작성할 때 동일한 이름으로 맞춰서 작성하기 때문에 이름을 의미있게 지어주어야 좋다.

 

 

 

Progress Bar를 사용하고 속성도 저렇게 맞춰준다.

 

블루프린트 방식으로 관리해도 되지만 속도가 느린 경향이 있으므로 C++로 작업할 때는 위젯 하나당 C++ 파일을 만들어서 세트로 연동시키는 것이 더 좋다.

실습에서는 User Widget을 상속받은 C++클래스를 만들어 주도록 한다.

 

 

애니메이션 블루프린트를 관리할 때, MyAnimInstance에 온갖 필요한 정보를 넣어놓고 사용하고 C++ 코드에서는 애니메이션 블루프린트의 존재를 모르고 클래스를 통해 정보를 전달하는 느낌으로 만들어서 사용했었다.

그와 유사하게 중간에 하나의 클래스를 만들어서 데이터를 관리한다고 생각하면 된다.

 

 

C++ 클래스를 만들었다면 기본으로 설정되어있는 부모 클래스를 만들어둔 클래스로 변경한다.

 

// MyCharacter.h
UPROPERTY(VisibleAnywhere)
class UWidgetComponent* HpBar;

// MyCharacter.cpp
#include "Components/WidgetComponent.h"
#include "MyCharacterWidget.h"

AMyCharacter::AMyCharacter()
{
	// .. 생략
    
    HpBar = CreateDefaultSubobject<UWidgetComponent>(TEXT("HPBAR"));
    HpBar->SetupAttachment(GetMesh());

    // 월드공간 기준으로 배치될지(3D UI), 스크린 좌표 기준으로 배치될지(2D UI) 결정
    // Screen은 절대로 화면 밖으로 짤리지 않는다
    HpBar->SetWidgetSpace(EWidgetSpace::Screen);

	// ConstructorHelpers로 파일을 불러들여서 설정하는 익숙한 그것
    static ConstructorHelpers::FClassFinder<UUserWidget> UW(TEXT("WidgetBlueprint'/Game/UI/WBP_HPBar.WBP_HPBar_C'"));
    if (UW.Succeeded())
    {
        HpBar->SetWidgetClass(UW.Class);
        HpBar->SetDrawSize(FVector2D(200.f, 50.f));
    }
}

void AMyCharacter::PostInitializeComponents()
{
	// .. 생략
    HpBar->InitWidget();

    // todo : 체력이 변화 될 때의 델리게이트 바인딩을 해주면 됨
}

 

아직 MyCharacterWidget은 작성하지 않았다. 여기까지 작성하고 실행하면 아래와 같은 결과가 나온다.

 

 

HpBar->SetRelativeLocation(FVector(0.f, 0.f, 200.f));

 

MyCharacter의 생성자에서 해당 코드를 추가해주면 체력바가 캐릭터 머리 위로 위치하게 된다.

 

 

이제 실시간으로 체력이 변화하는것을 구현할 때이다.

 

// MyStatComponent.h
DECLARE_MULTICAST_DELEGATE(FOnHpChanged); // 델리게이트 추가

public:
	void SetHp(int32 NewHp); // 앞으로 체력 변동은 이 메소드가 맡게된다
    void GetMaxHp() { return MaxHp; }
    float GetHpRatio() { return Hp / static_cast<float>(MaxHp); }

private:
    UPROPERTY(EditAnywhere, Category = Stat, Meta = (AllowPrivateAccess = true))
    int32 MaxHp; // 최대체력 추가
    
public:
	FOnHpChanged OnHpChanged;


// MyStatComponent.cpp
void UMyStatComponent::SetLevel(int32 NewLevel)
{
	auto MyGameInstance = Cast<UMyGameInstance>(UGameplayStatics::GetGameInstance(GetWorld()));
	if (MyGameInstance)
	{
		auto StatData = MyGameInstance->GetStatData(NewLevel);
		if (StatData)
		{
			Level = StatData->Level;
			SetHp(StatData->MaxHp);
			MaxHp = StatData->MaxHp;
			Attack = StatData->Attack;
		}
	}
}

void UMyStatComponent::SetHp(int32 NewHp)
{
	Hp = NewHp;
	if (Hp < 0)
		Hp = 0;

	// 옵저버 패턴 이용. 구독하는 클래스들에게 메시지를 날려줌
    // 싱글턴으로 접근해서 수정하는 다른방법도 있다. 다만 옵저버 패턴을 이용하면 결합도가 느슨해진다
	OnHpChanged.Broadcast();
}

void UMyStatComponent::OnAttacked(float DamageAmount)
{
	int32 NewHp = Hp - DamageAmount;
	SetHp(NewHp);

	UE_LOG(LogTemp, Warning, TEXT("OnAttacked %d"), Hp);
}

 

// MyCharacterWidget.h
UCLASS()
class TESTUNREALENGINE_API UMyCharacterWidget : public UUserWidget
{
	GENERATED_BODY()

public:
	void BindHp(class UMyStatComponent* StatComp);

	void UpdateHp();

private:
	TWeakObjectPtr<class UMyStatComponent> CurrentStatComp;

	// 블루프린트에서 만들었던 Progress Bar와 매핑 되게끔 유도함
	// 따로 초기화를 하지 않아도 이름을 동일하게 하면 블루프린트에서 만들어준 것을 알아서 찾아서 바인딩 함
	UPROPERTY(meta=(BindWidget))
	class UProgressBar* PB_HpBar;
};

// MyCharacterWidget.cpp
#include "MyStatComponent.h"
#include "Components/ProgressBar.h"

void UMyCharacterWidget::BindHp(UMyStatComponent* StatComp)
{
	// PB_HpBar = Cast<UProgressBar>(GetWidgetFromName(TEXT("PB_HpBar"));
	// 자동 바인딩을 하지 않을거면 이렇게 수동으로 해주면 됨
	CurrentStatComp = StatComp;
	StatComp->OnHpChanged.AddUObject(this, &UMyCharacterWidget::UpdateHp);
}

void UMyCharacterWidget::UpdateHp()
{
	if (CurrentStatComp.IsValid())
	{
		PB_HpBar->SetPercent(CurrentStatComp->GetHpRatio());
	}
}

 

// MyCharacter.cpp
void AMyCharacter::PostInitializeComponents()
{
	// .. 생략
	// todo
	auto HpWidget = Cast<UMyCharacterWidget>(HpBar->GetUserWidgetObject());
	if (HpWidget)
	{
		HpWidget->BindHp(Stat);
	}
}

 

전체적인 흐름은 꽤 복잡할 수 있다.

 

MyCharacter에서 HpBar에 대한 세팅과 BindHp로 바인딩

-> 피격시 AttackCheck에 의해 TakeDamage를 호출

-> TakeDamage에서 OnAttacked를 호출

-> OnAttacked에서 SetHp를 호출

-> SetHp에서 브로드캐스팅 호출

-> 맨 처음 BindHP에서 구독을 한 UpdateHp 호출

-> 최종적으로 UpdateHp에서 CurrentStatComp에서 체력 비율을 받아와서 Progress Bar에 적용하게 된다.

 

전반적인 흐름은 UI를 만들고 이름을 지어준 것을 이어 받아서 C++ 클래스에서 잘 바인딩 하여 연동시켜주면 된다.

'언리얼 엔진 > 언리얼 엔진4 입문' 카테고리의 다른 글

[UE4 입문] Behavior Tree #1  (0) 2022.09.12
[UE4 입문] AI Controller  (0) 2022.09.11
[UE4 입문] 스탯 매니저  (0) 2022.09.02
[UE4 입문] 아이템 줍기  (0) 2022.09.02
[UE4 입문] 소켓 실습  (0) 2022.09.02

DX나 유니티의 경우 싱글톤 패턴이나 전역 클래스를 이용해서 어디서든 편하게 접근할 수 있는 데이터 매니저를 만들어서 관리하면 굉장히 편했으나, 언리얼의 경우 이미 구조가 어느정도 잡혀있기 때문에 데이터 매니저를 만든다고 하면 어디에 둘지 굉장히 고민이 된다.

물론, 언리얼은 이미 구비가 되어있다. C++ 클래스의 GameInstance를 상속받아 만들면 된다.

 

보통 기획 직군이 밸런스를 담당해서 붙여줄텐데 데이터(수치)들을 하드코딩해서 관리하면 편하게 작업할 수 없고 수정사항이 발생할 때마다 프로그래머가 고쳐주어야 하는 불편함이 있다.

뿐만 아니라 하드코딩을 해버리면 실행파일 자체에 묶여서 들어가기때문에 수치만 조정하고 싶어도 다시 빌드해서 배포하는 수밖에 없다.

그러므로 데이터와 코드는 분리하는게 여러모로 편하다

 

 

// MyGameInstance.h

#include "Engine/DataTable.h"

USTRUCT()
struct  FMyCharacterData : public FTableRowBase
{
	GENERATED_BODY()

	UPROPERTY(EditAnywhere, BlueprintReadWrite)
	int32 Level;

	UPROPERTY(EditAnywhere, BlueprintReadWrite)
	int32 Attack;

	UPROPERTY(EditAnywhere, BlueprintReadWrite)
	int32 MaxHp;
};

 

 

행 구조 선택에서 C++ 코드로 작성해둔 구조체를 선택해주면 된다.

 

 

 

이제 이 데이터 테이블은 바이너리에 묶여서 나가는 방식이 아닌 외부 파일로 빠지게 된다.

행을 추가해서 각각의 데이터를 정해놓을 수 있다. 물론 이 방법뿐만 json이나 xml등을 파싱해서 사용하는것도 하나의 방법이다.

 

 

// MyGameInstance.h
class TESTUNREALENGINE_API UMyGameInstance : public UGameInstance
{
	GENERATED_BODY()

public:
	UMyGameInstance();

	virtual void Init() override;

	FMyCharacterData* GetStatData(int32 Level);

private:
	UPROPERTY()
	class UDataTable* MyStats;
	
};

// MyGameInstance.cpp
UMyGameInstance::UMyGameInstance()
{
	static ConstructorHelpers::FObjectFinder<UDataTable> DATA(TEXT("DataTable'/Game/Data/StatTable.StatTable'"));
	
	MyStats = DATA.Object;
}

void UMyGameInstance::Init()
{
	Super::Init();

	UE_LOG(LogTemp, Warning, TEXT("MyGameInstance %d"), GetStatData(1)->Attack);
}

FMyCharacterData* UMyGameInstance::GetStatData(int32 Level)
{
	return MyStats->FindRow<FMyCharacterData>(*FString::FromInt(Level), TEXT(""));
}

 

 

만들어둔 게임 인스턴스 클래스를 사용하려면 [세팅 - 프로젝트 세팅 - 맵&모드] 에서 게임 인스턴스 클래스를 바꿔주어야 한다.

 

 

A캐릭터와 B캐릭터간의 전투 시스템이 구현되고 A가 B를 공격했을 때, A클래스에서 B클래스에 접근해서 체력을 깎아야 하는지, 혹은 중간의 매니저쪽에서 처리해야 하는지가 고민이 된다.

대부분의 경우에서는 데미지를 받는 쪽에서 피격 함수를 만들어서 처리하는게 여러 상황들이 발생했을 때를 고려하면 좀 더 깔끔하다.

 

데이터 테이블로 데이터를 외부로 빼서 관리한다고 해도 캐릭터의 현재 체력같은 것들은 결국 클래스에서 관리를 해야하는 부분이다. 그렇다고 해당 데이터들을 계속 추가하는 것은 많아지면 관리하기 어려우므로 스탯 컴포넌트를 따로 만들어서 스탯끼리 관리하고 캐릭터에 붙여놓는게 조금 더 편리한 방법일 수 있다.

 

// MyStatComponent.h
class TESTUNREALENGINE_API UMyStatComponent : public UActorComponent
{
	GENERATED_BODY()

public:	
	// Sets default values for this component's properties
	UMyStatComponent();

protected:
	// Called when the game starts
	virtual void BeginPlay() override;
	virtual void InitializeComponent() override;

public:
	void SetLevel(int32 Level);
	void OnAttacked(float DamageAmount);

	int32 GetLevel() { return Level; }
	int32 GetHp() { return Hp; }
	int32 GetAttack() { return Attack; }

private:
	UPROPERTY(EditAnywhere, Category=Stat, Meta=(AllowPrivateAccess=true))
	int32 Level;

	UPROPERTY(EditAnywhere, Category = Stat, Meta = (AllowPrivateAccess = true))
	int32 Hp;
	
	UPROPERTY(EditAnywhere, Category = Stat, Meta = (AllowPrivateAccess = true))
	int32 Attack;
};

// MyStatComponent.cpp
#include "MygameInstance.h"
#include "Kismet/GameplayStatics.h"

// Sets default values for this component's properties
UMyStatComponent::UMyStatComponent()
{
	// Set this component to be initialized when the game starts, and to be ticked every frame.  You can turn these features
	// off to improve performance if you don't need them.
	PrimaryComponentTick.bCanEverTick = false; // 틱이 따로 필요없음

	bWantsInitializeComponent = true; // InitializeComponent 함수 호출을 위해 true
	Level = 1;

	// ...
}


// Called when the game starts
void UMyStatComponent::BeginPlay()
{
	Super::BeginPlay();

	// ...
	
}

void UMyStatComponent::InitializeComponent()
{
	Super::InitializeComponent();

	SetLevel(Level);
}

void UMyStatComponent::SetLevel(int32 NewLevel)
{
	// GameInstance를 받아와서 안의 값을 가져옴
	auto MyGameInstance = Cast<UMyGameInstance>(UGameplayStatics::GetGameInstance(GetWorld()));
	if (MyGameInstance)
	{
		auto StatData = MyGameInstance->GetStatData(NewLevel);
		if (StatData)
		{
			Level = StatData->Level;
			Hp = StatData->MaxHp;
			Attack = StatData->Attack;
		}
	}

}

void UMyStatComponent::OnAttacked(float DamageAmount)
{
	Hp -= DamageAmount;
	if (Hp < 0)
		Hp = 0;

	UE_LOG(LogTemp, Warning, TEXT("OnAttacked %d"), Hp);
}

// MyCharacter.h
	virtual float TakeDamage(float DamageAmount, FDamageEvent const& DamageEvent, AController* EventInstigator, AActor* DamageCauser) override;
	
    UPROPERTY(VisibleAnywhere)
    class UMyStatComponent* Stat;
    
// MyCharacter.cpp
#include "MyStatComponent.h"

AMyCharacter::AMyCharacter()
{
	// .. 생략
	Stat = CreateDefaultSubobject<UMyStatComponent>(TEXT("STAT"));
}

void AMyCharacter::AttackCheck()
{
	// .. 생략
    if (bResult && HitResult.Actor.IsValid())
    {
        FDamageEvent DamageEvent;
        UE_LOG(LogTemp, Log, TEXT("Hit Actor : %s"), *HitResult.Actor->GetName());
        // 데미지를 받는쪽의 TakeDamage를 호출한다
        HitResult.Actor->TakeDamage(Stat->GetAttack(), DamageEvent, GetController(), this);
    }
}

float AMyCharacter::TakeDamage(float DamageAmount, FDamageEvent const& DamageEvent, AController* EventInstigator, AActor* DamageCauser)
{
	Stat->OnAttacked(DamageAmount);

	return DamageAmount;
}

 

 

'언리얼 엔진 > 언리얼 엔진4 입문' 카테고리의 다른 글

[UE4 입문] AI Controller  (0) 2022.09.11
[UE4 입문] UI 실습  (0) 2022.09.03
[UE4 입문] 아이템 줍기  (0) 2022.09.02
[UE4 입문] 소켓 실습  (0) 2022.09.02
[UE4 입문] 충돌 기초  (0) 2022.09.02

 

 

콜리전 채널과 프리셋 세팅을 우선 해준다.

 

// MyWeapon.h
protected:
	// Called when the game starts or when spawned
	virtual void BeginPlay() override;

	virtual void PostInitializeComponents() override;

private:
	UFUNCTION()
	void OnCharacterOverlap(UPrimitiveComponent* OverlappedComp, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult);

public:
	UPROPERTY(VisibleAnywhere)
	UStaticMeshComponent* Weapon;

	UPROPERTY(VisibleAnywhere)
	class UBoxComponent* Trigger;
};

// MyWeapon.cpp
AMyWeapon::AMyWeapon()
{
 	// 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; // 무기는 Tick이 필요없다

	Weapon = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("WEAPON"));
	Trigger = CreateDefaultSubobject<UBoxComponent>(TEXT("TRIGGER"));
	
	static ConstructorHelpers::FObjectFinder<UStaticMesh> SW(TEXT("StaticMesh'/Game/ParagonGreystone/FX/Meshes/Heroes/Greystone/SM_Greystone_Blade_01.SM_Greystone_Blade_01'"));
	if (SW.Succeeded())
	{
		Weapon->SetStaticMesh(SW.Object);
	}
	
	Weapon->SetupAttachment(RootComponent);
	Trigger->SetupAttachment(Weapon); // 안하면 트리거가 따라가지 않을 수 있음

	// 만들어둔 콜리전 프리셋 설정
	Weapon->SetCollisionProfileName(TEXT("MyCollectible"));
	Trigger->SetCollisionProfileName(TEXT("MyCollectible"));
	Trigger->SetBoxExtent(FVector(30.f, 30.f, 30.f)); // 충돌 박스 생성
}

void AMyWeapon::PostInitializeComponents()
{
	Super::PostInitializeComponents();

	// 트리거 작동(충돌체크 발생)시 실행할 메소드를 콜백으로 넘겨줌
	Trigger->OnComponentBeginOverlap.AddDynamic(this, &AMyWeapon::OnCharacterOverlap);
}

void AMyWeapon::OnCharacterOverlap(UPrimitiveComponent* OverlappedComp, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
	UE_LOG(LogTemp, Log, TEXT("Overlapped"));

	AMyCharacter* MyCharacter = Cast<AMyCharacter>(OtherActor);

	if (MyCharacter)
	{
		FName WeaponSocket(TEXT("hand_l_socket"));

		// 충돌된 캐릭터(MyCharacter)의 WeaponSocket에 현재 컴포넌트를 붙여준다(MyWeapon)
		AttachToComponent(MyCharacter->GetMesh(),
			FAttachmentTransformRules::SnapToTargetNotIncludingScale,
			WeaponSocket);
	}
}

 

MyWeapon C++ 클래스를 맵에 몇개 배치해두고 위로 지나다니면 충돌 처리시 소켓에 붙게 된다.

'언리얼 엔진 > 언리얼 엔진4 입문' 카테고리의 다른 글

[UE4 입문] UI 실습  (0) 2022.09.03
[UE4 입문] 스탯 매니저  (0) 2022.09.02
[UE4 입문] 소켓 실습  (0) 2022.09.02
[UE4 입문] 충돌 기초  (0) 2022.09.02
[UE4 입문] 블렌드 스페이스  (0) 2022.09.02

+ Recent posts