Unreal Engine C++ The Ultimate Shooter Course (6)

Date:     Updated:

카테고리:

Chapter 15 AI and Behavior Trees

15-272 Unreal Engine AI

  • AI Character, Use AI Controller
  • Behavior Tree, Makes decisions
  • Blackboard, Stores data for the Behavior Tree
  • AI Controller, Moves the Character
  • Navigation Mesh, Enables AI navigation
  • Now, remember, the sequence( In Behavior Tree Nodes ) will execute the child tasks until one of them fails

15-273 Adding AI Modules

// Shooter.Build.cs

public class Shooter : ModuleRules
{
    public Shooter(ReadOnlyTargetRules Target) : base(Target)
    {
        PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;

        // Add AI Module
        PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore", "UMG", "PhysicsCore", "NavigationSystem", "AIModule" });

        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
    }
}
class SHOOTER_API AEnemy : public ACharacter, public IBulletHitInterface
{
    /** Behavior tree for the AI Character */
	UPROPERTY(EditAnywhere, Category = "Behavior Tree", meta = (AllowPrivateAccess = "true"))
	class UBehaviorTree* BehaviorTree;
}

15-274 AI Controller

ai

class SHOOTER_API AEnemy : public ACharacter, public IBulletHitInterface
{
    //..

    FORCEINLINE UBehaviorTree* GetBehaviorTree() const { return BehaviorTree; }

    //..
}
void AEnemyController::OnPossess(APawn* InPawn)
{
	Super::OnPossess(InPawn);
	if (InPawn == nullptr) return;

	AEnemy* Enemy = Cast<AEnemy>(InPawn);
	if (Enemy)
	{
		if (Enemy->GetBehaviorTree())
		{
			BlackboardComponent->InitializeBlackboard(*(Enemy->GetBehaviorTree()->BlackboardAsset));
		}
	}
}

15-275 Creating a Blackboard and Behavior Tree

bp

board

explain

btree

default

set

15-276 Patrol Point

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

	GetMesh()->SetCollisionResponseToChannel(ECollisionChannel::ECC_Visibility, ECollisionResponse::ECR_Block);
	
	// Converted from local space to worldspace
	const FVector WorldPatrolPoint = UKismetMathLibrary::TransformLocation(
		GetActorTransform(),
		PatrolPoint);

	DrawDebugSphere(
		GetWorld(),
		WorldPatrolPoint,
		25.f,
		12,
		FColor::Red,
		true
	);
}

pat

15-277 Move to Task

add

save

cont

pkey

void AEnemy::BeginPlay()
{
	//..

	if (EnemyController)
	{
		EnemyController->GetBlackboardComponent()->SetValueAsVector(
			TEXT("PatrolPoint"),
			WorldPatrolPoint);

		EnemyController->RunBehaviorTree(BehaviorTree);
	}
}

15-278 Enemy Run Animation

blend

update

space

15-279 Second Patrol Point

next

two

15-280 Wait Task

wait

15-281 Agro Sphere

obj

agro

display

void AEnemy::AgroSphereOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
	if (OtherActor == nullptr) return;

	auto Character = Cast<AShooterCharacter>(OtherActor);
	if (Character)
	{
		// Set the value of the Target Blackboard Key
		EnemyController->GetBlackboardComponent()->SetValueAsObject(
			TEXT("Target"),
			Character);
	}
}

15-282 Move To Actor

black

15-283 Stun Enemy

finish

anim

stun

15-284 Stunned Blackboard Decorator

save

check

void AEnemy::SetStunned(bool Stunned)
{
	bStunned = Stunned;

	if (EnemyController)
	{
		EnemyController->GetBlackboardComponent()->SetValueAsBool(
			TEXT("Stunned"),
			Stunned);
	}
}

fun

board

value

  • A decorator node behaves like a conditional It’s like an F check that says if a particular condition is true, then allow the node to execute, otherwise prevent the node from executing

15-285 Selector Node

  • The selector will continue executing children until one of them succeeds

blackboard

chose

node

15-286 In Attack Range

range

