2024. 8. 21. 17:30ㆍUnreal Engine
- 직렬화(Serialization):
- 직렬화는 복잡한 객체나 데이터 구조를 연속된 바이트 스트림으로 변환하는 과정입니다. 이 과정은 데이터를 파일에 저장하거나, 메모리 간에 전송하거나, 네트워크를 통해 데이터를 전송할 때 필요합니다.
- 예를 들어, 게임에서 특정 이벤트나 객체 상태를 다른 네트워크 클라이언트에 전송하려면 이 데이터를 직렬화하여 전송할 수 있는 형태로 변환해야 합니다.
- 역직렬화(Deserialization):
- 역직렬화는 직렬화된 바이트 스트림을 다시 원래의 복잡한 객체나 데이터 구조로 복원하는 과정입니다. 네트워크나 파일에서 데이터를 읽어와 다시 원래 형태의 데이터로 복원할 때 필요합니다.
- 네트워크에서 전송된 데이터가 직렬화된 형태로 오면, 이를 다시 원래의 객체로 복원해 게임 로직에서 사용할 수 있게 합니다.
직렬화가 필요한 이유
직렬화는 다음과 같은 이유로 필요합니다:
- 네트워크 통신:
- 멀티플레이어 게임에서는 여러 플레이어 간의 상태를 동기화하기 위해 데이터를 전송해야 합니다. 이 데이터는 주로 객체 형태이지만, 네트워크에서는 바이트 스트림 형식으로만 데이터를 주고받을 수 있습니다. 직렬화는 이러한 객체를 바이트 스트림으로 변환해 네트워크를 통해 전송할 수 있도록 합니다.
- 저장 및 로드:
- 게임 상태를 저장하거나 로드할 때 객체 상태를 파일에 저장해야 합니다. 이 과정에서도 객체를 직렬화해 저장하고, 다시 로드할 때 역직렬화합니다.
- 객체 간의 복사:
- 객체를 메모리에서 다른 메모리 위치로 복사할 때도 직렬화와 역직렬화를 사용할 수 있습니다. 이를 통해 객체의 상태를 복사하거나 이동할 수 있습니다.
FGameplayEffectContext::NetSerialize의 역할
FGameplayEffectContext::NetSerialize 함수는 주로 멀티플레이어 게임에서 FGameplayEffectContext 객체를 네트워크를 통해 전송하기 위해 사용됩니다. 이 객체에는 효과를 적용한 주체(Instigator), 효과를 일으킨 원인(EffectCauser), 적용된 위치(HitResult)와 같은 중요한 정보가 포함됩니다.
- 직렬화 과정:
- 서버나 클라이언트에서 FGameplayEffectContext 객체의 데이터를 바이트 스트림으로 변환합니다. 이 바이트 스트림은 네트워크를 통해 전송할 수 있는 형태로 변환되며, 다른 플레이어에게 전송됩니다.
- 역직렬화 과정:
- 수신 측에서 바이트 스트림을 받아 다시 FGameplayEffectContext 객체로 변환합니다. 이렇게 역직렬화된 데이터는 게임 로직에서 사용되며, 효과가 올바르게 적용되도록 보장합니다.
왜 직렬화와 역직렬화가 중요한가?
- 동기화 유지: 네트워크 게임에서 모든 클라이언트와 서버가 동일한 정보를 가지도록 보장해야 합니다. 직렬화는 데이터를 네트워크를 통해 전송할 수 있는 형태로 변환하고, 역직렬화는 이 데이터를 원래 객체로 복원합니다. 이를 통해 게임 내 이벤트와 상태가 모든 플레이어에게 일관되게 전달됩니다.
- 성능 최적화: 직렬화된 데이터는 최소한의 크기로 압축되어 전송되므로 네트워크 대역폭을 절약하고, 빠른 전송을 가능하게 합니다.
직렬화를 사용했을 때의 성능 향상
- 데이터 크기 감소:
- 직렬화된 데이터는 불필요한 메타데이터, 포인터, 또는 패딩을 제거하고, 데이터만 포함하기 때문에 더 작은 크기를 가집니다. 예를 들어, 직렬화된 객체는 최소한의 메모리만 사용하며, 정규화된 구조로 인해 네트워크 전송 시 더 적은 대역폭을 차지합니다.
- 반면, 직렬화하지 않은 객체는 원래의 메모리 구조와 레이아웃을 그대로 유지하므로, 불필요한 정보가 포함되기 때문에 데이터 크기가 커질 수 있습니다.
- 전송 속도 향상:
- 직렬화된 데이터는 일반적으로 크기가 작기 때문에 네트워크를 통해 더 빠르게 전송될 수 있습니다. 네트워크 통신에서 전송 시간은 데이터 크기에 비례하므로, 직렬화를 통해 데이터 크기를 줄이면 전송 속도가 빨라집니다.
- 예를 들어, 게임에서 수백 명의 플레이어가 동일한 서버에 접속해 있을 때, 각 플레이어의 상태를 직렬화하여 전송하면, 대역폭 사용량을 크게 줄일 수 있습니다. 이는 서버와 클라이언트 간의 통신 지연(Latency)을 줄이는 데도 기여합니다.
- 메모리 및 CPU 사용량 감소:
- 직렬화된 데이터는 메모리에 효율적으로 저장되며, 불필요한 참조나 포인터를 제거하기 때문에 메모리 사용량이 줄어듭니다. 또한, 직렬화 과정에서 데이터가 압축되거나 최적화되면, 이를 처리하는 데 필요한 CPU 리소스도 줄어듭니다.
- 예를 들어, 복잡한 객체 그래프를 직렬화하면, 객체 간의 포인터 참조가 제거되어 메모리 관리가 더 쉬워지고, CPU 캐시 효율이 향상됩니다.
- 데이터 일관성 유지:
- 직렬화는 데이터를 일정한 형식으로 유지하므로, 서로 다른 시스템 간의 데이터 호환성을 보장합니다. 이는 특히 이기종 시스템에서 동일한 데이터를 처리할 때 성능 및 안정성을 향상시킵니다.
- 직렬화를 하지 않고 데이터를 전송할 경우, 시스템 간에 데이터 구조가 다르면 데이터 불일치나 오류가 발생할 수 있습니다.
직렬화를 사용하지 않았을 때의 문제점
- 전송 데이터의 비효율성:
- 직렬화를 사용하지 않고 원본 데이터를 전송하면, 데이터의 크기가 커지고, 전송 시간과 네트워크 대역폭이 증가합니다. 이로 인해 네트워크 지연이 발생하고, 멀티플레이어 게임에서는 플레이어 간 동기화 문제가 발생할 수 있습니다.
- 데이터 해석의 어려움:
- 직렬화되지 않은 데이터는 메모리 레이아웃에 종속적이므로, 다른 시스템에서 데이터를 정확히 해석하기 어렵습니다. 이는 시스템 간 호환성을 저하시켜, 데이터 처리 과정에서 추가적인 복잡성을 초래합니다.
- 메모리 관리의 비효율성:
- 직렬화되지 않은 데이터는 참조 및 포인터를 포함하므로, 네트워크 전송 시 이러한 참조가 유효하지 않게 됩니다. 이로 인해 메모리 누수나 잘못된 데이터 참조가 발생할 수 있으며, 이를 방지하기 위한 추가적인 메모리 관리 비용이 발생합니다.
직렬화하는 데이터
FGameplayEffectContext::NetSerialize 함수는 FGameplayEffectContext 객체와 관련된 여러 데이터를 직렬화합니다. 직렬화되는 데이터의 종류는 다음과 같습니다:
- Instigator (Instigator):
- 이 효과를 유발한 주체입니다. 보통 공격자 또는 능력을 시전한 캐릭터입니다.
- 예: 플레이어가 적에게 화염구를 발사할 때, 플레이어가 Instigator입니다.
- Effect Causer (EffectCauser):
- 실제로 효과를 발생시킨 개체입니다. 이는 Instigator가 소환하거나 생성한 개체일 수 있습니다.
- 예: 플레이어가 소환한 포탑이 적에게 피해를 줄 경우, 포탑이 Effect Causer가 됩니다.
- Ability Class Default Object (AbilityCDO):
- 이 효과가 특정 능력에 의해 발생한 경우, 그 능력의 클래스 기본 객체입니다. 이 정보는 능력의 세부 사항을 참조하는 데 사용됩니다.
- Source Object (SourceObject):
- 효과의 출처로 간주되는 객체입니다. 특정 효과가 어떤 객체에서 유래되었는지 추적하기 위해 사용됩니다.
- Affected Actors (Actors):
- 이 효과의 대상이 된 모든 액터의 리스트입니다. 예를 들어, 폭발 효과가 여러 적에게 피해를 입혔다면, 이 리스트에 그 적들이 포함됩니다.
- Hit Result (HitResult):
- 효과가 발생한 위치와 관련된 충돌 정보입니다. 예를 들어, 총알이 적에게 명중한 경우 그 위치나 충돌 세부 사항을 나타냅니다.
- World Origin (WorldOrigin):
- 효과가 발생한 월드 좌표입니다. 이는 특정 위치에 기반한 효과에서 중요합니다.
CUSTOM FGameplayEffectContext
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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
|
bool FGameplayEffectBaseContext::NetSerialize(FArchive& Ar, UPackageMap* Map, bool& bOutSuccess)
{
uint32 RepBits = 0;
if (Ar.IsSaving())
{
if (bReplicateInstigator && Instigator.IsValid())
{
RepBits |= 1 << 0;
}
if (bReplicateEffectCauser && EffectCauser.IsValid() )
{
RepBits |= 1 << 1;
}
if (AbilityCDO.IsValid())
{
RepBits |= 1 << 2;
}
if (bReplicateSourceObject && SourceObject.IsValid())
{
RepBits |= 1 << 3;
}
if (Actors.Num() > 0)
{
RepBits |= 1 << 4;
}
if (HitResult.IsValid())
{
RepBits |= 1 << 5;
}
if (bHasWorldOrigin)
{
RepBits |= 1 << 6;
}
}
Ar.SerializeBits(&RepBits, 9);
if (RepBits & (1 << 0))
{
Ar << Instigator;
}
if (RepBits & (1 << 1))
{
Ar << EffectCauser;
}
if (RepBits & (1 << 2))
{
Ar << AbilityCDO;
}
if (RepBits & (1 << 3))
{
Ar << SourceObject;
}
if (RepBits & (1 << 4))
{
SafeNetSerializeTArray_Default<31>(Ar, Actors);
}
if (RepBits & (1 << 5))
{
if (Ar.IsLoading())
{
if (!HitResult.IsValid())
{
HitResult = TSharedPtr<FHitResult>(new FHitResult());
}
}
HitResult->NetSerialize(Ar, Map, bOutSuccess);
}
if (RepBits & (1 << 6))
{
Ar << WorldOrigin;
bHasWorldOrigin = true;
}
else
{
bHasWorldOrigin = false;
}
if (Ar.IsLoading())
{
AddInstigator(Instigator.Get(), EffectCauser.Get()); // Just to initialize InstigatorAbilitySystemComponent
}
bOutSuccess = true;
return true;
}
|
cs |
FArchive는 Unreal Engine에서 데이터 직렬화와 역직렬화에 사용되는 중요한 클래스입니다. FArchive 클래스는 데이터의 읽기 및 쓰기 작업을 추상화하여, 데이터를 파일, 메모리, 네트워크 스트림 등에 저장하거나, 그 반대로 이러한 매체에서 데이터를 읽어오는 데 사용됩니다. FArchive는 주로 직렬화와 관련된 작업을 처리하기 때문에, 직렬화 작업의 기반이 되는 클래스라고 할 수 있습니다.
FArchive의 역할
FArchive는 Unreal Engine에서 데이터를 효율적으로 처리하기 위한 다양한 기능을 제공합니다. 주요 역할은 다음과 같습니다:
- 직렬화(Serialization)와 역직렬화(Deserialization):
- FArchive는 객체 데이터를 바이트 스트림으로 변환하거나, 바이트 스트림을 다시 객체 데이터로 복원하는 작업을 수행합니다. 직렬화는 객체 데이터를 바이트 스트림으로 변환하는 과정이며, 역직렬화는 이 바이트 스트림을 다시 원래의 객체로 변환하는 과정입니다.
- 데이터 스트림 관리:
- FArchive는 데이터의 읽기/쓰기 포인터를 관리합니다. 이 포인터는 데이터 스트림에서 현재 읽거나 쓰고 있는 위치를 나타냅니다. 이 기능을 통해 데이터 스트림을 순차적으로 처리할 수 있습니다.
- 추상화된 입출력 작업:
- FArchive 클래스는 데이터를 어떻게 저장하거나 읽을지에 대한 구체적인 구현을 추상화합니다. 예를 들어, 데이터가 메모리에 저장되는지, 파일에 저장되는지, 또는 네트워크를 통해 전송되는지는 FArchive의 구체적인 구현체에 의해 결정됩니다.
FArchive& Ar에 데이터가 저장되는 방식
FArchive는 다양한 데이터 형식을 지원하며, 기본적으로 연속된 바이트 스트림에 데이터를 쓰거나 그 스트림에서 데이터를 읽습니다. Ar는 이 클래스의 참조(reference)로, 네트워크 직렬화 함수에서 데이터를 직렬화하거나 역직렬화할 때 사용됩니다.
1. 쓰기(직렬화) 작업
- 기본 데이터 타입 저장:
- Ar << Value와 같은 연산자를 사용하여 정수, 부동 소수점, 문자 등 기본 데이터 타입을 바이트 스트림으로 직렬화합니다.
- 예를 들어, Ar << Instigator;는 Instigator 변수를 바이트 스트림에 기록하는 것입니다.
- 객체 저장:
- 객체나 복잡한 데이터 구조도 Ar << MyObject;와 같은 방법으로 직렬화할 수 있습니다. 이 경우, 객체의 직렬화 함수가 호출되어 객체의 멤버 변수들이 순차적으로 바이트 스트림에 기록됩니다.
- 비트 단위 저장:
- Ar.SerializeBits(&RepBits, 9);와 같은 함수는 데이터를 비트 단위로 저장합니다. 이는 데이터를 효율적으로 저장하고, 필요한 최소한의 비트만 사용하여 전송하려는 목적이 있습니다.
2. 읽기(역직렬화) 작업
- 기본 데이터 타입 읽기:
- Ar << Value와 같은 연산자는 역직렬화할 때도 사용됩니다. 이 연산자는 바이트 스트림에서 데이터를 읽어와 변수에 저장합니다. 역직렬화는 직렬화의 반대 과정으로, 데이터를 바이트 스트림에서 꺼내어 원래의 객체로 복원합니다.
- 객체 읽기:
- 객체 역시 Ar << MyObject;와 같은 방식으로 역직렬화됩니다. 이때, 객체의 멤버 변수들은 바이트 스트림에서 읽어온 데이터를 통해 원래의 값으로 복원됩니다.
- 비트 단위 읽기:
- Ar.SerializeBits(&RepBits, 9);는 비트 단위로 데이터를 읽어옵니다. 읽어온 데이터는 RepBits 변수에 저장되며, 이후 조건에 따라 필요한 데이터를 역직렬화합니다.
FArchive의 중요성
FArchive는 Unreal Engine의 직렬화 시스템의 중심이 되는 클래스입니다. 특히 멀티플레이어 게임에서 네트워크를 통해 객체 상태를 동기화하거나, 게임 상태를 저장/로드할 때, 데이터의 직렬화 및 역직렬화는 매우 중요합니다. FArchive는 이 모든 작업을 추상화하고, 다양한 저장 매체 및 전송 경로에서 데이터를 일관되게 처리할 수 있도록 도와줍니다.
결론적으로, FArchive& Ar는 네트워크 또는 파일 시스템과 같은 외부 소스로 데이터를 읽고 쓰는 데 사용되는 데이터 스트림을 관리하는 중요한 객체입니다. 이 객체를 통해 FGameplayEffectBaseContext::NetSerialize와 같은 함수에서 데이터의 직렬화 및 역직렬화가 이루어지며, 이를 통해 네트워크 상에서 데이터 동기화를 유지할 수 있습니다.
1. RepBits 변수 초기화 및 설정
1
|
uint32 RepBits = 0;
|
cs |
RepBits는 직렬화할 데이터의 비트를 나타내는 32비트 변수입니다. 이 변수는 어떤 데이터가 직렬화되어야 하는지를 나타내는 플래그 역할을 합니다.
2. 데이터를 직렬화하기 위한 비트 설정 (Saving 모드)
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
|
if (Ar.IsSaving())
{
if (bReplicateInstigator && Instigator.IsValid())
{
RepBits |= 1 << 0;
}
if (bReplicateEffectCauser && EffectCauser.IsValid())
{
RepBits |= 1 << 1;
}
if (AbilityCDO.IsValid())
{
RepBits |= 1 << 2;
}
if (bReplicateSourceObject && SourceObject.IsValid())
{
RepBits |= 1 << 3;
}
if (Actors.Num() > 0)
{
RepBits |= 1 << 4;
}
if (HitResult.IsValid())
{
RepBits |= 1 << 5;
}
if (bHasWorldOrigin)
{
RepBits |= 1 << 6;
}
}
|
cs |
- Ar.IsSaving()은 직렬화 모드인지 확인합니다. 직렬화가 이루어질 때(Ar.IsSaving()이 true일 때), 각 조건에 따라 RepBits 변수의 특정 비트를 설정합니다.
- 각 비트는 특정 데이터가 유효할 때만 설정되며, 해당 데이터가 직렬화되도록 합니다.
- Instigator: 이 효과를 유발한 주체를 나타냅니다.
- EffectCauser: 효과를 발생시킨 원인입니다.
- AbilityCDO: 능력 클래스의 기본 객체입니다.
- SourceObject: 효과의 출처 객체입니다.
- Actors: 이 효과의 대상이 된 액터들입니다.
- HitResult: 효과가 발생한 위치의 충돌 정보입니다.
- WorldOrigin: 효과가 발생한 월드 좌표입니다.
3. 비트 필드의 직렬화
1
|
Ar.SerializeBits(&RepBits, 9);
|
cs |
설정된 RepBits를 9비트로 직렬화합니다. 이 과정에서 직렬화할 데이터의 종류를 네트워크로 전송합니다
4. 데이터를 직렬화 또는 역직렬화
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
35
36
37
38
39
40
|
if (RepBits & (1 << 0))
{
Ar << Instigator;
}
if (RepBits & (1 << 1))
{
Ar << EffectCauser;
}
if (RepBits & (1 << 2))
{
Ar << AbilityCDO;
}
if (RepBits & (1 << 3))
{
Ar << SourceObject;
}
if (RepBits & (1 << 4))
{
SafeNetSerializeTArray_Default<31>(Ar, Actors);
}
if (RepBits & (1 << 5))
{
if (Ar.IsLoading())
{
if (!HitResult.IsValid())
{
HitResult = TSharedPtr<FHitResult>(new FHitResult());
}
}
HitResult->NetSerialize(Ar, Map, bOutSuccess);
}
if (RepBits & (1 << 6))
{
Ar << WorldOrigin;
bHasWorldOrigin = true;
}
else
{
bHasWorldOrigin = false;
}
|
cs |
- RepBits의 각 비트를 검사하여, 해당 데이터가 직렬화되거나 역직렬화되는지 결정합니다.
- Instigator, EffectCauser, AbilityCDO, SourceObject는 단순히 직렬화/역직렬화됩니다.
- Actors는 SafeNetSerializeTArray_Default 함수를 사용하여 배열 형태로 직렬화/역직렬화됩니다.
- HitResult는 유효성을 검사한 후 직렬화/역직렬화합니다. 역직렬화 중인 경우(Ar.IsLoading())에 메모리를 초기화합니다.
- WorldOrigin은 직렬화/역직렬화되며, 해당 값이 존재하는지 여부에 따라 bHasWorldOrigin 플래그가 설정됩니다.
5. Instigator 및 EffectCauser 초기화 (Loading 모드)
1
2
3
4
|
if (Ar.IsLoading())
{
AddInstigator(Instigator.Get(), EffectCauser.Get()); // Just to initialize InstigatorAbilitySystemComponent
}
|
cs |
1
2
3
4
5
6
7
8
9
|
if (RepBits & (1 << 7))
{
Ar << bIsStun;
}
if (RepBits & (1 << 8))
{
Ar << bIsCasting;
}
}
|
cs |
커스텀시에 원하는 데이터를 만들어 이런식으로 직렬화 할 수 있습니다.
요약
객체의 여러 구성 요소를 네트워크를 통해 직렬화/역직렬화합니다. 직렬화는 멀티플레이어 게임에서 클라이언트와 서버 간의 효과 데이터 동기화를 위해 필수적인 과정이며, 이 코드에서는 RepBits를 사용해 직렬화할 데이터를 효율적으로 관리하고, 필요한 데이터만 네트워크로 전송할 수 있게 합니다. 이로 인해 네트워크 대역폭을 절약하고 성능을 최적화할 수 있습니다.
'Unreal Engine' 카테고리의 다른 글
UnrealEngine5 - UInterface, IInterface (0) | 2024.08.28 |
---|---|
Unreal Engine5 - Interface (2) | 2024.08.28 |
UE(GAS) - DECLARE_ATTRIBUTE_CAPTUREDEF(), DEFINE_ATTRIBUTE_CAPTUREDEF() (0) | 2024.08.20 |
UE(GAS) - UGameplayEffectExecutionCalculation (0) | 2024.08.20 |
UE(GAS) - IsNetMode() (0) | 2024.08.20 |