Unreal - Behavior Tree(5) 스폰 / 디스폰

2025. 5. 8. 13:26·Unreal5 프로젝트 다이어리

BehaviorTree에서 앞서 만들어둔 Dissolve Material을 사용하여

스폰과 디스폰을 구현해보도록하겠습니다

 

Dissolve Material 제작방법

2025.05.08 - [Unreal5 프로젝트 다이어리] - Unreal - Dissolve Material

 

Unreal - Dissolve Material

Behavior Tree의 AI가 죽었을때 애니메이션이 나오면서 사라지게하는것도 나쁘지않지만갑자기 사라져버리는것보다는 점점 사라지면서 마지막에는 없어지는방식으로 표현한다면 조금더 자연스러

lucodev.tistory.com

 

처음에 스폰을 했냐 안했냐 를 담당할 블랙보드키와

해당 키를 변경해줄 Task를 생성해주겠습니다

Spawn이라는 bool blackboard key를 추가해주었습니다

 

AI의 DissolveMaterial연출을 하며 3초뒤에 신호를 넘겨주는 태스크를 만들어보겠습니다

Task_BlackBoardBase를 상속받아 Task를 생성해줍니다

필요한 ExecuteTask, TickTask 그리고 deltaSeonds를 누적받아 신호를 컨트롤할 currentTime을 선언해줍니다.

public:
	UTask_SkeletonSpawn();

	virtual EBTNodeResult::Type ExecuteTask(UBehaviorTreeComponent& ownerComp, uint8* nodeMemory) override;
	virtual void TickTask(UBehaviorTreeComponent& ownerComp, uint8* nodeMemory, float deltaSeconds) override;
	float currentTime = 0.0f;

 

제가만든 skeleton과 필요한 헤더를 추가 선언해주겠습니다

#include "BasicSkeletonEnemy.h"
#include "BehaviorTree/BlackboardComponent.h"

 

3초동안 신호를 가지고있다가 블랙보드 Spawn의 이름을 가진

Bool Key의 값을 변경해주겠습니다

UTask_SkeletonSpawn::UTask_SkeletonSpawn()
{
	NodeName = "Dissolve Material Activate";
	bNotifyTick = true;
}

EBTNodeResult::Type UTask_SkeletonSpawn::ExecuteTask(UBehaviorTreeComponent& ownerComp, uint8* nodeMemory)
{
	Super::ExecuteTask(ownerComp, nodeMemory);
	return EBTNodeResult::InProgress;
}

void UTask_SkeletonSpawn::TickTask(UBehaviorTreeComponent& ownerComp, uint8* nodeMemory, float deltaSeconds)
{
	Super::TickTask(ownerComp, nodeMemory, deltaSeconds);
	currentTime += deltaSeconds;
	UBlackboardComponent* blackBoard = ownerComp.GetBlackboardComponent();
	if (currentTime < 3.0)
	{
		
	}
	else
	{
		blackBoard->SetValueAsBool("Spawn", true);
		FinishLatentTask(ownerComp, EBTNodeResult::Succeeded);
	}
}

 

수정된 BehaviorTree의 모습입니다

EnumState상위에 Selector을 만들고 서비스와 Decorator조건을 입력해주었습니다

SpawnBefore은 Spawn Key에 대한 Is Not Set값 즉 스폰 전 이라는 상태이며

SpawnAfter은 Spawn Key에 대한 Is Set이라는 즉 스폰 후 라는 상태입니다

 

3.0초가 지나면 SpawnKey가 변경되어 신호가 잘 넘어가는것을 확인할수있습니다

 

그럼 이제 DissolveMaterial을 적용해보겠습니다

메테리얼에 메테리얼 파라미터 컬렉션을 추가한뒤 그 값을 변동시켜주는것으로

제어하겠습니다

 

현재 Dissolve의 최소값은 -0.4 최대값은 0.7입니다

필요한 변수를 .h에서 선언해주었습니다

 

또한 여기서 중요한점이 있습니다

currentTime을 DeltaSeconds에 그냥 중첩시켜서 사용한다면

AI가 여러마리일 경우 빠르게 중첩이 되어 시간이 더 빨리 끝나버리는 현상이 일어납니다

즉 curentTime는 공유된다는것입니다

한마리가 currentTime += deltaSecond할때 다른한마리도 같이 currentTime에 덧붙혀 

더 빨리 누적되는 현상이 일어납니다

 

Task는 nodeMemory라는 개별 메모리 공간을 제공하는데

구조체로 reinterpret해서 사용한다면

AI마다 nodeMemory가 따로있으니 struct도 ai마다 따로 존재하며

개별 AI의 dissolve 타이밍도 유지가 가능해집니다

.h에 Struct를 정의를 해주고 Dissolve의 최소값과 최대값 (-0.4 ~ 0.7)값을 사용할 변수를 선언해줍니다

USTRUCT()
struct FTaskSkeletonSpawnMemory
{
	GENERATED_BODY()

	float structCurrentTime = 0.0f;
};