void AEnemy::CombatRangeOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
	if (OtherActor == nullptr) return;
	auto ShooterCharacter = Cast<AShooterCharacter>(OtherActor);
	if (ShooterCharacter)
	{
		bInAttackRange = true;
		if (EnemyController)
		{
			EnemyController->GetBlackboardComponent()->SetValueAsBool(
				TEXT("InAttackRange"),
				true
			);
		}
	}
}
void AEnemy::CombatRangeEndOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex)
{
	if (OtherActor == nullptr) return;
	auto ShooterCharacter = Cast<AShooterCharacter>(OtherActor);
	if (ShooterCharacter)
	{
		bInAttackRange = false;
		if (EnemyController)
		{
			EnemyController->GetBlackboardComponent()->SetValueAsBool(
				TEXT("InAttackRange"),
				false
			);
		}
	}
}

attack

true

15-287 Enemy Attack Montage

mon

slot

nodegood

void AEnemy::PlayAttackMontage(FName Section, float PlayRate)
{
	UAnimInstance* AnimInstance = GetMesh()->GetAnimInstance();
	if (AnimInstance && AttackMontage)
	{
		AnimInstance->Montage_Play(AttackMontage);
		AnimInstance->Montage_JumpToSection(Section, AttackMontage);
	}
}

15-288 Get Attack Section Name

FName AEnemy::GetAttackSectionName()
{
	FName SectionName;
	const int32 Section{ FMath::RandRange(1, 4) };
	switch (Section)
	{
	case 1:
		SectionName = AttackLFast;
		break;
	case 2:
		SectionName = AttackRFast;
		break;
	case 3:
		SectionName = AttackL;
		break;
	case 4:
		SectionName = AttackR;
		break;
	}
	return SectionName;
}

15-289 Custom Behavior Tree Task

  • Now, remember, a sequence node will execute its children from left to right until one of them fails

task

section

sel

know

void AEnemy::BeginPlay()
{
	//..

	// Ignore the camera for Mesh and Capsule
	GetMesh()->SetCollisionResponseToChannel(
		ECollisionChannel::ECC_Camera,
		ECollisionResponse::ECR_Ignore);
	GetCapsuleComponent()->SetCollisionResponseToChannel(
		ECollisionChannel::ECC_Camera,
		ECollisionResponse::ECR_Ignore
	);

	//..
}

15-290 Weapon Collision Volumes

socket

void AEnemy::BeginPlay()
{
	//..

	// Bind functions to overlap events for weapon boxes
	LeftWeaponCollision->OnComponentBeginOverlap.AddDynamic(
		this,
		&AEnemy::OnLeftWeaponOverlap);
	RightWeaponCollision->OnComponentBeginOverlap.AddDynamic(
		this,
		&AEnemy::OnRightWeaponOverlap);

	// Set collision presets for weapon boxes
	LeftWeaponCollision->SetCollisionEnabled(ECollisionEnabled::NoCollision);
	LeftWeaponCollision->SetCollisionObjectType(ECollisionChannel::ECC_WorldDynamic);
	LeftWeaponCollision->SetCollisionResponseToAllChannels(ECollisionResponse::ECR_Ignore);
	LeftWeaponCollision->SetCollisionResponseToChannel(
		ECollisionChannel::ECC_Pawn,
		ECollisionResponse::ECR_Overlap);
	RightWeaponCollision->SetCollisionEnabled(ECollisionEnabled::NoCollision);
	RightWeaponCollision->SetCollisionObjectType(ECollisionChannel::ECC_WorldDynamic);
	RightWeaponCollision->SetCollisionResponseToAllChannels(ECollisionResponse::ECR_Ignore);
	RightWeaponCollision->SetCollisionResponseToChannel(
		ECollisionChannel::ECC_Pawn,
		ECollisionResponse::ECR_Overlap);

	//..
}

pause

15-291 Activate and Deactivate Collision

active

de

15-292 Enemy Damage

void AEnemy::DoDamage(AActor* Victim)
{
	if (Victim == nullptr) return;
	auto Character = Cast<AShooterCharacter>(Victim);
	if (Character)
	{
		UGameplayStatics::ApplyDamage(
			Character,
			BaseDamage,
			EnemyController,
			this,
			UDamageType::StaticClass()
		);
	}
}
float AShooterCharacter::TakeDamage(float DamageAmount, FDamageEvent const& DamageEvent, AController* EventInstigator, AActor* DamageCauser)
{
	if (Health - DamageAmount <= 0.f)
	{
		Health = 0.f;
	}
	else
	{
		Health -= DamageAmount;
	}
	return DamageAmount;
}

