2026-04-24 TIL (44일차)
2026-04-24 TIL (44일차)
객체 지향 설계피해야 할 구조
코드를 짤 때 “돌아가기만 하면 된다”는 마인드는 프로젝트가 커질수록 유지보수를 지옥으로 만듭니다. 피해야 할 3가지 대표적인 안티 패턴입니다.
1) 뒤엉킨 변경
- 정의: 하나의 클래스가 여러 가지 이유로 계속 수정되어야 하는 상태입니다. (단일 책임 원칙 위배)
- 해결: 데이터 관리 로직과 게임플레이 로직을 각각 다른 클래스로 분리해야 합니다.
- 나쁜 예:
AGameManager안에 데이터 저장(Save/Load) 함수와 몬스터 스폰 함수가 같이 있음. - 좋은 예:
UPlayerDataManager와UGameplayManager로 쪼개기.
- 나쁜 예:
2) 샷건 수술
- 정의: 하나의 작은 기능을 수정하기 위해 여러 클래스의 코드를 동시다발적으로 열어서 수정해야 하는 상황입니다.
- 해결: 관련된 데미지/계산 로직을 한 군데(예: DamageSystem)로 모아서 캡슐화합니다.
3) 기능 편애
- 정의: 어떤 함수가 자신이 속한 객체의 데이터보다 남의 객체 데이터와 더 많이 소통하고 의존하는 현상입니다.
- 해결: 계산 로직을 데이터가 있는 쪽(객체 내부)으로 넘겨주고, 밖에서는 결과만 받아오도록 위임합니다.
Unreal
Collision
🔹 오브젝트 채널 vs 프리셋
- 오브젝트 채널 (Object Channel): “나는 누구인가?” (예: 베리어, 플레이어, 적)
- 프리셋 (Preset): “내가 다른 애들을 만났을 때 어떻게 상호작용할 것인가?”를 미리 세팅해둔 설정표입니다.
🔹 충돌 우선순위
콜리전 반응은 서로 다른 두 물체가 만났을 때 더 약한 쪽의 판정을 따라갑니다.
- 우선순위:
Ignore>Overlap>Block - 해석: 나는 상대를 Block(막음) 하려고 해도, 상대방이 나를 Ignore(무시)로 설정해두면 둘은 무조건 Ignore 처리되어 통과합니다.
🔹 단순 콜리전 vs 복합 콜리전
스태틱 메시의 기본 충돌체 모양을 결정합니다.
- 단순 콜리전 (Simple): 박스, 캡슐처럼 개발자가 대략적으로 씌운 가벼운 충돌체.
- 복합 콜리전 (Complex): 모델링의 폴리곤 형태를 100% 그대로 따라가는 무거운 충돌체.
- (Project Default / Simple And Complex): 평소 캐릭터가 걸어 다닐 때는 가벼운 ‘단순 콜리전’을 쓰고, 총을 쏘거나 정밀한 트레이스(헤드샷 판정 등)를 할 때만 ‘복합 콜리전’을 켜서 검사하는 방식을 주로 씁니다.
Trace
🔹 Kismet vs UWorld 차이점
- UKismetSystemLibrary: 블루프린트용으로 한번 래핑된 함수입니다. 매개변수가 친절하고 디버깅 선(DrawDebug)을 그리기 매우 편합니다.
- UWorld: C++ 네이티브 트레이스입니다. 디버깅은 불편하지만 설정이 자유롭고 성능이 미세하게 더 빠릅니다. (보통 Kismet으로 개발하고 최적화 시 UWorld로 넘어갑니다.)
🔹 트레이스 종류별 특징
- Single Trace: 레이저가 날아가다가 최초로 Block 처리된 물체 하나만 인식하고 멈춥니다. (Overlap은 그냥 무시하고 관통합니다.)
- Multi Trace: 레이저가 날아가며 만나는 모든 Overlap 물체들을 뚫고 지나가며 다 기록합니다. 마지막에 Block을 만나면 그때 멈춥니다.
- Async Trace (비동기): 몬스터 수백 마리가 매 프레임 시야각 트레이스를 쏘면 게임이 멈춥니다. 이를 방지하기 위해 메인 작업대(스레드)가 아닌 뒷단 작업대에서 트레이스를 계산하고 결과만 나중에 받아오는 최적화 기법입니다.
비동기 트레이스 (Async Trace) 코드 분석
비동기는 호출 즉시 결과를 주지 않으므로, 작업이 끝났을 때 알람을 받을 델리게이트(Delegate) 연결이 필수입니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
void ATraceTest::StartAsyncTrace()
{
// 1. 작업 완료 후 콜백받을 델리게이트 세팅
FTraceDelegate TraceDelegate;
TraceDelegate.BindUObject(this, &ATraceTest::OnAsyncTraceCompleted);
// 2. 콜리전 응답 및 예외 설정 (나 자신은 무시해라)
FCollisionResponseParams ResponseParams;
ResponseParams.CollisionResponse.WorldDynamic = ECR_Overlap;
FCollisionQueryParams QueryParams;
QueryParams.AddIgnoredActor(this);
QueryParams.bTraceComplex = false; // 복잡한 콜리전 사용 안함 (최적화)
// 3. 비동기 트레이스 발사! (Multi 방식)
GetWorld()->AsyncLineTraceByChannel(
EAsyncTraceType::Multi,
GetActorLocation(),
GetActorForwardVector() * 1000.f + GetActorLocation(),
ECC_Visibility, QueryParams, ResponseParams, &TraceDelegate
);
}
// 4. 트레이스 연산이 끝나면 자동으로 실행되는 콜백 함수
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, HitActor->GetName());
DrawDebugSphere(GetWorld(), Hit.ImpactPoint, 20.f, 12, FColor::Green, false, 2.f);
}
}
Damage
트레이스로 적을 맞췄다면 이제 데미지를 줘야 합니다. 언리얼은 데미지를 ‘주는 쪽(Apply)’과 ‘받는 쪽(Take)’을 명확히 분리해 설계했습니다.
데미지 타입 (UDamageType)과 CDO
- 데미지 타입: “이게 독 데미지냐, 화상 데미지냐, 타격이냐?”를 구분해주는 기준(분기점) 클래스입니다.
- CDO (Class Default Object) 활용: 데미지를 줄 때마다 데미지 타입 객체를 새로 생성(New)하면 메모리 낭비가 심합니다. 따라서 엔진 켜질 때 미리 만들어둔 원본 붕어빵(
GetDefaultObject<UDamageType>())을 참조해서 정보만 빼와서 계산합니다.
데미지 주고 받기 코드 로직
1) 공격자: 데미지 주기 (ApplyPointDamage) 총기류나 칼처럼 정확한 ‘타격 위치(Hit)’와 ‘방향’이 있을 때 사용합니다.
1
2
3
4
5
6
7
8
9
UGameplayStatics::ApplyPointDamage(
HitActor, // 1. 누구를 때렸나? (피격 대상)
50.f, // 2. 데미지 수치
GetActorForwardVector(), // 3. 어느 방향에서 때렸나? (넉백 계산용)
Hit, // 4. 트레이스에서 얻은 정확한 타격 정보
GetInstigatorController(), // 5. 이 공격을 지시한 흑막(컨트롤러)
this, // 6. 날 때린 무기/가해자
UMyTestDamageType::StaticClass() // 7. 데미지 속성 (불, 독 등)
);
2) 피해자: 데미지 받기 (TakeDamage)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
float ASpartaUnrealMasterCharacter::TakeDamage(float DamageAmount, FDamageEvent const& DamageEvent, AController* EventInstigator, AActor* DamageCauser)
{
// 1. 부모(엔진 코어) 클래스에서 무적 판정 등 기본 연산 거치기
float ActualDamage = Super::TakeDamage(DamageAmount, DamageEvent, EventInstigator, DamageCauser);
// 2. 어떤 종류의 데미지인지 파악하기 위해 CDO 불러오기
const UFireDamageType* FireDamage = DamageEvent.DamageTypeClass->GetDefaultObject<UFireDamageType>();
// 3. 만약 불 속성 데미지라면?
if (FireDamage)
{
// 방어력 관통 수치만큼 데미지 뻥튀기!
ActualDamage *= (1.0f + FireDamage->ArmorPenetration);
UE_LOG(LogTemp, Warning, TEXT("불이야!"));
}
// 4. 최종 체력 깎기
HP -= ActualDamage;
return ActualDamage;
}
This post is licensed under CC BY 4.0 by the author.