좋은 코드, 나쁜 코드
뒤엉킨 변경(Divergent Change)
- 한 모듈이 여러 이유로 자주 수정되어야하면 복잡해짐
- 다른 맥락의 동작은 다른 모듈로 분리
- 필요에 따라 단계를 나누고 클래스를 쪼개서 만들기
- 좋은 예시: 플레이어 데이터는 PlayerDataManager 클래스에, 게임 플레이는 GamePlayManager 클래스에
샷건 수술
- 작은 변경을 위해 여러 곳을 동시에 수정하는 일은 피하자.
- 관련된 것을 한 군데로 모아 수정 범위를 좁힌다.
- 좋은 예시: DamageSystem에서 모든 데미지 로직을 처리한다.
기능 편애
- 어떤 함수가 자기 객체보다 남의 객체 기능이나 데이터를 더 많이 쓰지 않도록 하자.
- 남의 객체에서 처리된 데이터만 가지고 오자.
- 좋은 예시: 데미지 감소량을 계산하는데 캐릭터의 데이터가 많이 필요한 경우, 캐릭터 클래스에 함수를 만들고 값만 반환받는다.
트레이스
트레이스 종류
- 하나만 인식하는 트레이스
- 여러명 인식하는 트레이스
- 반사각을 재는 트레이스
- 비동기 트레이스
1️⃣ 하나만 인식하는 트레이스
👉 Single Trace
- 가장 먼저 맞은 객체 하나만 반환
- 함수 예:
- LineTraceSingleByChannel
- SweepSingleByChannel
✔️ 특징
- 총알, 레이저처럼 “처음 맞은 것만 중요할 때” 사용
- 성능 가벼움
2️⃣ 여러 개 인식하는 트레이스
👉 Multi Trace
- 경로 상의 모든 충돌 객체 반환
- 함수 예:
- LineTraceMultiByChannel
- SweepMultiByChannel
✔️ 특징
- 관통 공격, 범위 감지, 다중 타겟 처리
- 결과가 배열로 나옴
3️⃣ 반사각을 재는 트레이스
👉 공식 이름은 따로 “반사 트레이스”가 있는 건 아니고
트레이스 + 수학 계산으로 처리함
- 사용 개념:
- Hit 결과의 Impact Normal
- 벡터 반사 공식:
-
Reflection = Direction - 2 * Dot(Direction, Normal) * Normal
✔️ 용도
- 레이저 반사
- 총알 튕김
- 빛 반사 시뮬레이션
4️⃣ 비동기 트레이스
👉 Async Trace
- 게임 스레드를 멈추지 않고 백그라운드에서 트레이스 실행
- 예:
- AsyncLineTraceByChannel
✔️ 특징
- 성능 최적화에 중요
- 결과는 콜백 / 핸들로 나중에 받음
- 대량 트레이스(예: AI 시야 계산)에 유리
추가로 알아두면 좋은 트레이스 종류
🔹 Shape Trace (Sweep)
- 선(Line)이 아니라 형태(Box, Sphere, Capsule)로 검사
- 예:
- SweepSingleByChannel
- SphereTraceMulti
👉 캐릭터 충돌, 근접 공격 등에 자주 씀
🔹 Channel / Object 타입
- ByChannel: 특정 충돌 채널 기준
- ByObjectType: 특정 오브젝트 타입만 검사
한 줄 정리
- Single → 하나만
- Multi → 여러 개
- Reflection → 노멀로 계산
- Async → 성능용 비동기 처리
콜리전










기본값은 해당 메시에서 설정된 값이고, 외부에서 바꿔서 적용 가능하다.







// Fill out your copyright notice in the Description page of Project Settings.
// 트레이스용 액터
#include "SpartaActor.h"
#include "Kismet/KismetSystemLibrary.h"
// Sets default values
ASpartaActor::ASpartaActor()
{
// 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;
}
// Called when the game starts or when spawned
void ASpartaActor::BeginPlay()
{
Super::BeginPlay();
}
// Called every frame
void ASpartaActor::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
StartSingleTrace();
}
void ASpartaActor::StartSingleTrace()
{
TArray<AActor*> ActorsToIgnore;
ActorsToIgnore.Add(this);
FHitResult HitResult;
UKismetSystemLibrary::LineTraceSingle(
GetWorld(), // 월드 지정
GetActorLocation(), // 트레이스 시작 지점
GetActorForwardVector() * 1000.f + GetActorLocation(), // 트레이스 범위
UEngineTypes::ConvertToTraceType(ECC_Visibility), // 트레이스 채널
true, // true면 복합 콜리전, false면 단순 콜리전 사용
ActorsToIgnore, // TArray 형태로 ignore. 비워두면 자기자신도 검출
EDrawDebugTrace::ForOneFrame,
HitResult, // 충돌 결과 저장
true, // 자기자신을 무시할 것인지. 오류 방지를 위해 ActorsToIgnore에 자신을 넣어줬더라도 true로 해주는 것이 좋다.
FLinearColor::Red, // 기본 색상
FLinearColor::Green // 충돌 시 색상
);
}
LineTrace는 GetWorld와 Kismet 두 가지 라이브러리로 사용할 수 있다.
Kismet은 Trace를 바로 보이게할 수 있고 GetWorld는 따로 디버그 라이브러리를 사용해야한다.
Kismet보다 GetWorld가 빠르긴 하지만 미미한 차이이다. 부하를 줄이려면 처음에는 Kismet으로 개발하고 마지막에 GetWorld로 변환해준다.