UCLASS()
class BLASTERDREAM_API UTask_SkeletonSpawn : public UBTTask_BlackboardBase
{
	GENERATED_BODY()
	
public:
	UTask_SkeletonSpawn();

	virtual EBTNodeResult::Type ExecuteTask(UBehaviorTreeComponent& ownerComp, uint8* nodeMemory) override;
	virtual void TickTask(UBehaviorTreeComponent& ownerComp, uint8* nodeMemory, float deltaSeconds) override;
	float currentTime = 0.0f;
	float minDissolveValue;
	float maxDissolveValue;

	
};

.cpp

struct로 따로 별도의 structCurrentTime을 만들고

이 구조체의 structCurrentTime에 deltaSeconds를 누적시킵니다

이값에 따라 결정한다면 아무리 AI가 다수가 되어도 값은 별도로 작동합니다

 

 

void UTask_SkeletonSpawn::TickTask(UBehaviorTreeComponent& ownerComp, uint8* nodeMemory, float deltaSeconds)
{
	Super::TickTask(ownerComp, nodeMemory, deltaSeconds);
	FTaskSkeletonSpawnMemory* taskMemory = reinterpret_cast<FTaskSkeletonSpawnMemory*>(nodeMemory);

	taskMemory->structCurrentTime += deltaSeconds;
	UBlackboardComponent* blackBoard = ownerComp.GetBlackboardComponent();
	if (taskMemory->structCurrentTime < 3.0f)
	{
		
		float dissolveValue = FMath::Lerp(maxDissolveValue, minDissolveValue, taskMemory->structCurrentTime / 3.0f);
		ABasicSkeletonEnemy* skeleton = Cast<ABasicSkeletonEnemy>(ownerComp.GetAIOwner()->GetPawn());
		for (int32 i = 0; i < 3; ++i)
		{
			int32 materialIndex = i;

			UMaterialInstanceDynamic* dynMat = skeleton->GetMesh()->CreateAndSetMaterialInstanceDynamic(materialIndex);
			if (dynMat)
			{
				dynMat->SetScalarParameterValue("Dissolve", dissolveValue);
			}
		}
		if (skeleton->sword)
		{
			int32 swordMatCount = skeleton->sword->GetNumMaterials();
			for (int32 i = 0; i < swordMatCount; ++i)
			{
				UMaterialInstanceDynamic* swordMat = skeleton->sword->CreateAndSetMaterialInstanceDynamic(i);
				if (swordMat)
				{
					swordMat->SetScalarParameterValue("Dissolve", dissolveValue);
				}
			}
		}
	}
	else
	{
		ABasicSkeletonEnemy* skeleton = Cast<ABasicSkeletonEnemy>(ownerComp.GetAIOwner()->GetPawn());
		blackBoard->SetValueAsBool("Spawn", true);
		FinishLatentTask(ownerComp, EBTNodeResult::Succeeded);
	}
}

maxDissolve ~ minDissove값만큼 Lerp한 값을 구해준뒤

dynaMat이라는 다이나믹 메테리얼을 만든뒤

Dissolve값을 변동시켜주었습니다

또한 AI가 들고있는 sword또한 같이 Dissolve 시켜주었습니다

 

또한 플레이어가 ai를 때릴때 blackboard 값을 체킹해서 스폰하고있을때는 때릴수없도록

예외처리 해주었습니다. 

 

결과

 

enum을 체크하는 Service노드에서 코드를 추가해줬습니다

if (skeleton->currentHp <= 0.0f)
{
	currentState = EAIStateEnum::Die;
	blackBoard->SetValueAsEnum("AIState", static_cast<uint8>(currentState));
	return;
}

죽었을때의 Task를 생성해보겠습니다

Task가 실행할 일은

1. Dissolve Material의 작동

2. Ragdoll실행

3. Collision을 NoCollision으로

4. Dissolve Material이 끝나면 Destroy

이렇게 총 4개입니다

 

완성된 cpp 코드입니다

UTask_SkeletonDie::UTask_SkeletonDie()
{
	NodeName = "Skeleton Die";
	bNotifyTick = true;
}

EBTNodeResult::Type UTask_SkeletonDie::ExecuteTask(UBehaviorTreeComponent& ownerComp, uint8* nodeMemory)
{
	Super::ExecuteTask(ownerComp, nodeMemory);
	minDissolveValue = -0.4f;
	maxDissolveValue = 0.7f;

	FTaskSkeletonDieMemory* taskMemory = reinterpret_cast<FTaskSkeletonDieMemory*>(nodeMemory);
	taskMemory->structCurrentTime;
	ASkeletonAIController* skeletalController = Cast<ASkeletonAIController>(ownerComp.GetAIOwner());
	skeletalController->StopMovement();
	ABasicSkeletonEnemy* skeleton = Cast<ABasicSkeletonEnemy>(ownerComp.GetAIOwner()->GetPawn());
	skeleton->GetMesh()->SetSimulatePhysics(true);
	skeleton->GetMesh()->SetCollisionResponseToChannel(ECC_Pawn, ECR_Ignore);
	skeleton->GetCapsuleComponent()->SetCollisionResponseToChannel(ECC_Pawn, ECR_Ignore);
	

	return EBTNodeResult::InProgress;
}

