rpg같은 게임에는 수많은 적, ai가 존재합니다
만약 AI한마리한마리 직접 코딩한다면 코드가 어마어마하게 많아지고 유지보수가 힘듭니다
Hash알고리즘을 사용하여 수많은 적, 데이터 를 관리할수있습니다
그래서 추가, 및 삭제 즉 유지보수가 편한 구조가 필요합니다
제가 선택한 방식은 데이터테이블의 Key : 행 Value : 값
을 사용한 Hash알고리즘으로 구현하였습니다
데이터테이블에서 값을 설정하고
Base가되는 Enemy에서 데이터테이블의 값을 인식한뒤
그값으로 동기화를 해주는방식으로 구현하였습니다
데이터테이블의 값이 될 구조체를 선언해주었습니다
#pragma once
#include "CoreMinimal.h"
#include "UObject/Interface.h"
#include "EnemyDataTable.generated.h"
USTRUCT(BlueprintType)
struct FEnemyDataTable : public FTableRowBase
{
GENERATED_BODY()
public:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "BaseData")
int32 enemyId;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "BaseData")
FName name;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "BaseData")
int32 teamIdNumber;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "BaseData")
USkeletalMesh* skeletalMesh;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "BaseData")
TSubclassOf<UAnimInstance> animBP;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "BaseData")
int32 maxHP;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "BaseData")
float maxPosture;
};
해당 구조체로 데이터테이블을 만들어 EnemyId를 1
그리고 사용할 변수값 및 스켈레탈메시나 애니메이션 블루프린트를 설정해주었습니다

애니메이션 블루프린트는 캐릭터의 speed와 방향을 구하는 공식을 사용하였습니다
UCLASS()
class PORTFOLIOMS_API UEnemyBaseAnimInstance : public UAnimInstance
{
GENERATED_BODY()
public:
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Movement")
float speed;
// 이동 방향
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Movement")
float direction;
virtual void NativeUpdateAnimation(float DeltaSeconds) override;
};
#include "EnemyBaseAnimInstance.h"
#include "Kismet/KismetMathLibrary.h"
#include "KismetAnimationLibrary.h"
void UEnemyBaseAnimInstance::NativeUpdateAnimation(float DeltaSeconds)
{
APawn* owningPawn = TryGetPawnOwner();
if (!owningPawn) return;
FVector velocity = owningPawn->GetVelocity();
velocity.Z = 0.f; // 점프 높이 같은 Z값 제외
// 속력 크기
speed = velocity.Size();
FVector forwardVec = owningPawn->GetActorForwardVector();
FVector rightVec = owningPawn->GetActorRightVector();
velocity.Normalize();
float forwardDot = FVector::DotProduct(forwardVec, velocity);
float rightDot = FVector::DotProduct(rightVec, velocity);
float angle = FMath::RadiansToDegrees(FMath::Acos(forwardDot));
if (rightDot < 0)
{
angle *= -1.f;
}
//-180 ~ 180
direction = angle;
}
해당 애님인스턴스에 StateMachine내에서 구한 변수값으로 블렌드스페이스를 연동해주었습니다

캐릭터에서 uproperty로 선언해서 할당해줄 EnemyDataTable을 선언해주고
키값을 분류할 enemyId를 선언해줍니다
virtual void OnConstruction(const FTransform& Transform) override;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "EnemyData")
UDataTable* enemyDataTable;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "EnemyData")
int32 enemyId;
적의 베이스가 될 baseCharacter c++ 캐릭터를 만들어주고 OnConstruction함수로 데이터테이블의
키값이되는 id를 찾아 할당해줍니다
UPROPERTY에 할당해줄 ID넘버와 데이터테이블을 할당해줍니다 해당 적 블루프린트의 ID는 1번

행을 추가한뒤 EnemyId를 1번으로 설정