블루프린트로 상속해주고 큐브를 붙여준다.
트리거박스를 하나 생성하고, 트레이스 앞에 놔준다.


싱글트레이스




싱글트레이스는 Overlap일 경우 인식하지 않고, Block만 인식하고 막힌다.
멀티트레이스
TArray<FHitResult> HitResult; // 멀티 트레이스일땐 FHitResult 배열 사용
TArray<AActor*> ActorsToIgnore;
ActorsToIgnore.Add(this);
UKismetSystemLibrary::LineTraceMulti(
GetWorld(),
GetActorLocation(),
GetActorForwardVector() * 1000.f + GetActorLocation(),
UEngineTypes::ConvertToTraceType(ECC_Visibility),
false,
ActorsToIgnore,
EDrawDebugTrace::ForOneFrame,
HitResult,
true,
FLinearColor::Red,
FLinearColor::Green
);




멀티트레이스의 경우 Overlap은 관통하고(인식은 함) Block은 막힌다.
비동기 트레이스
비동기 처리란 다른 스레드에 중간 작업물을 넘기고 처리되면 다시 돌려받는 것이다.
트리거 박스도 일종의 최적화가 잘 된 트레이스 다발이라고 볼 수있다.
트레이스가 무거워지면 연산이 느려질 수 있어서 비동기처리를 해준다.
// UWorld
void ASpartaActor::StartAsyncTrace()
{
FTraceDelegate TraceDelegate; // 호출할 함수 델리게이트 구조체. 함수 포인터 담음.
TraceDelegate.BindUObject(this, &ASpartaActor::OnAsyncTraceCompleted); // 현재 객체의 OnAsyncTraceCompleted 함수를 콜백으로 등록
FCollisionResponseParams ResponseParams; // 충돌 채널별 응답을 담는 구조체
ResponseParams.CollisionResponse.WorldDynamic = ECR_Block; 강제로 상호작용 타입을 바꿀 수 있음. WorldDynamic 타입 오브젝트에 대해 Overlap으로 처리
FCollisionQueryParams QueryParams; // 라인 트레이스(충돌 검사) 설정을 할 때 쓰는 구조체 설정
QueryParams.AddIgnoredActor(this); // 현재 액터 제외
QueryParams.bTraceComplex = false; // 복합 콜리전 검사 여부
GetWorld()->AsyncLineTraceByChannel(
EAsyncTraceType::Multi, // Text는 누군지가 아니라 맞았는지 안 맞았는지만 판단. 가벼움.
GetActorLocation(),
GetActorForwardVector() * 1000.f + GetActorLocation(),
ECC_Visibility,
QueryParams,
ResponseParams,
&TraceDelegate
);
}
void ASpartaActor::OnAsyncTraceCompleted(const FTraceHandle& Handle, FTraceDatum& Data)
{
for (const FHitResult& Hit : Data.OutHits)
{
AActor* HitActor = Hit.GetActor();
GEngine->AddOnScreenDebugMessage(-1, 0.f, FColor::Green, FString::Printf(TEXT("Multi Hit Actor : %s"), *HitActor->GetName()));
DrawDebugSphere(GetWorld(), Hit.ImpactNormal, 20.f, 12, FColor::Green, false, 2.f); // 디버깅이 안 되기 때문에 범위를 그려줘야 함
}
}
FTraceDelegate.BindUObject(this, &A)
비동기 트레이스(Async Trace)가 끝났을 때 호출될 함수(콜백)를 연결하는 코드.
Unreal Engine에서 자주 쓰는 델리게이트(delegate) 패턴.
코드 의미
FTraceDelegate TraceDelegate;
- 트레이스 완료 시 실행될 함수 포인터 같은 역할
- “끝나면 누구를 호출할지” 담는 변수
TraceDelegate.BindUObject(this, &ATraceTest::OnAsyncTraceCompleted);
👉 의미:
- 현재 객체(this)의
- OnAsyncTraceCompleted 함수를
- 콜백으로 등록
쉽게 풀면
👉 “트레이스 끝나면 이 함수 불러줘”
언제 쓰냐
비동기 트레이스 예:
GetWorld()->AsyncLineTraceByChannel(
EAsyncTraceType::Single,
Start,
End,
ECC_Visibility,
QueryParams,
ResponseParams,
&TraceDelegate
);
- 일반 LineTrace → 즉시 결과 반환 (동기)
- AsyncLineTrace → 나중에 결과 반환 (비동기)
👉 그래서:
- 결과를 바로 못 받음
- 대신 Delegate로 결과 받음
콜백 함수 형태 (예시)
void ATraceTest::OnAsyncTraceCompleted(const FTraceHandle& Handle, FTraceDatum& Data)
{
if (Data.OutHits.Num() > 0)
{
// 히트 처리
}
}
핵심 요약
- FTraceDelegate = “완료되면 실행할 함수 저장”
- BindUObject = “이 객체의 이 함수를 연결”
- 결과적으로 👉 비동기 작업 완료 알림 시스템
언제 쓰면 좋냐
- 트레이스 많이 쏘는 경우 (성능 최적화)
- 게임 프레임 안 끊기게 하고 싶을 때
- 멀티 트레이스 / AI / 탄도 계산 등
헷갈리면 이렇게 기억하면 됨:
동기 = 바로 결과 받음
비동기 = 나중에 콜백으로 받음

