[Unreal Engine] Data Modifier

이매·2026년 3월 27일

Unreal Data Driven Design

목록 보기
8/12
post-thumbnail

1. Data Modifier

게임에서 버프 시스템을 구현한다고 가정해보자.

플레이어의 공격력은 기본적으로 100이며, 특정 버프를 받으면 공격력이 150으로 증가한다.
이때 단순하게 변수 값을 직접 변경하는 방식으로 구현할 수도 있지만, 이러한 방식은 여러 문제를 발생시킨다.

  • 원본 데이터의 오염(변화)

공격력이라는 기본 데이터는 캐릭터의 본질적인 스탯을 나타내는 값이다.
하지만 버프나 디버프에 의해 이 값을 직접 변경하게 되면, 원래의 값이 무엇이었는지 추적하기 어려워진다. 또한 여러 효과가 중첩될 경우, 어떤 효과가 얼마나 영향을 미쳤는지 관리하기가 매우 복잡해진다.

  • 변화의 원인/과정은 표현 불가

Data Instance는 런타임에서 변화하는 데이터를 담기 위한 구조이지만, 단순히 값을 변경하는 방식만으로는 “왜 값이 변했는지”에 대한 정보를 표현할 수 없다.
따라서 데이터의 상태는 표현할 수 있지만, 변화의 원인과 과정은 표현할 수 없다.

이러한 문제를 해결하기 위해 등장하는 개념이 Data Modifier이다.

Data Modifier는 원본 데이터를 직접 변경하지 않고, 추가적인 데이터 계층을 통해 최종 결과값을 계산하는 방식을 사용한다.
이를 통해 데이터의 안정성을 유지하면서도, 다양한 게임 플레이 상황에 유연하게 대응할 수 있다.

1-1. Data Modifier 기본 구조

Data Modifier의 기본 구조는 다음과 같다.

  • Base Value (기본 값)
  • Modifier (변형 값)
  • Source (Modifier의 출처)
  • New Value (최종 값)

여기서 중요한 점은 New Value가 직접 저장되는 것이 아니라 계산된 결과라는 점이다.
이 구조의 핵심 개념은 다음과 같다.

실제 데이터 구조는 “결과 값(New Value)”을 보관하는 것이 아니라, “결과를 만들어내는 요소들(Modifier)”을 보관한다.

데이터를 변경하지 않고, 결과를 변경한다.

예를 들어

  • 공격력이 100인 캐릭터가 +20 버프를 부여 받음.

그러면 공격력을 120으로 바꾸는 것이 아니라 +20 Modifier가 존재한다는 정보를 추가하고 최종 계산 시 이를 반영하는 방식이다.

이때 추가적으로 Source 정보를 통해 각 Modifier가 어디서 왔는지를 명확하게 구분할 수 있어야, 특정 장비를 해제하거나 버프가 종료될 때 정확하게 해당 Modifier만 제거할 수 있다.

1-2. Data Modifier의 역할

Data Modifier를 통해 데이터를 변형하는 방식은 여러가지가 있을 수 있다.
가장 보편적으로 사용하는 방식은 사칙연산 기반의 값 변형 방식이다.

  • Add (- 혹은 + 값을 더함)
  • Multiply (배율로 곱함)
  • Override (특정 값으로 덮어씌움)

이러한 Modifier들은 각각 독립적으로 존재할 수도 있겠지만, 재사용성을 고려해서 하나의 계산 구조 안에서 결합하는 방식으로 많이 이용된다.

New Value=(Base Value+Additive)×(1+Multiplicative)New\ Value = (Base\ Value + \sum Additive) \times (1 + \sum Multiplicative)

Override Modifier가 존재할 경우 위 수식의 결과와 상관없이 최종값이 특정 수치로 강제함.

이러한 구조를 사용하는 이유는 다음과 같다.

  • 계산 순서를 명확하게 정의할 수 있다
  • Modifier 간의 상호작용을 예측 가능하게 만든다

1-3. Data Modifier 설계 원칙

Data Modifier 시스템은 데이터의 안정성과 확장성을 동시에 만족시키기 위한 설계 구조이다.
따라서 몇 가지 사용 규칙을 고려하면 좋다.

  1. Base 값은 변경하지 않는다.

  2. Modifier는 별도로 관리한다.

  3. 계산은 필요한 시점에 수행한다.

  4. Modifier는 조합 가능한 구조로 설계한다

  5. 계산 규칙은 일관되게 유지한다.



2. Data Modifier 사용 예시

