Unreal TPS 캐릭터와 AI 만들기

민트맛치킨·2025년 5월 2일

Unreal

목록 보기
7/26

AI를 만들고 KillCount를 UI에 표시

MainHud.h

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"
#include "Blueprint/UserWidget.h"
#include "MainHud.generated.h"

/**
 * 
 */
UCLASS()
class BASIS_API UMainHud : public UUserWidget
{
	GENERATED_BODY()
	
public:
	UPROPERTY(meta = (BindWidget))
		TObjectPtr<class UProgressBar> HPGauge;

	UPROPERTY(meta = (BindWidget))
		TObjectPtr<class UTextBlock> HPPercent;

	UPROPERTY(meta = (BindWidget))
		TObjectPtr<class UTextBlock> KC;

	UPROPERTY(meta = (BindWidget))
		TObjectPtr<class UTextBlock> SC;

	UFUNCTION()
		void SetHP(float Value);

	UFUNCTION()
		void SetKillCount(int32 Value);

	UFUNCTION()
		void SetStageCount(int32 Value);
};

MainHud.cpp

// Fill out your copyright notice in the Description page of Project Settings.
#include "MainHud.h"
#include "Components/ProgressBar.h"
#include "Components/TextBlock.h"

void UMainHud::SetHP(float Value)
{
	if (IsValid(HPGauge))
	{
		HPGauge->SetPercent(Value);
	}
	if (IsValid(HPPercent))
	{
		int32 Hp = Value * 100;
		FText Text = FText::FromString(FString::Printf(TEXT("%d"), Hp));
		HPPercent->SetText(Text);
	}
}

void UMainHud::SetKillCount(int32 Value)
{
	if (IsValid(KC))
	{
		FText Text = FText::FromString(FString::Printf(TEXT("KillCount: %d"), Value));
		KC->SetText(Text);
	}
}

void UMainHud::SetStageCount(int32 Value)
{
	if (IsValid(SC))
	{
		FText Text = FText::FromString(FString::Printf(TEXT("StageCount: %d"), Value));
		SC->SetText(Text);
	}
}

BTT_Attack.h

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"
#include "BehaviorTree/BTTaskNode.h"
#include "BTT_Attack.generated.h"

UCLASS()
class BASIS_API UBTT_Attack : public UBTTaskNode
{
	GENERATED_BODY()

public:
	UBTT_Attack();
	virtual EBTNodeResult::Type ExecuteTask(UBehaviorTreeComponent& BTC, uint8* NodeMemory) override;

	UPROPERTY(EditAnywhere)
		float AttackRange;

	UPROPERTY(EditAnywhere)
		float SightRange;

	UPROPERTY(EditAnywhere)
		float SightAngle;

	
};

BTT_Attack.cpp

// Fill out your copyright notice in the Description page of Project Settings.


#include "BTT_Attack.h"
#include "CharacterBase.h"
#include "PlayerBase.h"
#include "BehaviorTree/BlackboardComponent.h"
#include "AIController.h"
#include "Kismet/KismetMathLibrary.h"


UBTT_Attack::UBTT_Attack()
{
	NodeName = TEXT("Attack");
}