15-293 Character Health Bar

bp

bind

vari

hp

health

tint

radi

15-294 Enemy Weapon Attack Sounds

swing

sound

15-295 Melee Impact Sound

void AEnemy::DoDamage(AActor* Victim)
{
	if (Victim == nullptr) return;
	auto Character = Cast<AShooterCharacter>(Victim);
	if (Character)
	{
		UGameplayStatics::ApplyDamage(
			Character,
			BaseDamage,
			EnemyController,
			this,
			UDamageType::StaticClass()
		);

		if (Character->GetMeleeImpactSound())
		{
			UGameplayStatics::PlaySoundAtLocation(
				this,
				Character->GetMeleeImpactSound(),
				GetActorLocation());
		}
	}
}

hit

15-296 Enemy Vocal Attack Sounds

grux

pain

15-297 Weapon Trails

trail

15-298 Blood Particles

class SHOOTER_API AShooterCharacter : public ACharacter
{
	//..

	/** Blood splatter particles for melee hit */
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Combat, meta = (AllowPrivateAccess = "true"))
	UParticleSystem* BloodParticles;

	//..
}
void AEnemy::SpawnBlood(AShooterCharacter* Victim, FName SocketName)
{
	const USkeletalMeshSocket* TipSocket{ GetMesh()->GetSocketByName(SocketName) };
	if (TipSocket)
	{
		const FTransform SocketTransform{ TipSocket->GetSocketTransform(GetMesh()) };
		if (Victim->GetBloodParticles())
		{
			UGameplayStatics::SpawnEmitterAtLocation(
				GetWorld(),
				Victim->GetBloodParticles(),
				SocketTransform
			);
		}
	}
}

parti

15-299 Character Stun State

UENUM(BlueprintType)
enum class ECombatState : uint8
{
	ECS_Unoccupied UMETA(DisplayName = "Unoccupied"),
	ECS_FireTimerInProgress UMETA(DisplayName = "FireTimerInProgress"),
	ECS_Reloading UMETA(DisplayName = "Reloading"),
	ECS_Equipping UMETA(DisplayName = "Equipping"),

	// Stunned 상태 추가
	ECS_Stunned UMETA(DisplayName = "Stunned"),

	ECS_MAX UMETA(DisplayName = "DefaultMAX")
};

15-300 End Stun Anim Notify

mon

front

end

stun

15-301 Stun Character on Hit

void AEnemy::StunCharacter(AShooterCharacter* Victim)
{
	if (Victim)
	{
		const float Stun{ FMath::FRandRange(0.f, 1.f) };
		if (Stun <= Victim->GetStunChance())
		{
			Victim->Stun();
		}
	}
}

tage

react

slot

15-302 Limit Enemy Attacks

void AEnemy::PlayAttackMontage(FName Section, float PlayRate)
{
	//..

	bCanAttack = false;
	GetWorldTimerManager().SetTimer(
		AttackWaitTimer,
		this,
		&AEnemy::ResetCanAttack,
		AttackWaitTime
	);
	if (EnemyController)
	{
		EnemyController->GetBlackboardComponent()->SetValueAsBool(
			FName("CanAttack"),
			false);
	}
}

can

black

15-303 Delay Before Chasing Player

mov

motion

15-304 Agro Enemy when Shot

float AEnemy::TakeDamage(float DamageAmount, FDamageEvent const& DamageEvent, AController* EventInstigator, AActor* DamageCauser)
{
	// Set the Target Blackboard Key to agro the Character
	if (EnemyController)
	{
		EnemyController->GetBlackboardComponent()->SetValueAsObject(
			FName("Target"),
			DamageCauser);
	}

	if (Health - DamageAmount <= 0.f)
	{
		Health = 0.f;
		Die();
	}
	else
	{
		Health -= DamageAmount;
	}

	return DamageAmount;
}

15-305 Enemy Death Montage