가장 기본적인 설계는 Modifier 타입을 분리하고, 계산 순서를 고정하고 누적하는 것이다.
이를 위해 단순한 형태의 Modifier를 만들어보자.

FBaseModifier.h

#pragma once

#include "CoreMinimal.h"
#include "BaseModifier.generated.h"

UENUM(BlueprintType)
enum class EModifierOperation : uint8
{
	Add			UMETA(DisplayName = "Add"),		// 값을 더한다
	Multiply	UMETA(DisplayName = "Multiply"),// 배율로 곱한다
	Override	UMETA(DisplayName = "Override")	// 최종값을 덮어쓴다
};

USTRUCT(BlueprintType)
struct FBaseModifier
{
	GENERATED_BODY()

public:
	FBaseModifier()
		: Operation(EModifierOperation::Add)
		, Value(0.0f)
		, Source(nullptr)
	{
	}

	FBaseModifier(EModifierOperation InOperation, float InValue, UObject* InSource)
		: Operation(InOperation)
		, Value(InValue)
		, Source(InSource)
	{
	}

public:
	// Modifier 연산 방식
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Modifier")
	EModifierOperation Operation;

	// Modifier 값
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Modifier")
	float Value;

	// Modifier를 생성한 객체
	UPROPERTY()
	TWeakObjectPtr<UObject> Source;
};

Modifier Usage

static float EvaluateModifiers(float BaseValue, const TArray<FBaseModifier>& Modifiers)
{
	float AddSum = 0.0f;
	float MulProduct = 1.0f;
	TOptional<float> OverrideValue;		

	for (const FBaseModifier& Modifier : Modifiers)
	{
		switch (Modifier.Operation)
		{
		case EModifierOperation::Add:
			AddSum += Modifier.Value;				// 가산값 누적
			break;

		case EModifierOperation::Multiply:
			MulProduct *= (1.0f + Modifier.Value);			// 배율값 누적
			break;

		case EModifierOperation::Override:
			OverrideValue = Modifier.Value;			// 최종값 강제
			break;

		default:
			break;
		}
	}

	if (OverrideValue.IsSet()) // 값의 존재 여부 확인
	{
		return OverrideValue.GetValue();
	}

	// Add -> Multiply 순서로 계산
	return (BaseValue + AddSum) * MulProduct;
}

float BaseAttack = ChampionStatData.AttackDamage;

TArray<FBaseModifier> Modifiers;
Modifiers.Add(FBaseModifier(EModifierOperation::Add, 20.0f, SwordWeapon));
Modifiers.Add(FBaseModifier(EModifierOperation::Multiply, 0.15f, AttackBuff));

float FinalAttack = EvaluateModifiers(BaseAttack, Modifiers);
// 결과: (100 + 20) * (1.0f + 0.15f) = 138

이는 가장 기본적인 Modifier 구현 방식이며, 필요성에 따라

  • Duration(지속시간)
  • Stacking(중첩)

과 같은 개념도 Modifier에 추가할 수도 있다.

최종적으로 이러한 개념을 이용하면

  • 장비 시스템
  • 아이템 사용
  • 버프 / 패시브

등에서 다루는 데이터 개념을 유연하게 처리할 수 있다.

2-1. GAS에서의 Gameplay Effect Modifiers

언리얼의 GAS(Gameplay Ability System)에서도 유사한 개념이 그대로 사용된다.

  • AttributeSet → Base Value와 Instance 역할
  • GameplayEffect Modifier → Modifier 역할
  • Modifier Op (Add / Multiply / Divide / Override)
  • Source / Target Tags → Modifier의 출처와 수명 관리



3. Data Modifier와 Data Instance의 관계

이제 우리는 다음과 같은 흐름을 따라가며, 데이터가 어떻게 안정적으로 관리되고 구조화될 수 있는지 살펴보았다.

  • Data Definition → Data Instance → Modifier

이를 정리하면 다음과 같다.

  • Definition은 변하지 않는 설계 데이터이다.
  • Instance는 런타임에서 변화하는 데이터이다.
  • Modifier는 데이터 변화의 원인이다.

이 세 가지를 명확하게 분리하면, 데이터는 더 이상 단순한 “값”에 머무르지 않는다.
어떤 기준에서 시작되었는지, 현재 어떤 상태인지, 그리고 왜 그렇게 변했는지까지 함께 파악할 수 있는 구조가 된다.

profile
언리얼 엔진 주니어(신입) 개발자 | 소설 쓰는 취준 개발자

1개의 댓글

comment-user-thumbnail
2026년 3월 27일

잘 봤습니다!

답글 달기