EBTNodeResult::Type UBTT_Attack::ExecuteTask(UBehaviorTreeComponent& BTC, uint8* NodeMemory)
{
	AAIController* Controller = Cast<AAIController>(BTC.GetOwner());
	if (!IsValid(Controller))
	{
		return EBTNodeResult::Failed;
	}

	ACharacterBase* CB = Cast<ACharacterBase>(Controller->GetPawn());
	if (!IsValid(CB))
	{
		return EBTNodeResult::Failed;
	}
	UWorld* World = GetWorld();
	if (!IsValid(World)) // 월드가 존재하는지 확인
	{
		return EBTNodeResult::Failed;
	}
	APlayerController* PC = World->GetFirstPlayerController(); // 플레이어 컨트롤러 가져오기
	if (!IsValid(PC)) 
	{
		return EBTNodeResult::Failed;
	}
	APlayerBase* PB = Cast<APlayerBase>(PC->GetPawn()); // 폰 가져오기(적)
	if (!IsValid(PB))
	{
		return EBTNodeResult::Failed;
	}
	/*
	공격 기준
	1. 시야 내에 존재
	2. 적을 볼때 가림막 x
	3. 공격범위 내여야 한다
	*/



	// 1. 시야 내에 존재
	FVector Distance = PB->GetActorLocation() - CB->GetActorLocation(); // 플레이어와 AI의 거리
	if (Distance.Length() > SightRange) // 거리가 시야거리보다 클 경우 fail
	{
		return EBTNodeResult::Failed;
	}

	FVector AIForward = CB->GetActorForwardVector(); // AI 자기자신의 Forward
	Distance.Normalize();
	AIForward.Normalize();
	float DotResult = AIForward.Dot(Distance); // Distance와 AIForward를 내적
	float AngleBetweenVector = FMath::Acos(DotResult); // 내적으로 구한 cos를 arccos로 변환하여 각도 계산

	if (SightAngle < AngleBetweenVector) // 시야 각도 안에 들어오는지
	{
		return EBTNodeResult::Failed;
	}


	// 2. 적을 볼때 가림막 x ( Line Trace 활용 <- 광선을 발사하여 부딪힌 정보를 가져옴 )
	FCollisionQueryParams QueryParams;
	QueryParams.AddIgnoredActor(CB);

	FHitResult HitResult;
	World->LineTraceSingleByChannel(HitResult, CB->GetActorLocation(), PB->GetActorLocation(), ECollisionChannel::ECC_Camera, QueryParams);
	// 인자( 결과값, 시작점, 플레이어 위치, 특정물체는 부딪히지 않게, 쿼리파라미터 )

	if (!HitResult.bBlockingHit) // 광선이 조건에 만족하는 물체에 부딪히지 않으면 실패
	{
		return EBTNodeResult::Failed;
	}
	if (HitResult.GetActor() != PB) // 광선이 플레이어에게 부딪히지 않으면 실패
	{
		return EBTNodeResult::Failed;
	}
	UBlackboardComponent* BBC = BTC.GetBlackboardComponent(); // 블랙보드 컴포넌트 가져오기
	if (!IsValid(BBC)) // 블랙보드가 없으면 실패
	{
		return EBTNodeResult::Failed;
	}
	BBC->SetValueAsVector(TEXT("TargetPosition"), PB->GetActorLocation()); // 블랙보드에 플레이어(적) 위치를 TargetPosition으로 등록


	// 3. 공격범위 내여야 한다
	Distance = PB->GetActorLocation() - CB->GetActorLocation();
	if (Distance.Length() > AttackRange)
	{
		return EBTNodeResult::Failed;
	}


	// 공격기준이 충족되면 적을 향해 바라보기
	BTC.GetAIOwner()->StopMovement();
	FRotator Rot = UKismetMathLibrary::FindLookAtRotation(CB->GetActorLocation(), PB->GetActorLocation()); // FindLookAtRotation A위치에서 B위치로의 각도를 구함

	CB->SetActorRotation(Rot);
	CB->Attack();
	return EBTNodeResult::Succeeded;
}

BTT_Move.h

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"
#include "BehaviorTree/BTTaskNode.h"
#include "BTT_Move.generated.h"

/**
 * 
 */
UCLASS()
class BASIS_API UBTT_Move : public UBTTaskNode
{
	GENERATED_BODY()
	
public:
	UBTT_Move();
	virtual EBTNodeResult::Type ExecuteTask(UBehaviorTreeComponent& BTC, uint8* NodeMemory) override;
};

BTT_Move.cpp

// Fill out your copyright notice in the Description page of Project Settings.


#include "BTT_Move.h"
#include "BehaviorTree/BlackboardComponent.h"
#include "AIController.h"
#include <NavigationSystem.h>
#include "Engine/OverlapResult.h"