death

enemy

anim

void AEnemy::Die()
{
	if (bDying) return;

	bDying = true;

	HideHealthBar();

	UAnimInstance* AnimInstance = GetMesh()->GetAnimInstance();
	if (AnimInstance && DeathMontage)
	{
		AnimInstance->Montage_Play(DeathMontage);
	}

	if (EnemyController)
	{
		EnemyController->GetBlackboardComponent()->SetValueAsBool(
			FName("Dead"),
			true
		);
		EnemyController->StopMovement();
	}
}

option

sel

15-306 Destroy Enemy

end

node

15-307 Polish Up Enemy Death

void AEnemy::BulletHit_Implementation(FHitResult HitResult)
{
	if (ImpactSound)
	{
		UGameplayStatics::PlaySoundAtLocation(this, ImpactSound, GetActorLocation());
	}
	if (ImpactParticles)
	{
		UGameplayStatics::SpawnEmitterAtLocation(GetWorld(), ImpactParticles, HitResult.Location, FRotator(0.f), true);
	}

	// 죽었는데 스턴되는것 방지
	if (bDying) return;

	ShowHealthBar();	

	// Determine whether bullet hit stuns
	const float Stunned = FMath::FRandRange(0.f, 1.f);
	if (Stunned <= StunChance)
	{
		// Stun the Enemy
		PlayHitMontage(FName("HitReactFront"));
		SetStunned(true);
	}
}
// 몬스터가 죽고나서 잠시뒤 사라진다
void AEnemy::FinishDeath()
{
	GetMesh()->bPauseAnims = true;

	GetWorldTimerManager().SetTimer(
		DeathTimer,
		this,
		&AEnemy::DestroyEnemy,
		DeathTime
	);
}

15-308 Character Death

mon

set

setthis

node

finish

void AShooterCharacter::Die()
{
	UAnimInstance* AnimInstance = GetMesh()->GetAnimInstance();
	if (AnimInstance && DeathMontage)
	{
		AnimInstance->Montage_Play(DeathMontage);
	}
}
void AShooterCharacter::FinishDeath()
{
	GetMesh()->bPauseAnims = true;
	APlayerController* PC = UGameplayStatics::GetPlayerController(this, 0);
	if (PC)
	{
		DisableInput(PC);
	}
}

end

dup

remove

frame

wow

15-309 Stop Enemy Attack on Death

dead

check

float AShooterCharacter::TakeDamage(float DamageAmount, FDamageEvent const& DamageEvent, AController* EventInstigator, AActor* DamageCauser)
{
	if (Health - DamageAmount <= 0.f)
	{
		Health = 0.f;
		Die();

		auto EnemyController = Cast<AEnemyController>(EventInstigator);
		if (EnemyController)
		{
			EnemyController->GetBlackboardComponent()->SetValueAsBool(
				FName(TEXT("CharacterDead")),
				true
			);
		}
	}
	else
	{
		Health -= DamageAmount;
	}
	return DamageAmount;
}

15-310 Retargeting New Montages

health

dupin

twin

15-311 New Type of Enemies

class SHOOTER_API AEnemy : public ACharacter, public IBulletHitInterface
{
	//.. 

	// EditAnywhere 으로 설정해 Blueprint에서 컨트롤 가능하게 하자
	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Combat, meta = (AllowPrivateAccess = "true"))
	float Health;

	//..
}

dupbp

change

del

new

chief

value

15-312 Showcasing Different Grux Enemies

walk

draw

checkthis


Chapter 16 Khaimera

16-313 Adding Khaimera

grux

mesh

rename

soc

bp

16-314 Khaimera Animation Blueprint

connect

blend

space

loco

mon

melee

group

good

attack

16-315 Khaimera Attack Montage Notifies

ac

trail

this

16-316 Khaimera Death and Hit Montages

tage

kahi

death

what

save

react

16-317 Khaimera Anim Notifies

nodeone

nodetwo

16-318 Finishing Khaimera AnimBP

mon

time

remove

next

16-319 Different Khaimera Skins

dam