void UTask_SkeletonDie::TickTask(UBehaviorTreeComponent& ownerComp, uint8* nodeMemory, float deltaSeconds)
{
	Super::TickTask(ownerComp, nodeMemory, deltaSeconds);
	FTaskSkeletonDieMemory* taskMemory = reinterpret_cast<FTaskSkeletonDieMemory*>(nodeMemory);

	taskMemory->structCurrentTime += deltaSeconds;
	UBlackboardComponent* blackBoard = ownerComp.GetBlackboardComponent();
	
	if (taskMemory->structCurrentTime >= 7.0f)
	{
		//만약 깡으로 계산한다면
		//float dissolveValue = FMath::Lerp(minDissolveValue, maxDissolveValue, taskMemory->structCurrentTime / 9.0f);

		float dissolveAlpha = FMath::Clamp((taskMemory->structCurrentTime - 7.0f) / 2.0f, 0.0f, 1.0f);
		float dissolveValue = FMath::Lerp(minDissolveValue, maxDissolveValue, dissolveAlpha);
		ABasicSkeletonEnemy* skeleton = Cast<ABasicSkeletonEnemy>(ownerComp.GetAIOwner()->GetPawn());
		for (int32 i = 0; i < 3; ++i)
		{
			int32 materialIndex = i;
			UMaterialInstanceDynamic* dynMat = skeleton->GetMesh()->CreateAndSetMaterialInstanceDynamic(materialIndex);
			if (dynMat)
			{
				dynMat->SetScalarParameterValue("Dissolve", dissolveValue);
			}
		}
		if (skeleton->sword)
		{
			int32 swordMatCount = skeleton->sword->GetNumMaterials();
			for (int32 i = 0; i < swordMatCount; ++i)
			{
				UMaterialInstanceDynamic* swordMat = skeleton->sword->CreateAndSetMaterialInstanceDynamic(i);
				if (swordMat)
				{
					swordMat->SetScalarParameterValue("Dissolve", dissolveValue);
				}
			}
		}
		skeleton->sword->SetVisibility(false);
	}
	if (taskMemory->structCurrentTime >= 9.1f)
	{
		ABasicSkeletonEnemy* skeleton = Cast<ABasicSkeletonEnemy>(ownerComp.GetAIOwner()->GetPawn());
		skeleton->Destroy();
	}
}

 

Pawn채널을 무시해서 시체가 발에 걸리지않게 해주고

Dissolve를 적용시켜주었습니다

그리고 일정시간이 지나면 Destroy 시켜주었습니다

 

결과물

 

 

'Unreal5 프로젝트 다이어리' 카테고리의 다른 글

Unreal - AI에게 HP 프로그래스바 붙히기  (0) 2025.05.09
Unreal - ProjectTile을 사용한 원거리 AI 만들기  (0) 2025.05.09
Unreal - Dissolve Material  (0) 2025.05.08
Unreal - Behavior Tree(4) AI Perception Team ID  (1) 2025.05.04
Unreal - Behavior Tree(3) 플레이어와의 상호작용  (0) 2025.05.02
'Unreal5 프로젝트 다이어리' 카테고리의 다른 글
  • Unreal - AI에게 HP 프로그래스바 붙히기
  • Unreal - ProjectTile을 사용한 원거리 AI 만들기
  • Unreal - Dissolve Material
  • Unreal - Behavior Tree(4) AI Perception Team ID
lucodev
lucodev
커피와 노트북 그리고 개발
  • lucodev
    루코 개발테이블
    lucodev
  • 전체
    오늘
    어제
    • 분류 전체보기 (125) N
      • Unreal5 프로젝트 다이어리 (73)
      • Unreal5 프로젝트 다이어리2 (5) N
      • Unreal 팁 (8)
      • Unreal 디버깅 (8)
      • C++ 프로그래머스 다이어리 (23) N
        • Stack (3)
        • Hash (4)
        • Heap (2)
        • Sort (3) N
      • 코드 개인보관함 (8) N
  • 인기 글

  • 최근 글

  • 최근 댓글

  • 링크

  • 공지사항

  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 태그

    언리얼 로딩
    언리얼 foot step
    언리얼 시퀀스
    언리얼 비헤이비어트리
    unreal 모션매칭
    unreal 로딩
    unreal look at
    언리얼 look at
    언리얼 모션매칭
    언리얼 behaviortree
    unreal 컷씬
    언리얼 로딩창
    unreal 시퀀스
    언리얼 behavior tree
    unreal sequence
    언리얼 motionmatching
    언리얼 컷씬
    언리얼
    언리얼 페이드 아웃
    unreal loading
  • hELLO· Designed By정상우.v4.10.3
lucodev
Unreal - Behavior Tree(5) 스폰 / 디스폰
상단으로

티스토리툴바