UBTT_Move::UBTT_Move()
{
	NodeName = TEXT("Move");
}

EBTNodeResult::Type UBTT_Move::ExecuteTask(UBehaviorTreeComponent& BTC, uint8* NodeMemory)
{

	UBlackboardComponent* BBC = BTC.GetBlackboardComponent();
	if (!IsValid(BBC))
	{
		return EBTNodeResult::Failed;
	}
	FVector TargetPosition = BBC->GetValueAsVector(TEXT("TargetPosition"));

	AAIController* AIC = BTC.GetAIOwner(); // AI 컨트롤러 가져오기
	if (!IsValid(AIC))
	{
		return EBTNodeResult::Failed;
	}
	APawn* Pawn = AIC->GetPawn(); // 폰 가져오기
	if (!IsValid(Pawn))
	{
		return EBTNodeResult::Failed;
	}

	// Heal 팩 먹기
	if (IsValid(GetWorld()))
	{
		TArray<FOverlapResult> result;
		GetWorld()->OverlapMultiByChannel(result, Pawn->GetActorLocation(), FQuat::Identity, ECollisionChannel::ECC_Camera, FCollisionShape::MakeSphere(1000)); 
		// 오버랩으로 물체확인, FQuat::Identity는 기본 각도

		for (int i = 0; i < result.Num(); i++)
		{
			AActor* Actor = result[i].GetActor();
			if (!IsValid(Actor))
			{
				continue;
			}
			if (Actor->GetActorLabel().Equals(TEXT("HealActor")))
			{
				BBC->SetValueAsVector(TEXT("TargetPosition"), Actor->GetActorLocation()); // 블랙보드 TargetPosition에 HealActor위치로 설정
				AIC->MoveToLocation(TargetPosition); // TargetPosition으로 이동
				return EBTNodeResult::Succeeded;
			}
		}
	}

	if ((TargetPosition - Pawn->GetActorLocation()).Length() < 100) // TargetPosition과 폰의 거리차가 1미터보다 작으면 랜덤으로 움직이기
	{
		UNavigationSystemV1* NavSystem = UNavigationSystemV1::GetNavigationSystem(GetWorld());

		FNavLocation LOC;
		NavSystem->GetRandomPoint(LOC); // 랜덤한 포인트
		BBC->SetValueAsVector(TEXT("TargetPosition"), LOC.Location);
	}
	AIC->MoveToLocation(TargetPosition);
	return EBTNodeResult::Succeeded;
}

AIControllerBase.h

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"
#include "AIController.h"
#include "AIControllerBase.generated.h"

/**
 * 
 */
UCLASS()
class BASIS_API AAIControllerBase : public AAIController
{
	GENERATED_BODY()
public:
	virtual void OnPossess(APawn* InPawn) override;

public:
	UPROPERTY(EditAnywhere)
		class UBehaviorTree* BT;
	
};

AIControllerBase.cpp

// Fill out your copyright notice in the Description page of Project Settings.


#include "AIControllerBase.h"
#include "BehaviorTree/BehaviorTreeComponent.h"

void AAIControllerBase::OnPossess(APawn* InPawn)
{
	Super::OnPossess(InPawn);
	if (BT == nullptr)
	{
		return;
	}
	RunBehaviorTree(BT);
}

StartUI.h

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"
#include "Blueprint/UserWidget.h"
#include "StartUI.generated.h"

UCLASS()
class BASIS_API UStartUI : public UUserWidget
{
	GENERATED_BODY()
	
public:
	virtual void NativeOnInitialized() override;

	UPROPERTY(meta = (BindWidget))
		TObjectPtr<class UButton> GameStart;

	UPROPERTY(meta = (BindWidget))
		TObjectPtr<class UButton> Exit;

	UFUNCTION(BlueprintCallable)
		void OnGameStart();

	UFUNCTION(BlueprintCallable)
		void OnExit();
};

StartUI.cpp

// Fill out your copyright notice in the Description page of Project Settings.