16-320 Explosive Get Overlapping Actors

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

	ExplosiveMesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("ExplosiveMesh"));
	SetRootComponent(ExplosiveMesh);

	OverlapSphere = CreateDefaultSubobject<USphereComponent>(TEXT("OverlapSphere"));
	OverlapSphere->SetupAttachment(GetRootComponent());
}
void AExplosive::BulletHit_Implementation(FHitResult HitResult)
{
	//..

	// Apply explosive damage
	TArray<AActor*> OverlappingActors;
	GetOverlappingActors(OverlappingActors, ACharacter::StaticClass());

	for (auto Actor : OverlappingActors)
	{
		UE_LOG(LogTemp, Warning, TEXT("Actor damaged by explosive: %s"), *Actor->GetName());
	}

	Destroy();

}

code

delete

ex

bar

16-321 Explosive Agro Enemy

// Shooter, ShooterController 매개 변수가 추가되었다
void AExplosive::BulletHit_Implementation(FHitResult HitResult, AActor* Shooter, AController* ShooterController)
{
	//..

	// Apply explosive damage
	TArray<AActor*> OverlappingActors;
	GetOverlappingActors(OverlappingActors, ACharacter::StaticClass());

	for (auto Actor : OverlappingActors)
	{
		UE_LOG(LogTemp, Warning, TEXT("Actor damaged by explosive: %s"), *Actor->GetName());

		UGameplayStatics::ApplyDamage(
			Actor,
			Damage,
			ShooterController,
			Shooter,
			UDamageType::StaticClass()
		);
	}

	Destroy();
}

16-322 Health Pickup

blueprint

mesh

sphere

add

amount

option


Chapter 17 Level Creation and Finishing the Game!

17-323 Level Prototyping Tools

start

file

proto

17-324 Level Prototype - Starting Area

add

17-325 Level Prototype - Courtyard

group

17-326 Level Prototype - Courtyard2

mig

17-327 Level Prototype - Using Paragon Props

mesh

17-328 Ruins Assets - Floor

check

blank

17-329 Ruins Assets - Walls

  • make walls

17-330 Ruins Assets - Walls 2

  • make walls

17-331 Ruins Assets - Corners

brick

17-332 Ruins Assets - Levels

  • make levels

17-333 Ruins Assets - Levels 2

work

17-334 Ruins Assets - Levels 3

regroup

mat

17-335 Ruins Assets - Arches

  • make arches

17-336 Ruins Assets - Arches2

  • make arches2

17-337 Ruins Assets - Large Stairs

  • 액션 모빌리티에 대한 설명은 (참고)를 확인하자

17-338 Ruins Assets - Boss Room

  • make boss room

17-339 Ruins Assets - Finishing Up the Level

mesh

two

17-340 Level Design - Enemies

nav

value

sim

radi

17-341 Polishing Gameplay

class SHOOTER_API AShooterCharacter : public ACharacter
{
	//..

	/** true when Character dies */
	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = Combat, meta = (AllowPrivateAccess = "true"))
	bool bDead;
}

chardead

bp

17-342 Polishing Gameplay 2

type

dup

rar

17-343 Prevent Attack when Enemy Dead

next

17-344 Sphere Reflection Capture

dark

light

  • LightmassImportanceVolume 은 라이트매스가 광자를 방출하는 영역을 제한시켜, 디테일한 간접광이 필요한 지역에 집중시킬 수 있습니다 (참고)

17-345 Post Process Effects

nav

black

infinit

bloom

manual

camera

chromatic

  • 색수차(Chromatic Aberration)는 광선이 렌즈의 여러 지점으로 들어가면서 RGB 컬러의 분리를 유발하는 현상입니다 (참고)

effect

intense

shadow

high

oc

  • Ambient Occlusion은 빛의 차폐로 인한 감쇠 근사치를 구하는 이펙트입니다 현실에서도 방의 구석 부분은 훨씬 더 어둡듯이, 구석이나 틈 같은 곳을 더 어둡게 하여 더욱 자연스럽고 사실적인 느낌을 낼 수 있도록, 표준 글로벌 일루미네이션에 더해 미묘한 이펙트로 사용하는 것이 보통 가장 좋습니다 (참고)

per

17-346 ESC to Quit

quit

17-347 Finishing the Game

phy


맨 위로 이동하기

댓글남기기