최근 GDC 2026과 주요 기술 세션에서 다뤄진 핵심 게임 산업 트렌드 중 하나는 AI 연산의 온디바이스(On-Device)화입니다. 지금까지 많은 게임이 생성형 AI나 복잡한 의사결정 모델을 구현할 때 클라우드 API를 호출해 처리하는 방식을 택했습니다. 하지만 이는 실시간 프레임 동기화가 필요한 게임플레이 환경에서 치명적인 네트워크 지연(Latency)을 유발하고, 동시 접속자가 늘어날수록 서버 유지 비용이 기하급수적으로 폭증하는 한계를 지닙니다.
이러한 문제를 극복하고자 2026년의 선두적인 개발 스튜디오들은 클라우드 비용을 전혀 쓰지 않고 유저 기기의 CPU나 GPU, 혹은 NPU(Neural Processing Unit)를 직접 활용하여 경량 신경망 모델을 실행하는 '온디바이스 ML' 구조를 프로덕션에 적극적으로 도입하고 있습니다. 특히 언리얼 엔진은 버전이 거듭되면서 NNE(Neural Network Engine) 프레임워크를 정식 런타임 스택으로 확고히 자리매김하며 개발자가 직접 학습시킨 ONNX(Open Neural Network Exchange) 모델을 C++ 게임 런타임에서 초고속으로 실시간 추론할 수 있는 환경을 완비했습니다.
본 글에서는 언리얼 엔진의 NNE 구조와 트렌드를 간략히 짚어보고, 실제로 프로젝트에 적용할 수 있는 C++ 연동 예제 코드를 바탕으로 온디바이스 AI 캐릭터 제어를 구현하는 구체적인 실무 방법을 살펴보겠습니다.
1. 언리얼 엔진 NNE 아키텍처 개요
언리얼 엔진의 NNE는 PyTorch, TensorFlow, JAX 등의 프레임워크에서 학습되고 내보내진 ONNX 형식의 신경망 모델을 게임 엔진 내부에서 로드하고 다중 플랫폼 하드웨어 가속 환경에서 안정적으로 구동하기 위한 표준 추상화 레이어입니다.
NNE 아키텍처의 큰 장점은 플랫폼마다 다른 AI 하드웨어 가속기 백엔드를 코드 수준에서 알 필요 없이 공통 인터페이스로 묶어준다는 점입니다. 예를 들어, Windows 환경에서는 DirectRD(DirectML 기반)나 Windows 오디오 모델 가속기를 사용하고, 모바일 및 콘솔 환경에서는 해당 기기들의 NPU나 GPU 전용 엔진으로 유연하게 폴백(Fallback) 처리됩니다. 이를 통해 개발자는 일관된 C++ 코드로 멀티플랫폼 게임에 온디바이스 머신러닝을 구현할 수 있습니다.
2. NNE 사용을 위한 C++ 빌드 설정
NNE API는 엔진의 별도 플러그인 모듈로 제공되므로 프로젝트에서 사용하려면 Build.cs 파일에 종속성을 추가해야 합니다. 또한 에디터 플러그인 설정에서 NNE 관련 플러그인들이 활성화되어 있는지 확인이 필요합니다.
// MyProject.Build.cs
using UnrealBuildTool;
public class MyProject : ModuleRules
{
public MyProject(ReadOnlyTargetRules Target) : base(Target)
{
PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
PublicDependencyModuleNames.AddRange(new string[] {
"Core",
"CoreUObject",
"Engine",
"InputCore",
"NNE" // NNE 모듈 의존성 추가
});
}
}
3. C++ 기반 NNE 컴포넌트 선언 및 초기화
게임 액터나 캐릭터에 장착하여 온디바이스 추론을 수행할 헬퍼 컴포넌트 예시입니다. ONNX 모델 파일은 언리얼 에디터에 임포트되어 UNNEModelData 에셋 형식으로 다뤄집니다.
먼저 텐서 버퍼와 런타임 인스턴스를 관리할 헤더 파일을 정의합니다.
// OnDeviceAIComponent.h
#pragma once
#include "CoreMinimal.h"
#include "Components/ActorComponent.h"
#include "NNE.h"
#include "NNERuntime.h"
#include "NNEModelData.h"
#include "OnDeviceAIComponent.generated.h"
UCLASS(ClassGroup=(Custom), meta=(BlueprintSpawnableComponent))
class MYPROJECT_API UOnDeviceAIComponent : public UActorComponent
{
GENERATED_BODY()
public:
UOnDeviceAIComponent();
protected:
virtual void BeginPlay() override;
public:
virtual void TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override;
private:
// 에디터에서 지정할 학습 완료된 ONNX 모델 에셋
UPROPERTY(EditAnywhere, Category = "OnDeviceAI")
TObjectPtr<UNNEModelData> ModelData;
// NNE 모델 인스턴스 인터페이스 (GPU/DirectML 백엔드용 모델 인스턴스)
TSharedPtr<UE::NNE::IModelInstanceRD> ModelInstance;
// 입출력 텐서 연산을 위한 C++ 메모리 버퍼
TArray<float> InputBuffer;
TArray<float> OutputBuffer;
bool InitializeModel();
void RunInference();
};
4. 모델 로드 및 실시간 추론 루프 구현
소스 파일에서는 엔진의 NNE 모듈을 통해 사용 가능한 최적의 런타임 백엔드(예: NNERuntimeRD)를 찾고, 입력 텐서의 볼륨에 맞춰 C++ 측 메모리를 미리 할당합니다. 이후 Tick에서 캐릭터의 물리 정보(속도, 상대 거리 등)를 전처리하여 입력 텐서에 밀어 넣고 추론 함수를 실행합니다.
// OnDeviceAIComponent.cpp
#include "OnDeviceAIComponent.h"
#include "GameFramework/Actor.h"
UOnDeviceAIComponent::UOnDeviceAIComponent()
{
PrimaryComponentTick.bCanEverTick = true;
}
void UOnDeviceAIComponent::BeginPlay()
{
Super::BeginPlay();
InitializeModel();
}
bool UOnDeviceAIComponent::InitializeModel()
{
if (!ModelData)
{
UE_LOG(LogTemp, Warning, TEXT("ModelData가 지정되지 않았습니다."));
return false;
}
// NNE에서 하드웨어 가속(GPU/NPU)을 지원하는 런타임 백엔드 가져오기
TWeakInterfacePtr<INNERuntimeRD> NNERuntime = UE::NNE::GetRuntime<INNERuntimeRD>(TEXT("NNERuntimeRD"));
if (!NNERuntime.IsValid())
{
UE_LOG(LogTemp, Error, TEXT("적합한 NNE 하드웨어 가속 런타임을 찾을 수 없습니다."));
return false;
}
// ONNX 모델 데이터 기반 모델 개체 생성
TUniquePtr<UE::NNE::IModelRD> Model = NNERuntime->CreateModel(ModelData);
if (!Model.IsValid())
{
UE_LOG(LogTemp, Error, TEXT("NNE 모델 생성에 실패했습니다."));
return false;
}
// 추론 연산을 지속적으로 수행할 모델 인스턴스 생성
ModelInstance = Model->CreateModelInstance();
if (!ModelInstance.IsValid())
{
UE_LOG(LogTemp, Error, TEXT("NNE 모델 인스턴스 생성에 실패했습니다."));
return false;
}
// 입출력 텐서 모양(Shape) 및 원소 수 정의
TConstArrayView<UE::NNE::FTensorShape> InputShapes = ModelInstance->GetInputTensorShapes();
TConstArrayView<UE::NNE::FTensorShape> OutputShapes = ModelInstance->GetOutputTensorShapes();
if (InputShapes.Num() > 0 && OutputShapes.Num() > 0)
{
// 첫 번째 입력/출력 텐서의 원소 개수만큼 로컬 메모리 버퍼 확보
uint32 InputElementCount = InputShapes[0].GetVolume();
InputBuffer.Init(0.0f, InputElementCount);
uint32 OutputElementCount = OutputShapes[0].GetVolume();
OutputBuffer.Init(0.0f, OutputElementCount);
UE_LOG(LogTemp, Log, TEXT("NNE 모델 초기화 완료. 입력: %d, 출력: %d"), InputElementCount, OutputElementCount);
return true;
}
return false;
}
void UOnDeviceAIComponent::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction)
{
Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
if (!ModelInstance.IsValid())
{
return;
}
AActor* Owner = GetOwner();
if (Owner)
{
// 1. 게임플레이 환경 센싱 데이터 정규화 및 피처 팩터링
FVector Velocity = Owner->GetVelocity();
// 모델 가중치 학습 형태에 맞추어 스케일링 전처리 수행
InputBuffer[0] = Velocity.X / 1000.0f;
InputBuffer[1] = Velocity.Y / 1000.0f;
InputBuffer[2] = Velocity.Z / 1000.0f;
// 2. 신경망 추론
RunInference();
// 3. 추론 결과 기반의 피드백 제어 (예: 애니메이션 가중치 조절, 장애물 회피 보정값 등)
float MoveBias = OutputBuffer[0];
if (MoveBias > 0.5f)
{
// 추론된 오프셋이나 조작 편향값에 따른 물리 반응 처리 구현
}
}
}
void UOnDeviceAIComponent::RunInference()
{
// C++ 어레이 포인터를 바인딩 객체로 NNE 텐서 입력단에 주입
TArray<UE::NNE::FTensorBindingRD> InputBindings;
InputBindings.Add(UE::NNE::FTensorBindingRD{ InputBuffer.GetData(), InputBuffer.Num() * sizeof(float) });
TArray<UE::NNE::FTensorBindingRD> OutputBindings;
OutputBindings.Add(UE::NNE::FTensorBindingRD{ OutputBuffer.GetData(), OutputBuffer.Num() * sizeof(float) });
// 실시간 동기 추론 실행
UE::NNE::EConnectionStatus Status = ModelInstance->RunSync(InputBindings, OutputBindings);
if (Status != UE::NNE::EConnectionStatus::Success)
{
UE_LOG(LogTemp, Error, TEXT("NNE 추론 연산 도중 에러가 발생했습니다."));
}
}
5. 온디바이스 AI 성능 및 프로덕션 적용 팁
게임 클라이언트 내 틱 루프에서 딥러닝 추론을 실행하는 것은 상당한 하드웨어 리소스를 소모합니다. 다음 세 가지 핵심 최적화 기법을 적용하여 프레임 드랍을 원천 차단해야 합니다.
- FP16 및 INT8 양자화(Quantization) 모델 도입
일반적으로 PyTorch에서 모델을 내보내면 FP32(32비트 부동소수점) 정밀도를 가집니다. 하지만 모바일 디바이스나 콘솔 APU는 대역폭 절약과 연산 유닛 구조 상 FP16이나 8비트 정수로 축소 정밀화된 모델을 돌릴 때 훨씬 높은 처리 속도를 냅니다. ONNX Runtime 툴킷이나 외부 컴파일러를 통해 에셋 자체를 양자화하여 이식하는 것이 권장됩니다. - 비동기 추론 구조 설계 (
RunAsync)
경량 예측 연산이라 하더라도 60FPS 이상의 타이트한 루프에서 매 프레임 동기 추론을 구동하면 가끔 프레임이 튀는 현상(Stuttering)이 나타날 수 있습니다. NNE는 비동기 처리 인터페이스인IModelInstance기반의 비동기 버전을 별도로 제공하므로, 연산량이 큰 모델의 경우 작업 스레드나 TaskGraph에 태스크를 위임하고 결과가 나오면 다음 프레임에 반영하는 래핑 아키텍처가 적합합니다. - 결정적인(Deterministic) 입력 데이터 파이프라인 수립
온디바이스 ML의 흔한 버그 중 하나는 게임 플레이 중 비정상적으로 큰 값(예: 물리 충돌로 인한 찰나의 속도 발산)이 입력 텐서로 주입될 때 신경망 출력이 붕괴되는 현상입니다. C++ 파이프라인 단계에서 데이터 주입 전 반드시FMath::Clamp및 정규화(Standardization)를 엄격히 수행해야 예상치 못한 이상 동작을 막을 수 있습니다.