#include "StartUI.h"
#include "Kismet/KismetSystemLibrary.h"
#include <Kismet/GameplayStatics.h>
#include "Components/Button.h"

void UStartUI::NativeOnInitialized()
{
	Super::NativeOnInitialized();

	if (!IsValid(GetWorld()))
	{
		return;
	}
	if (!IsValid(GetWorld()->GetFirstPlayerController()))
	{
		return;
	}
	GetWorld()->GetFirstPlayerController()->SetShowMouseCursor(true);

	if (IsValid(GameStart))
	{
		GameStart->OnClicked.AddDynamic(this, &UStartUI::OnGameStart);
	}

	if (IsValid(Exit))
	{
		Exit->OnClicked.AddDynamic(this, &UStartUI::OnExit);
	}

}

void UStartUI::OnGameStart()
{
	UGameplayStatics::OpenLevel(this, TEXT("MainLevel2")); // OpenLevel함수로 MainLevel 열기

}

void UStartUI::OnExit()
{
	// 인자 : 끝내는 주체, 특정한 플레이어 대상, 나가는 방법, 제약조건
	UKismetSystemLibrary::QuitGame(this, nullptr, EQuitPreference::Quit, false);

}

MainGameMode.h

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/GameMode.h"
#include "MainGameMode.generated.h"

UCLASS()
class BASIS_API AMainGameMode : public AGameMode
{
	GENERATED_BODY()

public:

	virtual void BeginPlay() override;
	virtual void Tick(float DeltaTime) override;

	void ChangeToEnd();

	UPROPERTY(EditAnywhere)
		TSubclassOf<class UUserWidget> WidgetClass; // TSubclassOf는 특정한 클래스의 하위클래스들 구조 -> WidgetClass

	UPROPERTY()
		TObjectPtr<class UUserWidget> Widget; // 클래스의 생성체 -> Widget

	UPROPERTY(EditAnywhere)
		TSubclassOf<class UUserWidget> EndWidgetClass; // TSubclassOf는 특정한 클래스의 하위클래스들 구조 -> WidgetClass

	UPROPERTY()
		TObjectPtr<class UUserWidget> EndWidget;

	UPROPERTY(EditAnywhere)
		TSubclassOf<AActor> Enemy;

	int32 Time;
	int32 Stage;
	/* KillCount와 HP는 플레이어가 가지고 있는 내용, 가져오는 방법
	1. 플레이어의 KillCount와 HP가 갱신될 때 마다 여기에 갱신
	2. 매 Tick마다 갱신
	*/
	int32 KillCount;
	float HP;
};

MainGameMode.cpp

// Fill out your copyright notice in the Description page of Project Settings.


#include "MainGameMode.h"
#include "Blueprint/UserWidget.h"
#include "PlayerBase.h"
#include "MainHud.h"
#include <NavigationSystem.h>

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

	if (WidgetClass != nullptr)
	{
		Widget = CreateWidget<UUserWidget>(GetWorld(), WidgetClass); // WidgetClass에 해당하는 것을 GetWorld에 생성 -> Widget
	}
	if (EndWidgetClass != nullptr)
	{
		EndWidget = CreateWidget<UUserWidget>(GetWorld(), EndWidgetClass);
	}

	if (IsValid(Widget))
	{
		Widget->AddToViewport();
	}
	if (!IsValid(GetWorld()))
	{
		return;
	}
	if (!IsValid(GetWorld()->GetFirstPlayerController()))
	{
		return;
	}
	GetWorld()->GetFirstPlayerController()->SetShowMouseCursor(false);
	Time = 0;
	Stage = 0;
	// Widget->RemoveFromViewport; 위젯삭제할 경우
}