OnConstruction은 언리얼 엔진에서 Actor가 생성될 때 호출되는 함수 입니다.
해당 함수를 사용하여 데이터테이블에 있는 스켈레탈메시, 애님블루프린트를 바꾸는 구조를 설계하였습니다
void AEnemyBaseCharacter::OnConstruction(const FTransform& Transform)
{
Super::OnConstruction(Transform);
if (!enemyDataTable) return;
FName rowName = FName(*FString::FromInt(enemyId));
TArray<FEnemyDataTable*> enemyDataTableAllRows;
enemyDataTable->GetAllRows<FEnemyDataTable>(TEXT("OnConstruction"), enemyDataTableAllRows);
for (FEnemyDataTable* row : enemyDataTableAllRows)
{
if (!row) return;
if (row->enemyId == enemyId)
{
GetMesh()->SetSkeletalMesh(row->skeletalMesh);
if (row->animBP)
{
GetMesh()->SetAnimInstanceClass(row->animBP);
}
}
break;
}
}
ID를 1번으로 할당한 BaseCharacter 를 종속받은 블루프린트는 꺼내면 데이터테이블에 할당된 캐릭터
스켈레탈메시와 블루프린트로 설정이 됩니다

또한 무기마다 사용할 소켓을 만들어준뒤 위치를 잡아줍니다

또한 무기를 담당할 데이터테이블또한 만들어주었습니다
#pragma once
#include "CoreMinimal.h"
#include "UObject/Interface.h"
#include "WeaponDataTable.generated.h"
USTRUCT(BlueprintType)
struct FWeaponDataTable : public FTableRowBase
{
GENERATED_BODY()
public:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "BaseData")
int32 weaponId;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "BaseData")
FName name;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "BaseData")
TSubclassOf<AActor> weaponBP;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "BaseData")
FString attachSocket;
};
Onconstruction함수에 무기 데이터테이블에있는 원하는 BP를 원하는 소켓 위치에 Attach되도록 설계하였습니다
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "EnemyData")
int32 weaponId;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Weapon")
UChildActorComponent* swordActorComp = nullptr;
//weapon data
TArray<FWeaponDataTable*> weaponDataTableAllRow;
weaponDataTable->GetAllRows<FWeaponDataTable>(TEXT("OnConstruction"), weaponDataTableAllRow);
for (FWeaponDataTable* row : weaponDataTableAllRow)
{
if (!row) return;
if (row->weaponId == weaponId)
{
if (!row->weaponBP) break;
// 기존 ChildActorComponent가 있으면 제거
if (swordActorComp)
{
swordActorComp->DestroyComponent();
swordActorComp = nullptr;
}
swordActorComp = NewObject<UChildActorComponent>(this, UChildActorComponent::StaticClass(),
TEXT("SwordActorComp"));
swordActorComp->RegisterComponent();
swordActorComp->SetChildActorClass(row->weaponBP);
// attachSocket 위치에 Attach
FName socketName(*row->attachSocket);
swordActorComp->AttachToComponent(GetMesh(), FAttachmentTransformRules::SnapToTargetIncludingScale, socketName);
break;
}
}

무기를 들고있을때와 안들고있을때의 블렌드스페이스를 별도로 만들어 스테이트머신에서 변수로 제어했습니다

각 블렌드스페이스에는 이렇게 별도의 8방향 블렌드스페이스가 들어있습니다

결과물
1번의 캐릭터에셋과 1번의 무기 에셋을 적용해주겠습니다
1번의 캐릭터에셋입니다

1번의 무기 에셋입니다

캐릭터 uproperty에서의 enemyId와 WeaponId를 데이터테이블에 존재하는 원하는 id 숫자로 입력해줍니다

데이터테이블에서 설정한 값들이 적한테 바로 적용되는것을 확인할수있습니다

'Unreal 프로젝트 다이어리 > 두번째 프로젝트' 카테고리의 다른 글
| Unreal - Posture Progress / Hit Direction (0) | 2025.08.29 |
|---|---|
| Unreal - HitInterface 상호작용 (0) | 2025.08.23 |
| Unreal - 콤보 공격 (0) | 2025.08.22 |
| Unreal - 무기 Draw (0) | 2025.08.19 |
| Unreal - Layered Per Bone (상 하체 애니메이션 분리) (0) | 2025.08.17 |