해당 포스팅은 Stuart Butler , Tom Oliver의 Game Development Patterns with Unreal Engine 5를 바탕으로 작성한 내용입니다.
Tick 함수를 최적화해보자.
위와 같이 적을 색적하기 위한 감시 타워 오브젝트를 만들었다.
cpp
// GuardTower.h
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "GuardTower_CH5_1.generated.h"
class UArrowComponent;
class USpotLightComponent;
UCLASS()
class RTS_AI_API AGuardTower_CH5_1 : public AActor
{
GENERATED_BODY()
UPROPERTY(VisibleAnywhere, BlueprintReadWrite, meta=(AllowPrivateAccess = true))
TObjectPtr<UStaticMeshComponent> _TowerMesh;
UPROPERTY(VisibleAnywhere, BlueprintReadWrite, meta=(AllowPrivateAccess = true))
TObjectPtr<USceneComponent> _LightPivot;
UPROPERTY(VisibleAnywhere, BlueprintReadWrite, meta=(AllowPrivateAccess = true))
TObjectPtr<UStaticMeshComponent> _LightMesh;
UPROPERTY(VisibleAnywhere, BlueprintReadWrite, meta=(AllowPrivateAccess = true))
TObjectPtr<USpotLightComponent> _SpotLight;
UPROPERTY(VisibleAnywhere, BlueprintReadWrite, meta=(AllowPrivateAccess = true))
TObjectPtr<UArrowComponent> _Arrow;
public:
// Sets default values for this actor's properties
AGuardTower_CH5_1();
virtual void Tick(float DeltaTime) override;
protected:
UPROPERTY(EditAnywhere, BlueprintReadWrite)
bool _RotateForward;
UPROPERTY(EditAnywhere, BlueprintReadWrite)
bool _EnemySpotted;
UPROPERTY(EditAnywhere, BlueprintReadWrite)
float _DetectionRange;
UPROPERTY(EditAnywhere, BlueprintReadWrite)
float _DetectionRadius;
};cpp
// GuardTower.cpp
#include "GuardTower_CH5_1.h"
#include "Components/ArrowComponent.h"
#include "Components/SpotLightComponent.h"
#include "GameFramework/Character.h"
#include "Kismet/KismetSystemLibrary.h"
AGuardTower_CH5_1::AGuardTower_CH5_1()
{
PrimaryActorTick.bCanEverTick = true;
_RotateForward = true;
_EnemySpotted = false;
_DetectionRange = 4000.f;
_DetectionRadius = 250.f;
_TowerMesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("TowerMesh"));
RootComponent = _TowerMesh;
_LightPivot = CreateDefaultSubobject<USceneComponent>(TEXT("LightPivot"));
_LightPivot->SetupAttachment(_TowerMesh);
_LightMesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("LightMesh"));
_LightMesh->SetupAttachment(_LightPivot);
_SpotLight = CreateDefaultSubobject<USpotLightComponent>(TEXT("SpotLight"));
_SpotLight->SetupAttachment(_LightMesh);
_Arrow = CreateDefaultSubobject<UArrowComponent>(TEXT("Arrow"));
_Arrow->SetupAttachment(_LightMesh);
}
void AGuardTower_CH5_1::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
// 시작 위치와 끝 위치 계산 (전방으로 DetectionRange만큼)
FVector startLocation = _Arrow->GetComponentLocation();
FVector endLocation = _Arrow->GetComponentLocation() + (_Arrow->GetForwardVector() * _DetectionRange);
// 구체(스피어) 트레이스 결과 저장용
FHitResult hit;
TArray<AActor*> ActorsToIgnore;
// 스피어 트레이스 실행
UKismetSystemLibrary::SphereTraceSingle(
GetWorld(),
startLocation,
endLocation,
_DetectionRadius,
UEngineTypes::ConvertToTraceType(ECC_Visibility),
false,
ActorsToIgnore,
EDrawDebugTrace::ForOneFrame,
hit,
true
);
// 히트된 액터를 캐릭터로 캐스팅해 적 탐지 여부 확인
ACharacter* otherCasted = Cast<ACharacter>(hit.GetActor());
_EnemySpotted = (otherCasted != nullptr);
// 적이 없을 때만 라이트를 좌우로 회전
if(!_EnemySpotted)
{
if(_RotateForward)
{
// 오른쪽(또는 지정 방향)으로 회전
_LightPivot->AddLocalRotation(FRotator(0.0, 0.2, 0.0));
// Yaw가 40도에 거의 도달하면 회전 방향 반전
if(FMath::IsNearlyEqual(_LightPivot->GetRelativeRotation().Yaw, 40.f))
{
_RotateForward = false;
}
}
else
{
// 반대 방향으로 회전
_LightPivot->AddLocalRotation(FRotator(0.0, -0.2, 0.0));
// Yaw가 -40도에 거의 도달하면 회전 방향 반전
if(FMath::IsNearlyEqual(_LightPivot->GetRelativeRotation().Yaw, -40.f))
{
_RotateForward = true;
}
}
}
}해당 코드로 실행을 해도 목표로 하는 동작은 정상적으로 수행된다. 하지만 이러한 코드는 잠재적으로 여러가지 문제점을 내포하고 있다.
1. 모든 로직이 Tick 안에 존재
- 스피어 트레이스, 라이트 회전, 적 탐지 모두 Tick에서 수행
- 매 프레임 불필요한 연산 발생 → 성능 저하 가능
2. 매 프레임 Getter/Vector 연산 반복
_Arrow->GetComponentLocation()같은 함수가 매 프레임 반복 호출됨- 불필요한 함수 호출과 Vector 연산 발생
3. 형변환(Cast) 연산 반복
ACharacter* otherCasted = Cast<ACharacter>(hit.GetActor());- 매 Tick마다 수행 → 캐릭터가 많으면 비용 증가
4. 회전 로직의 하드코딩
- Yaw 제한값(-40, 40)과 회전량(0.2f) 하드코딩
5. Gated Polling 미흡
- 적이 감지되지 않은 상태에서도 매 Tick 라이트 회전 계속 수행
- 적 발견 시에도 트레이스나 회전 체크가 Tick에서 계속 이루어짐
최적화 방안
1. Tick 의존 제거 → 이벤트 기반
- SphereComponent의 Overlap 이벤트(OnSphereOverlapBegin, OnSphereOverlapEnd)를 활용해 적 감지
- 적이 들어왔을 때만 LineTrace 수행 → 불필요한 매 프레임 검사 제거 (Gated Polling 적용)
cpp
// GuardTower.h
UFUNCTION()
void OnSphereOverlapBegin(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor,
UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool FromSweep, const FHitResult& SweepResult);
UFUNCTION()
void OnSphereOverlapEnd(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor,
UPrimitiveComponent* OtherComp, int32 OtherBodyIndex);cpp
// GuardTower.cpp
_Sphere = CreateDefaultSubobject<USphereComponent>(TEXT("Sphere"));
_Sphere->SetupAttachment(_LightMesh);
_Sphere->OnComponentBeginOverlap.AddDynamic(this, &AGuardTower_CH5_3::OnSphereOverlapBegin);
_Sphere->OnComponentEndOverlap.AddDynamic(this, &AGuardTower_CH5_3::OnSphereOverlapEnd);
...
void AGuardTower_CH5_3::OnSphereOverlapBegin(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor,
UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool FromSweep, const FHitResult& SweepResult)
{
if(_EnemyUnit != nullptr) return;
_EnemyUnit = Cast<ACharacter>(OtherActor);
if(_EnemyUnit == nullptr) return;
// 필요할 때만 LineTrace 수행
FHitResult hit(ForceInit);
FVector start = _Arrow->GetComponentLocation();
FVector end = _EnemyUnit->GetActorLocation();
TArray<AActor*> ActorsToIgnore;
ActorsToIgnore.Add(_EnemyUnit);
if(UKismetSystemLibrary::LineTraceSingle(GetWorld(), start, end,
UEngineTypes::ConvertToTraceType(ECC_Visibility), false,
ActorsToIgnore, EDrawDebugTrace::ForDuration, hit, true, FLinearColor::Red,
FLinearColor::Green, 0.5f))
{
if(hit.GetActor() != OtherActor) return;
}
_EnemySpotted = true;
StopRotation();
}2. 라인 트레이스 최소화
- _Arrow->GetComponentLocation() 같은 Getter 호출을 이벤트 발생 시점으로 제한
- 매 Tick마다 불필요한 연산 제거
cpp
FVector start = _Arrow->GetComponentLocation();
FVector end = _EnemyUnit->GetActorLocation();
TArray<AActor*> ActorsToIgnore;
ActorsToIgnore.Add(_EnemyUnit);
UKismetSystemLibrary::LineTraceSingle(GetWorld(), start, end,
UEngineTypes::ConvertToTraceType(ECC_Visibility), false,
ActorsToIgnore, EDrawDebugTrace::ForDuration, hit, true, FLinearColor::Red,
FLinearColor::Green, 0.5f);3. 회전 로직 개선(Timeline)
- Timeline + CurveFloat를 사용해 프레임 독립적 회전 구현
- FRotator::SetRelativeRotation(FMath::Lerp(-40.f, 40.f, val))로 부드러운 회전
- 하드코딩 값은 Curve에서 관리 가능
cpp
// GuardTower.h
// Timeline 관련 Delegate
FOnTimelineFloat onTimeline_Update;
FOnTimelineEventStatic onTimeline_Finished;
// Timeline 처리 함수
UFUNCTION()
void Handle_RotateLight_Update(float val);
UFUNCTION()
void Handle_RotateLight_Finished();
// Timeline 제어
void StartRotation();
void StopRotation();
// Timeline 컴포넌트
UPROPERTY(EditAnywhere, BlueprintReadWrite, meta=(AllowPrivateAccess = true))
TObjectPtr<UTimelineComponent> T_RotateLight;
// CurveFloat
UPROPERTY(EditAnywhere)
UCurveFloat* _Curve;cpp
// GuardTower.cpp
AGuardTower_CH5_3::AGuardTower_CH5_3()
{
...
T_RotateLight = CreateDefaultSubobject<UTimelineComponent>(TEXT("T_RotateLight"));
onTimeline_Update.BindUFunction(this, FName("Handle_RotateLight_Update"));
onTimeline_Finished.BindUFunction(this, FName("Handle_RotateLight_Finished"));
}
void AGuardTower_CH5_3::BeginPlay()
{
Super::BeginPlay();
if(_Curve == nullptr) { return;}
T_RotateLight->AddInterpFloat(_Curve, onTimeline_Update, FName("Alpha"));
T_RotateLight->SetTimelineFinishedFunc(onTimeline_Finished);
T_RotateLight->SetLooping(false);
T_RotateLight->SetIgnoreTimeDilation(true);
StartRotation();
}
void AGuardTower_CH5_3::Handle_RotateLight_Update(float val)
{
// Curve 기반 Lerp로 부드럽게 회전
_LightPivot->SetRelativeRotation(
FRotator(0.f, FMath::Lerp(-40.f, 40.f, val), 0.f));
}
void AGuardTower_CH5_3::Handle_RotateLight_Finished()
{
// 회전 방향 토글
_RotateForward = !_RotateForward;
StartRotation();
}
void AGuardTower_CH5_3::StartRotation()
{
if(_RotateForward)
T_RotateLight->Play();
else
T_RotateLight->Reverse();
}
void AGuardTower_CH5_3::StopRotation()
{
T_RotateLight->Stop();
}4. Gated Polling 적용
- _EnemySpotted 값에 따라 Timeline 회전 제어
- 적 발견 시 Timeline 중지 → 불필요한 반복 제거
- 적이 사라지면 Timeline 다시 재생
cpp
void AGuardTower_CH5_3::OnSphereOverlapEnd(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor,
UPrimitiveComponent* OtherComp, int32 OtherBodyIndex)
{
if(_EnemyUnit != OtherActor) return;
_EnemySpotted = false;
_EnemyUnit = nullptr;
StartRotation(); // 적이 사라지면 다시 회전 시작
}5. 로직 함수 분리
- 최종적으로 회전 처리, Timeline 제어, 적 감지 처리 등을 Tick함수에서 개별 함수로 분리
- 코드 가독성 및 유지보수성 향상
cpp
// 회전 처리
void AGuardTower_CH5_3::Handle_RotateLight_Update(float val);
// 회전 종료 후 방향 토글
void AGuardTower_CH5_3::Handle_RotateLight_Finished();
// Timeline 제어
void AGuardTower_CH5_3::StartRotation();
void AGuardTower_CH5_3::StopRotation();
// 적 감지 처리
void AGuardTower_CH5_3::OnSphereOverlapBegin(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor,
UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool FromSweep, const FHitResult& SweepResult);
void AGuardTower_CH5_3::OnSphereOverlapEnd(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor,
UPrimitiveComponent* OtherComp, int32 OtherBodyIndex);최종 코드 전체
cpp
// GuardTower.h 개선 버전
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "Components/SphereComponent.h"
#include "Components/TimelineComponent.h"
#include "GameFramework/Actor.h"
#include "GuardTower_CH5_3.generated.h"
class ACharacter;
class UArrowComponent;
class USpotLightComponent;
UCLASS()
class RTS_AI_API AGuardTower_CH5_3 : public AActor
{
GENERATED_BODY()
UPROPERTY(VisibleAnywhere, BlueprintReadWrite, meta=(AllowPrivateAccess = true))
TObjectPtr<UStaticMeshComponent> _TowerMesh;
UPROPERTY(VisibleAnywhere, BlueprintReadWrite, meta=(AllowPrivateAccess = true))
TObjectPtr<USceneComponent> _LightPivot;
UPROPERTY(VisibleAnywhere, BlueprintReadWrite, meta=(AllowPrivateAccess = true))
TObjectPtr<UStaticMeshComponent> _LightMesh;
UPROPERTY(VisibleAnywhere, BlueprintReadWrite, meta=(AllowPrivateAccess = true))
TObjectPtr<USpotLightComponent> _SpotLight;
UPROPERTY(VisibleAnywhere, BlueprintReadWrite, meta=(AllowPrivateAccess = true))
TObjectPtr<UArrowComponent> _Arrow;
UPROPERTY(VisibleAnywhere, BlueprintReadWrite, meta=(AllowPrivateAccess = true))
TObjectPtr<USphereComponent> _Sphere;
FOnTimelineFloat onTimeline_Update;
FOnTimelineEventStatic onTimeline_Finished;
UFUNCTION()
void Handle_RotateLight_Update(float val);
UFUNCTION()
void Handle_RotateLight_Finished();
void StartRotation();
void StopRotation();
public:
AGuardTower_CH5_3();
protected:
UPROPERTY(EditAnywhere, BlueprintReadWrite)
bool _RotateForward;
UPROPERTY(EditAnywhere, BlueprintReadWrite)
bool _EnemySpotted;
UPROPERTY(EditAnywhere, BlueprintReadWrite)
float _DetectionRange;
UPROPERTY(EditAnywhere, BlueprintReadWrite)
float _DetectionRadius;
UPROPERTY(EditAnywhere, BlueprintReadWrite)
TObjectPtr<ACharacter> _EnemyUnit;
UPROPERTY(EditAnywhere, BlueprintReadWrite)
TObjectPtr<UTimelineComponent> T_RotateLight;
UPROPERTY(EditAnywhere)
UCurveFloat* _Curve;
virtual void BeginPlay() override;
UFUNCTION()
void OnSphereOverlapBegin(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool FromSweep, const FHitResult& SweepResult);
UFUNCTION()
void OnSphereOverlapEnd(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex);
};cpp
// GuardTower.cpp 개선 버전
#include "GuardTower_CH5_3.h"
#include "Components/ArrowComponent.h"
#include "Components/SpotLightComponent.h"
#include "GameFramework/Character.h"
#include "Kismet/KismetSystemLibrary.h"
AGuardTower_CH5_3::AGuardTower_CH5_3()
{
PrimaryActorTick.bCanEverTick = true;
_RotateForward = true;
_EnemySpotted = false;
_DetectionRange = 4000.f;
_DetectionRadius = 250.f;
_TowerMesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("TowerMesh"));
RootComponent = _TowerMesh;
_LightPivot = CreateDefaultSubobject<USceneComponent>(TEXT("LightPivot"));
_LightPivot->SetupAttachment(_TowerMesh);
_LightMesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("LightMesh"));
_LightMesh->SetupAttachment(_LightPivot);
_SpotLight = CreateDefaultSubobject<USpotLightComponent>(TEXT("SpotLight"));
_SpotLight->SetupAttachment(_LightMesh);
_Arrow = CreateDefaultSubobject<UArrowComponent>(TEXT("Arrow"));
_Arrow->SetupAttachment(_LightMesh);
_Sphere = CreateDefaultSubobject<USphereComponent>(TEXT("Sphere"));
_Sphere->SetupAttachment(_LightMesh);
_Sphere->OnComponentBeginOverlap.AddDynamic(this, &AGuardTower_CH5_3::OnSphereOverlapBegin);
_Sphere->OnComponentEndOverlap.AddDynamic(this, &AGuardTower_CH5_3::OnSphereOverlapEnd);
T_RotateLight = CreateDefaultSubobject<UTimelineComponent>(TEXT("T_RotateLight"));
onTimeline_Update.BindUFunction(this, FName("Handle_RotateLight_Update"));
onTimeline_Finished.BindUFunction(this, FName("Handle_RotateLight_Finished"));
}
void AGuardTower_CH5_3::BeginPlay()
{
Super::BeginPlay();
if(_Curve == nullptr) { return;}
T_RotateLight->AddInterpFloat(_Curve, onTimeline_Update, FName("Alpha"));
T_RotateLight->SetTimelineFinishedFunc(onTimeline_Finished);
T_RotateLight->SetLooping(false);
T_RotateLight->SetIgnoreTimeDilation(true);
StartRotation();
}
void AGuardTower_CH5_3::Handle_RotateLight_Update(float val)
{
_LightPivot->SetRelativeRotation(
FRotator(0.f, FMath::Lerp(-40.f, 40.f, val), 0.f));
}
void AGuardTower_CH5_3::Handle_RotateLight_Finished()
{
_RotateForward = !_RotateForward;
StartRotation();
}
void AGuardTower_CH5_3::StartRotation()
{
if(_RotateForward)
{
T_RotateLight->Play();
}
else
{
T_RotateLight->Reverse();
}
}
void AGuardTower_CH5_3::StopRotation()
{
T_RotateLight->Stop();
}
void AGuardTower_CH5_3::OnSphereOverlapBegin(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor,
UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool FromSweep, const FHitResult& SweepResult)
{
if(_EnemyUnit != nullptr) {return;}
_EnemyUnit = Cast<ACharacter>(OtherActor);
if(_EnemyUnit == nullptr) {return;}
FHitResult hit(ForceInit);
FVector start = _Arrow->GetComponentLocation();
FVector end = _EnemyUnit->GetActorLocation();
TArray<AActor*> ActorsToIgnore;
ActorsToIgnore.Add(_EnemyUnit);
if(UKismetSystemLibrary::LineTraceSingle(GetWorld(), start, end,
UEngineTypes::ConvertToTraceType(ECC_Visibility), false,
ActorsToIgnore, EDrawDebugTrace::ForDuration, hit, true, FLinearColor::Red,
FLinearColor::Green, 0.5f))
{
if(hit.GetActor() != OtherActor) return;
}
_EnemySpotted = true;
StopRotation();
}
void AGuardTower_CH5_3::OnSphereOverlapEnd(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor,
UPrimitiveComponent* OtherComp, int32 OtherBodyIndex)
{
if(_EnemyUnit != OtherActor) {return;}
_EnemySpotted = false;
_EnemyUnit = nullptr;
StartRotation();
}요약 비교
| 항목 | 수정 전 | 수정 후 |
|---|---|---|
| 적 감지 | 매 Tick 스피어 트레이스 | SphereComponent Overlap 이벤트 + LineTrace |
| 라이트 회전 | Tick에서 AddLocalRotation, 하드코딩 | Timeline + CurveFloat로 프레임 독립적 회전 |
| Gated Polling | 미적용, 항상 검사 | 적 발견 여부에 따라 회전/트레이스 제어 |
| 성능 | 매 프레임 불필요 연산 | 이벤트 발생 시에만 최소 연산 |
| 가독성 | Tick에 모든 로직 포함 | 함수 분리, 역할 별 모듈화 |