void AMainGameMode::Tick(float DeltaTime)
{
	Time += DeltaTime;
	UWorld* World = GetWorld();

	if (!IsValid(World))
	{
		return;
	}
	if (!IsValid(Enemy))
	{
		return;
	}

	if ((int)(Time / 3 + 1) != Stage)
	{
		Stage++;

		for (int i = 0; i < Stage; i++)
		{
			UNavigationSystemV1* NavSystem = UNavigationSystemV1::GetNavigationSystem(GetWorld());

			FNavLocation LOC;
			NavSystem->GetRandomPoint(LOC); // 랜덤한 포인트

			FActorSpawnParameters param;
			param.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn;

			World->SpawnActor<AActor>(Enemy, LOC.Location, FRotator::ZeroRotator, param);
		}
	}

	APlayerController* PC = World->GetFirstPlayerController();
	if (!IsValid(PC))
	{
		return;
	}
	APlayerBase* PB = Cast<APlayerBase>(PC->GetPawn());

	if (!IsValid(PB))
	{
		return;
	}

	HP = (float)(PB->CurrentHP) / PB->FullHP;
	KillCount = PB->KillCount;

	UMainHud* Hud = Cast<UMainHud>(Widget);
	if (!IsValid(Hud))
	{
		return;
	}
	Hud->SetHP(HP);
	Hud->SetKillCount(KillCount);
	Hud->SetStageCount(Stage);

}

void AMainGameMode::ChangeToEnd()
{
	if (!IsValid(GetWorld()))
	{
		return;
	}
	if (!IsValid(GetWorld()->GetFirstPlayerController()))
	{
		return;
	}
	GetWorld()->GetFirstPlayerController()->SetShowMouseCursor(true);
	Widget->RemoveFromParent();
	EndWidget->AddToViewport();
}

AIPlayerBase.h

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"
#include "CharacterBase.h"
#include "InputMappingContext.h"
#include "AIPlayerBase.generated.h"

class AWeapon;
UCLASS()
class BASIS_API AAIPlayerBase : public ACharacterBase
{
	GENERATED_BODY()

public:
	virtual void BeginPlay() override;
	virtual void Hit(int32 Damage, AActor* ByWho) override;
	virtual void Attack() override;

	UPROPERTY(EditAnywhere)
		TSubclassOf<AWeapon> Weapon; // 액터의 클래스 부분
	UPROPERTY()
		TObjectPtr<AWeapon> WeaponActor; // 실제 스폰되는 액터

	void Move(const FInputActionValue& Value);
};

AIPlayerBase.cpp

// Fill out your copyright notice in the Description page of Project Settings.


#include "AIPlayerBase.h"
#include "Weapon.h"
#include "EnhancedInputSubsystems.h"

void AAIPlayerBase::BeginPlay()
{
	Super::BeginPlay();
	WeaponActor = GetWorld()->SpawnActor<AWeapon>(Weapon); // 세계를 가져오고 AWeapon의 하위 클래스 중 Weapon을 SpawnActor
	if (IsValid(WeaponActor))
	{
		FAttachmentTransformRules TransformRules(EAttachmentRule::SnapToTarget, true); // SnapToTarget으로 붙이기
		WeaponActor->AttachToComponent(GetMesh(), TransformRules, TEXT("WeaponSocket")); // 메시를 가져오고 WeaponSocket이란 이름으로 붙임
		WeaponActor->SetOwner(this); // 소유자는 자기자신 플레이어
	}
}

void AAIPlayerBase::Hit(int32 Damage, AActor* ByWho)

{
	Super::Hit(Damage, ByWho);

	if (CurrentHP > 0) {
		return;
	}
	if (IsValid(WeaponActor))
	{
		WeaponActor->Destroy();
	}

	Destroy();
}

void AAIPlayerBase::Attack()
{
	Super::Attack();

	if (IsValid(WeaponActor))
	{
		WeaponActor->Fire();
	}
}

void AAIPlayerBase::Move(const FInputActionValue& Value)
{
	FVector2D MovementVector = Value.Get<FVector2D>();

	if (Controller != nullptr)
	{
		AddMovementInput(GetActorForwardVector(), MovementVector.Y);
		AddMovementInput(GetActorRightVector(), MovementVector.X);
	}
}

플레이 영상

0개의 댓글