Igonore > Overlap > Block 순서로 적용된다. Ignore처리를 해놓으면 Overlap으로 바꾸지 못하고, Overlap으로 처리해둔 건 Block으로 바꿀 수 없다. Overlap에서 Ignore, Block에서 Overlap 같은 경우는 가능하다.
데미지 주기

UGameplayStatics::ApplyDamage
#include "Kismet/GameplayStatics.h"로 사용


데미지 타입이란, 공격을 받았을 때 어떤 타입인지 판단할 수 있게 해주는 인자이다. CDO를 사용하게 된다.
데미지 타입 클래스의 코드.
UCLASS(MinimalAPI, const, Blueprintable, BlueprintType)
class UDamageType : public UObject
{
GENERATED_UCLASS_BODY()
/** True if this damagetype is caused by the world (falling off level, into lava, etc). */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category=DamageType)
uint32 bCausedByWorld:1;
/** True to scale imparted momentum by the receiving pawn's mass for pawns using character movement */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category=DamageType)
uint32 bScaleMomentumByMass:1;
/** When applying radial impulses, whether to treat as impulse or velocity change. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = RigidBody)
uint32 bRadialDamageVelChange : 1;
/** The magnitude of impulse to apply to the Actors damaged by this type. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category=RigidBody)
float DamageImpulse;
/** How large the impulse should be applied to destructible meshes */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category=Destruction)
float DestructibleImpulse;
/** How much the damage spreads on a destructible mesh */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category=Destruction)
float DestructibleDamageSpreadScale;
/** Damage fall-off for radius damage (exponent). Default 1.0=linear, 2.0=square of distance, etc. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category=DamageType)
float DamageFalloff;
};
기능은 들어있지 않지만, 사용자 쪽에서 원하는 용도로 분류하여 분기 처리를 하는 역할을 한다.
#include "MySpartaDamageType.h"
UMySpartaDamageType::UMySpartaDamageType()
{
// 물리적 충격 정도
DamageImpulse = 5000.f;
// 물리적 충격에 따른 밀림 여부
bScaleMomentumByMass = true;
// 환경에 의한 데미지. AI가 범인을 찾지 못하게 할 때 유용
bCausedByWorld = true;
}
데미지 타입의 생성자에서 멤버 변수들을 초기화해준다.
이를 사용하기 위해 만든 데미지 타입 헤더와 Kismet/KismetSystemLibrary.h를 include한다.
#include "MySpartaDamageType.h"
#include "Kismet/KismetSystemLibrary.h"
void ATraceTest::OnAsyncTraceCompleted(const FTraceHandle& Handle, FTraceDatum& Data)
{
for (const FHitResult& Hit : Data.OutHits)
{
AActor* HitActor = Hit.GetActor();
GEngine->AddOnScreenDebugMessage(-1, 0.f, FColor::Green, FString::Printf(TEXT("Multi Hit Actor : %s"), *HitActor->GetName()));
DrawDebugSphere(GetWorld(), Hit.ImpactPoint, 20.f, 12, FColor::Green, false, 2.f);
UGameplayStatics::ApplyPointDamage(
HitActor, // 피격자
50.f, // 대미지 값
GetActorForwardVector(), // 대미지 방향
Hit, // 맞은 부위 정보
GetInstigatorController(), // 공격 주체
this, // 대미지를 발생시킨 액터
UMySpartaDamageType::StaticClass() // 대미지 타입은 정적 클래스로 넣어준다. 불속성이면 불속성, 물속성 공격이면 물속성 대미지 타입 넣어줄 수 있다.
);
}
}
ApplyPointDamage
한 지점에 맞은 대미지를 계산하는 함수. 총알처럼 정확한 히트 위치가 있는 공격에 쓰인다.
호출하면 HitActor의 TakeDamage 함수를 실행한다.
- ApplyPointDamage 호출
- → FPointDamageEvent 생성됨
- → HitActor->TakeDamage(...) 실행
ReceiveAnyDamage()
ReceivePointDamage()
OnTakePointDamage (Delegate)
이런 함수들도 같이 실행된다.
TakeDamage 함수 만들기
C++ 프로젝트 생성하면 기본적으로 들어있는 캐릭터 클래스에 Tick함수와 TakeDamage 함수를 추가한다.
헤더 파일
virtual void Tick(float DeltaSecond) override;
virtual float TakeDamage(float DamageAmount, struct FDamageEvent const& DamageEvent, class AController* EventInstigator, AActor* DamageCauser) override;
TakeDamage는 protected로 주로 쓴다.
cpp에는 헤더로 #include "Engine/DamageEvents.h"를 추가해준다.
#include "Engine/DamageEvents.h"
float AMyProjectCharacter::TakeDamage(float DamageAmount, FDamageEvent const& DamageEvent, AController* EventInstigator, AActor* DamageCauser)
{
const float ActualDamage = Super::TakeDamage(DamageAmount, DamageEvent, EventInstigator, DamageCauser);
const UDamageType* DT = DamageEvent.DamageTypeClass->GetDefaultObject<UDamageType>();
if (DT && DT->bCausedByWorld) // DT 유효성 검사 & 월드 대미지일 경우
{
// 낙사 또는 트랩 등일 경우 특별 처리(예: 비명소리 다르게 재생)
UE_LOG(LogTemp, Warning, TEXT("Environmental Damage Received!"));
}
if (EventInstigator)
{
// 일반적인 적의 경우 타겟팅 로직 실행
UE_LOG(LogTemp, Warning, TEXT("I'm Enemy"));
}
return ActualDamage;
}
const UDamageType* DT = DamageEvent.DamageTypeClass->GetDefaultObject<UDamageType>();
GetDefaultObeject() 같은 경우는 CDO 값을 가져와서 사용해준다는 뜻이다.
커스텀 DamageTypeClass 생성
커스텀하려면 MyDamageType 헤더로 들어가서 원하는 대미지를 설정해준다.
화상효과를 만들기 위해 UDamageType을 상속받은 UFireDamageType을 만들어준다.
UCLASS()
class MYGAME_API UFireDamageType : public UDamageType
{
GENERATED_BODY()
public:
UPROPERTY(EditAnywhere, BlueprintReadOnly)
float BurnDuration = 5.0f; // 화상 지속 시간
UPROPERTY(EditAnywhere, BlueprintReadOnly)
float ArmorPenetration = 0.2f; // 방어력 관통 (20%)
};
float ASpartaUnrealMasterCharacter::TakeDamage(float DamageAmount, FDamageEvent const& DamageEvent, AController* EventInstigator, AActor* DamageCauser)
{
float ActualDamage = Super::TakeDamage(DamageAmount, DamageEvent, EventInstigator, DamageCauser);
const UFireDamageType* FireDamage = DamageEvent.DamageTypeClass->GetDefaultObject<UFireDamageType>();
if (FireDamage)
{
//화상
ActualDamage *= (1.f + FireDamage->ArmorPenetration);
//화상효과!! 이펙... 사운드!!!!!
UE_LOG(LogTemp, Warning, TEXT("ByWorld Damage Received"));
}
HP -= ActualDamage;
return ActualDamage;
}
여러 타입의 데미지는 따로 함수로 만들어주어야한다.
커스텀 데이터 활용
과제

'게임 개발 > 학습 일지' 카테고리의 다른 글
| 언리얼 심화 - 템플릿 패턴, 샌드박스 패턴 (0) | 2026.05.01 |
|---|---|
| 언리얼 C++ - 파티클과 사운드로 게임 효과 연출하기 (0) | 2026.04.30 |
| 언리얼 C++ - UI 애니메이션 효과 및 3D 위젯 UI 구현하기 (0) | 2026.04.29 |
| 언리얼 C++ - UI 위젯 설계와 실시간 데이터 연동하기 (0) | 2026.04.27 |
| 언리얼 C++ - State Machine 설계를 통한 캐릭터 동작 애니메이션 적용하기 (0) | 2026.04.24 |