* 정의

 

외적(cross product, outer product)은 3차원에서만 정의되는 개념으로 두 3차원 벡터 a와 b의 외적(a × b)은 a, b 모두에 직교인 또 다른 벡터 n로 정의된다.

 

 

- 기본 공식, 사이 각도와 관련된 공식

 

 

 

- 왼손, 오른손 좌표계에서의 방향

 

이때 a, b에 모두 직교인 벡터 n의 방향은 두가지가 있을 수 있는데, 방향 선택은 오른손 좌표계면 오른손 법칙 왼손 좌표계면 왼손 법칙을 사용한다.

 

출처 : https://en.wikipedia.org/wiki/Right-hand_rule
출처 : http://www.f-lohmueller.de/pov_tut/a_geo/a_geo85e.htm

 

 

- 사이 각도

 

a × b = ||a|| ||b|| sinθ n

위와 같은 외적의 공식에서 n은 a,b모두에 직교인 벡터이며, θ는 a, b사이의 각도이다. (0 <= θ <= π)

 

 

  • a, b가 서로 수직하면(θ = π/2 or θ = 3π/2sinθ의 절댓값이 최대가 되어 a × b의 크기가 가장 커진다.
  • a, b가 서로 평행하면(θ = 0 or θ = π) sinθ은 0이되어 a × b = 0이된다.
  • 두 벡터가 직교할 때 절댓값이 가장 크다.

외적의 크기를 비교하면 두 벡터가 얼마나 직교하는 지 확인할 수 있다.

 

 

 

- 외적의 크기

 

외적의 크기는 다음과 같이 정의할 수 있다.

||a × b|| = ||a|| ||b|| |sinθ|

위와 같은 외적의 공식에서 n은 a,b모두에 직교인 벡터이며, θ는 a, b사이의 각도이다. (0 <= θ <= π)

 

기하학적으로 생각해보면 평행사변형의 크기와 같고 두 벡터가 직교할 때 최댓값을 갖는다.

 

 

- 좌표계 관점

 

좌표계에서 직접 어떤 벡터인지 유도할 수 있다.

a = (a1, a2, a3), b = (b1, b2, b3)

(i, j, k)은 양의 직교기저이다. (각각 (1,0,0), (0,1,0), (0,0,1))

아래와 같은 유도과정으로 다음 식을 얻을 수 있다. (왼손좌표계든 오른손좌표계든 둘 다 성립한다.) 

a × b = (a2*b3 - a3*b2, a3*b1 - a1*b3, a1*b2 - a2*b1) != b × a

 

 

 

- 행렬 관점

 

다음과 같이 표현할 수도 있다.

출처 :&amp;amp;amp;amp;amp;nbsp;https://en.wikipedia.org/wiki/Cross_product

 

 

// 2차원 유사 외적 : 2차원에서는 두 벡터에 수직인 벡터가 존재하지 않지만, 하나의 2차원 벡터 U = (ux, uy)에 수직인 벡터 V는 구할 수 있다.

 

 

- 추가 공식

 

1, 3번 공식이 자주 쓰인다.

 

 

* 사용 이유

 

- 게임에서 사용하는 방식

 

게임에서는 두 방향 벡터를 가지고 이와 수직인 벡터를 구해야할 때 사용한다.

 

가장 대표적으로 컴퓨터 그래픽스 분야에서 삼각형의 법선 벡터를 구할 때 사용된다.

 

 

- 사용 사례

 

@ 삼각형의 법선벡터

삼각형의 법선 벡터를 구할 수 있다. 

 

 

@ 회전 시뮬레이션

회전력을 시뮬레이션하기 위해서 토크(돌림힘)를 거쳐야 하는데, 이때 힘에 따라 객체를 회전을 시뮬레이션할 수 있다.

 

 

* 내적과 외적 활용하기

 

문제 1. 플레이어와 몬스터가 존재하고, 플레이어가 바라보는 방향을 v1, 플레이어위치를 a 몬스터 위치를 b, 하늘 방향벡터를 up이라고 할때 플레이어가 몬스터를 바라보기 위해서는 어느방향으로 얼마나 회전해야할까? (단 플레이어와 몬스터가 존재하는 좌표계는 왼손좌표계이다.)

 

 

1.1. 플레이어에서 몬스터로의 방향을 v2 = b - a라고 하자.

 

1.2. v1과 v2를 내적하여 각도를 구한다. 단, 크기만 나오기 때문에 아직 방향은 알 수 없다.

사이의 각도를 θ라고 하면 θ = cos-1( v1 · v2 / ||v1|| ||v2||)

여기서 θ 두 각도 중 더 작은 각도를 의미하기 때문에 최소회전으로 몬스터를 바라볼 수 도록 보장할 수 있다.

 

1.3. 방향을 판별하기 위해서 v1과 v2를 외적하여 그 결과인 n을 up벡터와 내적하여 관계를 알아본다.

n = v1 × v2 = (v1y*v2z - v1z*v2y, v1z*v2x - v1x*v2z, v1x*v2y - v1y*v2x)

 

n · up > 0

 : n과 up이 같은 방향이기 때문에 왼손법칙(왼손좌표계이므로)에 의해서 플레이어가 반시계 방향으로 회전해야한다.

 

n · up < 0

 : n과 up이 다른 방향이기 때문에 왼손법칙(왼손좌표계이므로)에 의해서 플레이어가 시계 방향으로 회전해야한다.

 

 

'수학 > 선형대수학' 카테고리의 다른 글

내적  (0) 2022.11.16
Gimbal lock(짐벌락)  (0) 2022.06.11
사원수  (0) 2021.11.12
여러 변환 정리  (2) 2021.11.12
행렬 대수 정리  (0) 2021.11.12

* 정의

 

점곱(dot product)이라고도 부르는 내적(inner product)은 스칼라값을 내는 벡터 곱셈의 일종이다.

결과가 스칼라기 때문에 스칼라 곱(scalar product)이라고 부르기도 한다.

 

 

- 기본 공식

 

다음 두 벡터가 영벡터가 아닐 때 다음과 같이 정의된다. (위치를 내적하는 것은 의미가 없고, 영벡터와 내적하면 모두 0이 나오기 때문에 각도를 사용할 수 없다.)

 

U = (ux, uy, uz), V = (vx, vy, vz) 일 때, 내적은 다음과 같이 정의된다.

U · V = ux * vx + uy * vy + uz * vz

 

 

- 사이 각도와 관련된 공식

 

기하학적 의미를 확인하기 위해서 제1 코사인법칙 > 제2 코사인법칙 > 내적의 공식을 통해서 새로운 식을 유도할 수 있다. 유도된 식은 다음과 같다.

U · V = ||U|| ||V|| cosθ

 

 

- 사이 각도

 

여기서 두 벡터 사이의 각도 θ는 두 벡터 U와 V로 만들어지는 두 각도 중 더 작은 각도를 의미한다. (0 <= θ <= π)

사이 각도 θ에 대해서 식을 써보면 다음과 같다.

θ = cos-1( U · V / ||U|| ||V||)

 

여기서 ||U||와 ||V||가 0이 아닌 양수이기 때문에 다음과 같은 속성을 가지고 있다.

 

  • U · V = 0 이면 U⊥V이다 (θ = π/2)
  • U · V > 0 이면 (0 <= θ < π/2)을 만족한다.
  • U · V < 0 이면 (π/2 < θ <= π)을 만족한다.
  • 두 벡터가 평행할 때 절댓값이 가장 크다.

내적의 절댓값을 비교하면 두 벡터가 얼마나 평행한 지 확인할 수 있다. 

 

 

 

활용 예시를 하나 들어보면 한 축을 의미하는 벡터에 내적을 했을 때 양수면 해당 축의 양의 영역에 있는 것이고, 음수면 해당 축의 음의 영역이 있다고 판단할 수 있다.

 

간단한 예제를 확인해보면

U = (2, 4, 5), V = (-1, -5, 3)이라고 할 때, U와 V사이의 각도를 구해보자.

θ = cos-1((2 * -1 + 4 * -5 + 5 * 3) / √(2² + 4² + 5²) * √(-1² + -5² + 3²)) = cos-1(-7 / √45 * √35)

 

 

* 사용 이유

 

- 게임에서 사용하는 방식

 

게임에서는 두 방향 벡터를 가지고 그 사이의 각도를 구하기 위해서 사용된다. 위에서 구한 식을 사용한다.

θ = cos-1( U · V / ||U|| ||V||)

 

이를 실제 C++ 코드로 작성해보면 다음과 같다.

const float v1Length = sqrt(v1X * v1X + v1Y * v1Y);
const float v2Length = sqrt(v2X * v2X + v2Y * v2Y);

const float dotValue = v1X * v2X + v1Y * v2Y;

// 소수점으로 계산하기 때문에 조금 초과하거나 모자랄 수 있다.
float cosValue = dotValue / (directionLength * toTargetDirectionLength);
cosValue = clamp(-1.0f, 1.0f, coasValue);

// api에 따라 오일러 각으로 나올 수도 있다.
const float angle = acos(cosValue) * (180.0 / PI);

 

 

- 사용 사례

 

@ AI 시야

AI의 방향과 AI에서 다른 타겟에 대한 방향을 알고 있으면 각도를 구할 수 있고, 이 각도를 적절히 제한함으로써 시야를 구현할 수 있다.

 

 

@ 편도 트리거

트리거는 보통 충돌하면 이벤트를 발생시키지만 트리거에 방향을 앞방향으로 두면, 플레이어가 앞방향으로 진행하고, 트리거에 충돌했을 때만 특정 이벤트를 수행할 수 있다. 예를 들어 동굴을 빠져나와 새로운 곳을 발견했을 때 텍스트로 띄워줄 수 있다. 반대로 동굴에 들어갈 때는 텍스트가 뜨지 않는다.

 

 

@ 객체와 상호 작용

객체(문, 보물 상자 등)의 거리와 플레어가 해당 객체를 바라보는지 내적으로 알 수 있고, 그에 따라 상호 작용 키 UI를 띄워줄 수 있다.

 

 

* 내적과 외적 활용하기

 

문제 1. 플레이어와 몬스터가 존재하고, 플레이어가 바라보는 방향을 v1, 플레이어위치를 a 몬스터 위치를 b, 하늘 방향벡터를 up이라고 할때 플레이어가 몬스터를 바라보기 위해서는 어느방향으로 얼마나 회전해야할까? (단 플레이어와 몬스터가 존재하는 좌표계는 왼손좌표계이다.)

 

 

1.1. 플레이어에서 몬스터로의 방향을 v2 = b - a라고 하자.

 

1.2. v1과 v2를 내적하여 각도를 구한다. 단, 크기만 나오기 때문에 아직 방향은 알 수 없다.

사이의 각도를 θ라고 하면 θ = cos-1( v1 · v2 / ||v1|| ||v2||)

여기서 θ 두 각도 중 더 작은 각도를 의미하기 때문에 최소회전으로 몬스터를 바라볼 수 도록 보장할 수 있다.

 

1.3. 방향을 판별하기 위해서 v1과 v2를 외적하여 그 결과인 n을 up벡터와 내적하여 관계를 알아본다.

n = v1 × v2 = (v1y*v2z - v1z*v2y, v1z*v2x - v1x*v2z, v1x*v2y - v1y*v2x)

 

n · up > 0

 : n과 up이 같은 방향이기 때문에 왼손법칙(왼손좌표계이므로)에 의해서 플레이어가 반시계 방향으로 회전해야한다.

 

n · up < 0

 : n과 up이 다른 방향이기 때문에 왼손법칙(왼손좌표계이므로)에 의해서 플레이어가 시계 방향으로 회전해야한다.

 

 

 

'수학 > 선형대수학' 카테고리의 다른 글

외적  (2) 2022.11.16
Gimbal lock(짐벌락)  (0) 2022.06.11
사원수  (0) 2021.11.12
여러 변환 정리  (2) 2021.11.12
행렬 대수 정리  (0) 2021.11.12

* 효율적인 TArray 사용 방법

 

- Add

 

@ 과정

Add를 호출하면 (내부적으로 Emplace -> AddUninitialized 과정을 수행한다.) 요소를 추가할 수 있으며, 이 과정에서 용량이 부족하다면 Relocation을 진행한다. Relocation은 TArray에 넘겨준 Allocator 타입에 따라서 다양하게 동작한다.(Allocator의 CalculateSlackGrow, ResizeAllocation 함수를 통해 진행된다.)

 

@ Add와 Relocation

Relocation 과정은 새롭게 공간을 할당하고 이전 값들을 새로운 공간에 복사한 후, 이전 공간을 삭제하는 단계로 이루어진다. (성능에 좋지 않다.)

 

Relocation을 진행할 때, 기본 Allocator에서는 배열에 요소가 없는 경우를 제외하고는 여분의 공간을 더 할당한다. 하지만 요소가 몇 개 추가된다면 다시 Relocation 해야 한다.

 

이를 방지하기 위해 요소의 개수를 알고 있다면 그 공간만큼 미리 할당해두면 Relocation 을 한번만 수행할 수 있다. TArray에서도 std::vector와 마찬가지로 Reserve 라는 함수를 제공한다.


이미 TArray에 요소가 존재하여 추가할당해야하는 경우에는 여분의 공간을 더 할당하긴하지만, 요소가 빠르게 추가되면 다시 Relocation을 해야한다. 그리고 너무 당연하지만, 함수의 입력으로 넘겨줄 때 &을 사용하여 새로운 할당을 방지해야한다.

 

 

- Remove

 

@ Remove, RemoveSwap 과 Relocation

Remove 함수는 배열에서 요소를 삭제 후, 뒤에 있는 요소들의 메모리를 이동하여 빈 공간을 채운다. RemoveSwap 함수는 배열에서 순서가 중요하지 않은 경우 사용할 수 있는 함수로, 맨 뒤의 요소와 스왑하여 빈 공간을 채운다.

 

RemoveSwap함수를 사용할 때, 배열의 크기를 줄이기 위해(Shrink) Relocation이 발생할 수 있다. 오래 사용되는 배열의 경우 사용되지 않는 공간을 확보하는 것이 좋기 때문에 필요한 옵션이다. 하지만 짧게 사용되는 배열의 경우 Relocation으로 성능이 나빠질 수 있기 때문에 bAllowShrinking 옵션을 통해 꺼주는 것이 좋다.

 

 

- Allocator : FDefaultAllocator, TInlineAllocator, TFixedAllocator

 

@  FDefaultAllocator(TSizedHeapAllocator<32>)

TArray에 기본적으로 사용되는 Allocator는 FDefaultAllocator(TSizedHeapAllocator<32>)로 이는 동적 메모리 Allocator이다. (힙에 할당) 상황에 따라 동적으로 메모리 할당, 해제하게 된다. 힙 메모리 할당 과정은 lock이 필요하고, 빈 블록 찾는 과정을 거치며, 또 새로운 주소의 공간을 할당하기 때문에 기존에 사용되던 캐시는 전부 버려진다.

 

@  TInlineAllocator

이를 방지하기 위해 Unreal Engine에서는 TInlineAllocator를 제공하고 있다. 이 Allocator를 사용하면 처음 N개의 공간을 미리 할당해 둘 수 있다.(힙에 할당하지 않고, TArray 내부에 포함된다.)

// typedef TArray<FOverlapInfo, TInlineAllocator<3>> TInlineOverlapInfoArray;와 같이 사용된다.

 

만약 사용할 공간을 예측할 수 있다면 힙에 메모리를 할당하는 것을 피할 수 있다. 만약 추가 메모리가 필요한 경우에는 TInlineAllocator의 SecondaryAllocator(FDefaultAllocator)를 통해 ( Secondary가 FDefaultAllocator 인 경우에 힙에) 할당된다. (Relocation 과정을 거친다.)

 

@  TFixedAllocator

TFixedAllocator는 TInlineAllocator와 동일하지만, SecondaryAllocator가 존재하지 않는다.

 

 

* Unreal Engine에서의 UObject 관리 간단 분석

 

- 생성 과정 분석

 

@ 간단 과정

UObject 를 생성할 때 NewObject 함수나 Actor의 경우 SpawnActor를 사용한다. SpawnActor도 내부적으로 NewObject 함수를 호출하고 있다.

내부 코드는 너무 길지만 간단히 일부 과정을 설명하면, 메모리를 적절한 사이즈에 맞게 할당하고 Archetype(Prefab, Template)을 CDO나 Blueprint 로 설정되어 초기화된다. (내부코드까지 자세히 보진 않아서 정확하지는 않다.)

 

그 과정에서 UObjectBase(UObject의 최상위 부모클래스) 생성자를 호출하고, 생성자에서 AddObject 함수를 통해 싱글톤 해시테이블(FUObjectHashTables)과 전역 UObject 배열(GUObjectArray)에 객체를 등록한다.

 

@ FUObjectHashTables
FUObjectHashTables는 다양한 역할을 하고 있지만 주로 클래스 종류별로 UObject를 관리하고 있다. 예를 들어 TActorIterator에서 특정 클래스의 객체를 찾을 때 FUObjectHashTables가 활용된다.

 

@ GUObjectArray
GUObjectArray은 객체의 배열을 가지고 있는 풀 자료구조로 실제 생성된 객체와 할당할 수 있는 공간을 관리한다. 객체를 등록할 때는 일반적으로 비어있는 풀의 인덱스를 가지고 있는 배열에서 인덱스를 받아서 풀에 등록하게 된다. (UObjectBase(UObject의 최상위 부모클래스)의 InternalIndex에 해당 풀 인덱스를 저장하고 있다.)


객체가 풀에 저장될 때는 실제 객체가 저장되지 않고 FUObjectItem 형태로 저장된다. FUObjectItem 객체를 담는 자료구조는 UObjectBase 포인터와 WeakObjectPtr 에서 사용되는 SerialNumber 등을 가지고 있다.

둘 다 전역객체이기 때문에 크리티컬섹션을 통해 데이터 경쟁 상태를 방지한다.

 

 

- TWeakObjectPtr 동작 방식

 

UObject를 생성하면 GUObjectArray이라는 풀에 저장되고 풀의 객체는 FUObjectItem형식이다. FUObjectItem 내부에 SerialNumber는 멤버로 가지고 있는데, 실제 존재하는(사용중인) 객체인 경우에만 다른 객체와 다른 고유한 값을 가진다.


따라서 최초로 TWeakObjectPtr을 할당하게 되면 객체는 고유한 SerialNumber을 FUObjectItem와 TWeakObjectPtr에 저장한다. 이후 TWeakObjecPtr을 통해 해당 객체에 접근할 때, SerialNumber가 일치하는 지 확인한다. (추가 검증 과정이 더 존재한다.)


일치한다면 경우라면 아직 존재하는 객체이고, SerialNumber가 다르다면(다시 풀에 반납할 때 SerialNumber를 0으로 초기화) 널포인터를 리턴한다.

 

 

* Role

 

Unreal Engine에는 리플리케이션과 관련된 Role과 RemoteRole이라는 두 가지 속성이 존재한다. Role은 간단히 현재 이 시스템에서의 역할을 의미하고, RemoteRole은 연결된 다른 쪽의 역할을 의마한다.  이 두 가지 속성을 통해 다음과 같은 사실을 확인할 수 있다.

 

  • 누가 Actor의 권한을 가졌는지 확인할 수 있다.
  • 해당 Actor가 리플리케이션된 Actor인지 확인할 수 있다.
  • 리플리케이션 되는 방식에 대해 알 수 있다.

 

 

- Role의 종류와 Replication

 

기본적으로 Role에는 3가지 종류가 있다. (더 존재하지만, 기본적으로 3가지가 가장 많이 활용된다.)

 

이러한 3가지 종류의 Role은 게임을 실행중인 곳에서 각각 다르며, 이를 활용하여 적절하게 리플리케이션한다. 서버는 대역폭, 성능의 문제로 매 프레임마다 Actor들을 클라이언트로 리플리케이션하지 않기 때문에 서버와 클라이언트의 동기화를 위해서는 Role에 따라 적절한 리플리케이션이 필요하다.

 

 

@ ROLE_Authority 

 : 기본적으로 서버의 모든 Actor의 Role이 ROLE_Authority 로 설정되어 있다. 이로 인해 자연스럽게 클라이언트의 RemoteRole은 ROLE_Authority 가 된다. 가끔 클라이언트에만 스폰되어 존재하는 Actor나 싱글 플레이어 게임에서는 Role 이 ROLE_Authority로 설정된다. 

 

(기본적으로 Actor를 스폰한 곳에서는 Role은 ROLE_Authority 이지만, 클라이언트가 멀티플레이어 게임에서 이 특성을 악용할 수 없다. 클라이언트에서 스폰된 Actor는 서버로 리플리케이션되지 않기 때문이다.)

 

 

@ ROLE_SimulatedProxy

 : 클라이언트에서 직접 컨트롤하지 않고 서버에서 리플리케이션되어 시뮬레이션되는 Actor의 Role이 ROLE_SimulatedProxy 로 설정되어 있다.

 

서버에서의 통보받은 해당 Actor의 마지막 속도 등의 정보를 기반으로 시뮬레이션하고, 보간한다.

 

 

@ ROLE_AutonomousProxy  

 : 클라이언트에서 직접 컨트롤하는 Actor의 Role이 ROLE_AutonomousProxy 로 설정되어 있다. 간단하게 말하면 PlayerController 에 의해 소유되는 Actor 들이고, 사용자의 입력을 받게 된다. 이때 서버의 RemoteRole은 ROLE_AutonomousProxy 로 설정된다.

 

ROLE_SimulatedProxy 와 달리 마지막으로 받은 정보를 기반으로 시뮬레이션, 보간을 하지 않고, 입력을 기반으로 시뮬레이션하게 된다. 훨씬 더 정확한 시뮬레이션이 가능해지며, 컨트롤하고 있는 Actor가 원하는대로 시뮬레이션 될 것이다.

 

 

@ Role과 관련된 함수  

  • HasAuthority 함수를 통해 ROLE_Authority 를 체크할 수 있다. 
  • Pawn::IsLocallyControlled 함수를 사용하면 이 폰이 실제로 로컬에서 컨트롤 되는 지 알 수 있다. 싱글플레이어 게임일 때 true를 반환하고, 멀티플레이어 게임인 경우에는 클라이언트이고 ROLE_AutonomousProxy Role을 가졌을 때나, ROLE_Authority Role와 ROLE_AutonomousProxy RemoteRole을 가졌을 때 true를 반환한다.(현재 다른 플레이어가 컨트롤하고 있지 않고, 시스템이 서버인 경우) 

 

 

* Replication

 

서버의 Actor들을 각 클라이언트로 리플리케이션하기 위한 조건은 다음과 같다.

  • 리플리케이션할 Actor는 SetReplicates(true); 를 호출한다. 폰 종류는 PossessedBy가 호출될 때 알아서 호출된다.
  • 컴포넌트의 경우 SetIsReplicated(true); 를 호출한다.
  • SetReplicateMovement를 사용하여 움직임도 리플리케이션할 수 있다. 리플리케이션 비용이 비싸기 때문에 보통 게임에 영향을 주지 않는 경우에는 꺼두는 편이 좋다.
  • 리플리케이션할 속성을 GetLifetimeReplicatedProps 함수 내부에 추가한다.
  • 리플리케이션할 속성을 UPROPERTY(Replicated) 또는 UPROPERTY(ReplicatedUsing=OnRep_PropertyName)으로 지정한다.

 

- 속성 리플리케이션 세부 설명

 

Actor 들은 리플리케이션되는 속성의 리스트를 가지고 있으며 서버는 속성이 변경되었을 때 클라이언트로 리플리케이션한다. 속성 리플리케이션은 항상 보장되며, MinNetUpdateFrequency, NetUpdateFrequency 를 참조하여 특정 간격마다 가장 최신의 값이 리플리케이션된다. 만약 서버에서 리플리케이션되기 전에 값이 빠르게 변경되었을 때 가장 최종적으로 변경된 값이 리플리케이션된다.

 

 

@ 속성 지정자

속성 지정자를 통해 리플리케이션을 설정할 수 있다. 속성 지정자로 Replicated, ReplicatedUsing을 사용할 수 있으며, ReplicatedUsing 을 사용했을 경우 리플리케이션되었을 때 호출될 함수를 지정할 수 있다. 

 

보통 함수명에 접두사로 OnRep을 붙여서 OnRep_PropertyName의 형태로 많이 사용한다.

바운딩되는 함수는 OnRep_PropertyName는 UFUNCTION 이여야 한다.

 

 

@ 리플리케이션될 속성 반환 함수 오버라이딩

GetLifetimeReplicatedProps 함수를 재정의 하여 어떤 속성을 어디로 리플리케이션할지 결정한다.

 

간단히 리플리케이션할 변수를 지정한다고 생각하면 된다. 이 함수에 다음과 같이 추가하면 다음 변수는 모든 클라이언트에게 리플리케이션된다.

 

다음과 같이 내부에 코드를 작성한다. 모든 클라이언트로 리플리케이션할 수 있고, 조건에 따라 리플리케이션할 수 있다.

  • DOREPLIFETIME(ClassName, PropertyName);
  • DOREPLIFETIME_CONDITION(ClassName, PropertyName, COND_OwnerOnly);

 

@ 속성 리플리케이션 시 주의할 점

  • 서버에서 속성의 값이 변화가 없다면 클라이언트로 리플리케이션하지 않기 때문에 매번 리플리케이션이 필요한 환경에서는 이를 고려해야한다. (내부 최적화 로직)
  • 즉시 리플리케이션이 되지 않아도 될 Actor는 NetUpdateFrequency를 작게 두는 것이 좋다.

 

 

* RPC

 

- RPC 종류

 

RPC에는 크게 3가지 종류가 있다.

  • Server
  • Client
  • NetMulticast

 

이 3가지 함수를 구현하기 위해 적절한 속성지정자를 설정해야 한다. 각 속성 지정자는 다음과 같은 의미를 지닌다.

  • Server : 서버에서 호출되는 함수
  • Client : 클라이언트에서 호출되는 함수
  • NetMulticast :  서버와 서버에 연결된 모든 클라이언트에서 호출되는 함수
  • Reliable : 신뢰성 있는 호출을 보장한다.
  • WithValidation : 보통 서버 함수를 호출하기 전에 직접 구현한 검증 함수를 추가로 호출하여 패킷이 비정상적인 상황인지 확인한다. 만약 검증에 실패했다면 클라이언트를 로그아웃 시킨다. (클라이언트 함수도 선택적으로 사용할 수 있다고 나와있다.)

 

 

- 함수 정의

 

예를 들어 서버 함수를 만드는 방법은 다음과 같다. 필요에 따라 속성 지정자를 설정하면 된다. 보통 함수 이름으로 서버 함수는 Server, 클라이언트 함수는 Client, 멀티캐스트 함수는 Multicast 접두사를 붙인다.

UFUNCTION(Server, Reliable, WithValidation)
void ServerFunctionName()

 

 

- 함수 구현

 

함수명 그대로 구현하지 않고 함수명 뒤에 _Implementation을 붙인 이름의 함수를 구현한다. 

void ClassName::ServerFunctionName_Implementation()
{
	// ...
}

 

WithValidation 속성 지정자를 사용한 경우에 다음과 같이 검증 함수도 구현해야한다. 함수명 뒤에 _Validate 을 붙인 형태다. 만약 false를 반환하게 되면 시스템이 연결을 끊어낸다.

bool ClassName::ServerFunctionName_Validate()
{
	// ...
}

 

 

- 함수 호출

 

실제로 함수를 호출할 때는 구현한 함수명(XXX_Implementation)이 아닌 선언한 함수명을 사용하여 호출한다. (ServerFunctionName(); 와 같이 호출한다.)

 

서버에서 클라이언트 함수를 호출하면 해당 Actor를 소유한 클라이언트만 호출되고, 멀티캐스트 함수를 호출하면 서버와 모든 연결된 클라이언트가 호출된다. 나머지 모든 함수는 서버에서 호출된다.

 

클라이언트에서 서버 함수를 호출하면 해당 클라이언트가 Actor를 소유하고 있는 경우 내부적으로 패킷을 서버로 보내어 서버 함수를 호출한다. 나머지 모든 경우는 해당 클라이언트만 호출된다.

 

 

* 기타(너무 당연한 내용은 제외) - 추후 세부 내용 정리

 

- 게임 관련 상태 정보

 

* GameMode는 서버에만 존재하는 게임에 관련된 정보를 가지고 있는 클래스로 AGameMode는 너무 많은 정보를 가지고 있기 때문에 보통 AGameModeBase를 상속받아 구현한다. (GameMode 도 Actor를 상속 받고 있기 때문에 Tcik 함수를 사용할 수 있다.)

 

* GameMode는 서버에만 존재하기 때문에 클라이언트로 게임의 여러 상태를 리플리케이션해주기 위해 GameState 클래스가 존재한다. 주로 현재 동기화된 게임 시간 등의 정보를 담고 있다.

 

* 플레이어에 대한 개별적인 정보를 관리하는 PlayerState라는 클래스도 존재한다. 클라이언트로 리플리케이션된다. PlayerController가 이미 존재해서 플레이어의 정보들을 여기서 관리해도 될 것 같지만, 클라이언트는 본인의 PlayerController만 가지고 있기 때문에 PlayerState가 필요하다. (서버는 전부 다 볼 수 있다.)

 

 

- 액터

 

* SceneComponent 은 ActorComponent을 상속받은 클래스로 Transform 정보를 가지고 있기 때문에 월드에서 사용할 수 있다. SceneComponent를 상속받은 여러 클래스는 충돌처리를 담당하거나, 시각적인 요소를 담당하기도 한다. 일반적인 ActorComponent와 다르게 계층 구조를 이룰 수 있다.

 

* ActorComponent은 기능만 거의 담당하고 있으며, SceneComponent 계층 구조에 포함되지 않는다.

 

 

- 에디터 활용(블루프린트 등)

 

* 크래시가 발생하면 현재 켜둔 에디터에 VS를 붙여서 확인할 수도 있지만, DebugGame Editor 모드(최적화하지 않아서 조금 더 자세히 확인할 수 있다.)로 바꾸고 VS를 실행하여 새로운 에디터를 열어서 확인할 수도 있다.

 

* EditDefaultOnly를 통해 해당 클래스를 상속받은 블루프린트 클래스에서만 해당 속성이 수정하도록 설정할 수 있는데, 시각적으로 작업해야할 때(Mesh정보의 위치를 조정 등), 빠르게 여러 값을 테스트 해볼 때 유용하게 사용될 수 있다.

 

* 블루프린트는 C++ 의 접근 지정자 공식을 따르지 않는다. 만약 Protected 수준의 접근성을 부여하고 싶다면 다음과 같이 해야한다. UPROPERTY(BlueprintCallable, BlueprintProtected)

 

* 애니메이션 블루프린트의 애님 그래프에서 내부에 존재하는 로컬 변수를 통해 다양한 애니메이션 전이와 효과를 구현할 수 있다.

 

* 애니메이션 블루프린트에 존재하는 로컬 변수는 CPP 코드 상에서 직접 수정해도 되지만, 이벤트 그래프를 이용하여 세팅하여 편하게 갱신시킬 수 있다.

 

* UENUM(BlueprintType) 로 설정하면 블루 프린트에서도 볼 수 있다. 예를 들어 Enum 타입에 따라서 블루프린트를 통해서 UI를 쉽게 컨트롤하기 위해서 사용될 수 있다. (전방 선언시 다음과 같이 사용 enum EnumName : XXX(int8, int32 ...);)

 

* 델리게이트를 쓰거나 이벤트에 바인딩하는 함수는 무조건 UFUNCTION이다. 다이나믹 델리게이트는 블루프린트와 연동할 수 있고, BlueprintAssignable로 설정하면 블루프린트에서 구현할 수 있다.

 

* ClassGroup을 설정하면 커스텀 컴포넌트를 정렬해서 볼수 있기 때문에 편리하다.

 

 

- 수학, 물리

 

* FRotator 나 FQuat 를 사용하여 회전에 대항 방향을 받아올 수 있다.

 

* FQuat를 FRotator 로 반환할 수 있지만 변환하기 위해서 FQuat::Rotator() 함수 내부에서 Atan2를 사용하고 있기 때문에 각도는 [-180, 180]로 제한된다.

 

* FCollisionQueryParams를 적절히 활용하면 LineTrace에 여러 정보를 설정할 수 있다. (AddIgnoredActor : 무시할 액터 결정, bTraceComplex : 메시기반으로 조금 더 정확한 트레이싱 등)

 

* 프로젝트 설정에서 Physical Surface 를 커스텀으로 설정할 수 있다. 이를 설정함으로써 다양한 표면을 정의할 수 있고, 표면에 따라 다양한 효과를 표현할 수 있다.Physical Material 생성해서 Physical Surface 를 설정할 수 있고, 스켈레탈 메시의 Physica Asset으로 가서 각 부위를 Physical Material로 설정할 수 있다.

 

* PhysMaterial를 사용하면 타격 지점의 세부적인 정보를 설정할 수 있다. LineTrace가 적중했을 때, FHitResult에서 PhysicalMaterial의 정보(SurfaceType 등)를 받아올 수 있고, 이에 따라 다양한 처리가 가능하다. (부위 별로 다른 데미지 공식 계산 등)

 

* OnActorBeginOverlap 델리게이트는 액터가 다른 액터와 겹쳐졌을 때 브로드캐스팅된다.(겹쳐지는 조건은 액터의 모든 컴포넌트를 확인하여 PrimitiveComponent를 찾고, 충돌되었는지 확인한다.)

 

* 3차 스플라인 보간을 활용하기 위한 함수를 Unreal Engine에서도 제공한다. (CubicInterp, CubicInterpDerivative) 주로 자연스럽게 경로를 예측하여 연결하기 위해 사용된다.

 

 

- AI

 

* 모든 BehaviorTree 는 BlackBoard 컴포넌트와 함꼐 사용되며, BlackBoard는 내부에 데이터를 가지고 있다.

 

* BehaviorTree에서 데코레이터를 통해 특정 조건을 체크할 수 있으며, 기본적으로 제공하는 NPC, Player 체크 이외에 직접 구현하여 사용할 수 있다. 데코레이터에는 Observer aborts 속성이 존재하고, 다음과 같은 의미를 갖는다.

  • Self : 조건이 만족되었을 때 바로 본인의 서브트리를 빠져나감
  • Lower Priority : 앞의 조건(더 높은 우선순위의 서브트리)이 만족되었을 때 뒤에서 실행되는 서브트리를 빠져나감. 앞에서 설정한다.

 

* BehaviorTree에는 매번 실행되는 서비스라는 것이 존재한다. 예를 들어 서비스를 통해 계속 타겟을 바라보게 할 수 있다.

 

 

- EQS

 

* EQS(Environment Query System) 시스템은 AI와 함께 사용되는 시스템으로, 주변 환경에 대한 여러가지 정보를 얻을 수 있다. 이러한 쿼리는 Unreal Engine의 행동트리에서 노드로 사용 가능하다.

 

이 시스템을 사용하면 주변 환경의 노드 포인트들을 원하는 대로 생성할 수 있고, 조건에 따라 노드를 결정할 수 있다. 조건들은 어떤 액터들에 대한(컨텍스트라고 한다.) 거리나, LineTrace 가능 여부 등 으로 정말 다양하게 설정할 수 있으며, 모든 조건을 확인하고 노드에 조건들에 따라 최종 점수를 산정하여 최적의 노드를 결정한다.

 

이를 활용하면 체력이 얼마 남지 않았을 때 플레이어가 보지 못하는 곳으로 도망치는 AI를 만들 수도 있고, 플레이어가 보지 못하고 5000cm 이상 떨어진 무작위의 노드를 새로운 AI의 스폰지점으로 설정할 수도 있다.

 

하지만 아무리 최적화를 잘했다고 하더라도 조건들이 많아지거나 조건 검사 비용이 비싸지고, 쿼리가 많아짐에 따라 성능에 영향이 갈 수 밖에 없는 구조인 것 같다. 주의해서 사용해야 한다고 생각한다.

 

 

- Material

 

* Material 을 만들고 Material Instance를 만들어서 Mesh에 적용할 수 있다. 이를 활용한 예시를 들어보면 Material 1개 만들어 두고 여러 Material Instance를 만들고 색만 바꿔서 사용할 수 있다. 효과는 같지만 색만 다른 형태로 사용할 수 있다.

 

* Material의 변수를 런타임에 동적으로 변경하고 싶으면 바로 직접 변경해서는 안된다. 직접 변경하게 되면 해당 Material을 사용하는 모든 곳이 다 변경된다. Mesh에서 해당 Material의 MaterialInstanceDynamic를 만들고, 이를 변경해야 한다. (Material이 적절하게 들어가 있는 상황에서 CreateAndSetMaterialInstanceDynamicFromMaterial 함수를 사용한다.)

 

* Material의 내부 변수를 변경하기 위해서 SetScalarParameterValue함수를 많이 사용한다.

 

 

- Sound

 

* 엔진에서 Sound를 사용할 때 넣을 때 실제 파일로부터 Sound Cue를 만들어서 사용한다. 특정 위치에서 플레이할 수도 있고, 특정 컴포넌트에 붙여서 사용할 수도 있다. Sound Cue에 Attenuation(감쇠)라는 것이 있기 때문에 거리에 따라 소리를 미세하게 조절할 수 있다.

 

 

- 기타

 

* 리플리케이션되는 Enum 값을 그대로 넘겨주지 않고, TEnumAsByte로 래핑해서 보내준다.

 

* 멀티플레이어 게임에서 PlayerStart 를 통해 스폰 위치를 결정할 수 있다.

 

* FName은 조회 용도로 많이 사용되고, FText로 변환될 수 있다.

 

* ConsoleVariable 을 사용하면 런타임에 변수를 바꿀 수 있고, 이는 디버깅에 유용하다.

// 치트가 활성화된 경우에만 쓸 수 있다.
int32 DebugWeaponDrawing = 0;

FAutoConsoleVariableRef CVARDebugWeaponDrawing(TEXT("B.DebugWeapons"),
                                            DebugWeaponDrawing,
                                            TEXT("Draw DebugLine for Weapon"),
                                            ECVF_Cheat);

 

 

 

 

* 인벤토리, 장착

 

- 슬롯 관련(장착 슬롯, 인벤토리)

 

* Inventory Gui 부모 객체에 따라 크기 조절되도록 구현
* Inventory Gui 상수 값(CommonConstant)에 따라 자동으로 레이아웃을 만들어서 슬롯을 배치하도록 구현
* Inventory와 Backpack 연결 후 서버, 클라이언트 정보 갱신
* Inventory 정보를 서버, 클라이언트 동기화 되도록 구현

* EquipSlots Gui 부모 객체에 따라 크기 조절되도록 구현
* EquipSlots Gui 상수 값(CommonConstant)에 따라 자동으로 레이아웃을 만들어서 슬롯을 배치하도록 구현
* EquipSlots 정보를 서버, 클라이언트 동기화 되도록 구현
* Inventory, EquipSlots Gui 단축키 바인딩
* 아이템 장착시 리플리케이션 순서 문제 해결
  => 이벤트 도중에 EquipSlots 갱신 패킷을 보냈더니 해당 Tool이 갱신되지 않은 상황
  => 이벤트를 바인딩하여 처리하지 않고 리플레케이션이 완료되면 패킷을 보내 처리하도록 한다.
* Unequip 로직 Equip로직과 비슷한 시퀀스로 변경
* 방어구 장착 시 메시 데이터 본에 붙여서 같이 시뮬레이션 되도록 작업(Tool 하위 구조 설계)

* Tooltip Gui 부모 객체크기에 따라 조절되도록 구현
* Tooltip에서 해당 Tool의 ToolGameData기반으로 출력
* Tooltip에서 슬롯과 Tool 종류에 따라 적절한 버튼이 나올 수 있도록 구현
* Tooltip에서 버튼에 따라 적절한 요청할 수 있도록 버튼에 요청 함수 바인딩
* Tooltip에서 클라이언트에서 Tool 장착 요청 처리
* Tooltip에서 클라이언트에서 Tool 장착해제 요청 처리
* Tooltip에서 클라이언트에서 Tool 선택 요청 처리
* Inventory, EquipSlot 닫을 때 Tooltip도 닫히도록 구현

 


- 공통

 

* EquipSlots, Inventory, PlayerStatisic 등을 위한 서버와 클라이언트에 저장소 확보와 저장소 구조 설계
* 서버 클라이언트 패킷 구현부 분리

 

 

- 영상

 

 

 

- Github

 

https://github.com/HwangJeongIn/ProjectR

 

GitHub - HwangJeongIn/ProjectR: Roblox Battle Royal

Roblox Battle Royal. Contribute to HwangJeongIn/ProjectR development by creating an account on GitHub.

github.com

 

* Lua의 메모리 관리 : 가비지 컬렉터(Garbage Collector)

 

Lua에서는 자동 메모리 관리를 지원한다. Lua는 가비지 컬렉션을 통해 자동으로 메모리를 관리한다. 따라서 메모리를 수동으로 관리하는 관리 과정에서 나타날 수 있는 허상포인터, 메모리 누수에 대해서 자유롭다.

 

다른 가비지 컬렉터과 다르게 Lua의 가비지 컬렉터은 사이클에 대해 안전하기 때문에 순환하는 자료 구조를 구현할 떄 따로 신경쓰지 않아도 된다. 

 

 

- 특정 상황에서의 가비지 컬렉션(Garbage Collection)

 

가비지 컬렉터은 내부 로직에 의해 가비지라고 간주한 메모리만 수집하기 때문에 예상한 것과 다르게 동작할 수 있다.

 

예를 들어 Stack을 구현할 때 예전에 Pop 되었던 값들이 남아 있을 수 있고, 전역 변수의 경우 사용되지 않는 경우라고 해도 삭제되지 않는다. 이 경우 개발자가 명시적으로 nil을 할당하여 해결할 수 있다.

 

프로그램에서 컬렉션을 사용하는 경우 컬렉션에 들어있는 객체는 수집되지 않는다. 예를 들어보면 일반적인 테이블에서는 키나 값이 객체일 경우 strong reference이기 때문에 가비지 컬렉터가 수집하지 못한다. 

 

 

* Weak Table

 

- 가비지 수집을 허용하는 Weak Table

 

Weak Table은 객체를 수집할 수 있도록 하기 위한 테이블이다. Weak Table은 모든 객체에 대한 참조를 weak reference로 수행한다. weak reference는 객체에 대한 참조로 가비지 컬렉터가 고려하지 않는 참조이기 때문에 가비지 컬렉터의 가비지 수집을 방지하지 않는다. 따라서 Weak Table에 존재하는 객체는 가비지 컬렉터가 수집할 수 있다.

 

Weak Table은 키, 값, 키와 값 을 weak reference로 가질 수 있기 떄문에 3종류 존재하며 키나 값이 수집되면 해당 항목이 테이블에서 사라진다.

 

 

- Weak Table의 사용 방법

 

테이블의 필드인 __mode를 통해 Weak Table로 설정할 수 있다. 

    a = {}
    b = {}
    setmetatable(a, b)
    b.__mode = "k"         -- now `a' has weak keys
    key = {}               -- creates first key
    a[key] = 1
    key = {}               -- creates second key
    a[key] = 2
    collectgarbage()       -- forces a garbage collection cycle
    for k, v in pairs(a) do print(v) end
      --> 2

 

객체 타입에만 적용되며 값 타입(정수형 등)은 적용되지 않는다. 문자열 또한 프로그래머 입장에서는 값 타입이기 때문에 동작하지 않는다.

 

* Rojo로 포팅

 

- 특징

 

  • VS Code에서 Rojo 플로그인을 설치하여 Roblox를 연결해서 사용한다.
  • default.project.json를 통해서 로블록스의 폴더와 VS Code의 폴더를 연결시킬 수 있다. 이 정보를 기반으로 VS Code의 코드가 변경되면 로블록스를 갱신할 수 있다.
  • VS Code(서버)에서 에디팅하여 Roblox(클라이언트)에 반영한다. // 양방향 갱신이 가능하긴 하지만 크래시가 발생하는 버그가 있다.
  • 간단한 것들은 .model.json 파일에 클래스의 이름과 이름을 정의하여 추가할 수 있다.
  • Script : .server.lua / LocalScript : .client.lua / ModuleScript : .lua or .json 로 매칭된다.
  • 폴더가 아닌 스크립트 하위에 존재하는 스크립트를 만들기 위해서는 상위 스크립트를 폴더로 만들고 Script / LocalScript / ModuleScript에 따라 init.server.lua / init.client.lua / init.lua을 폴더 내부에 작성하면 된다. 그러면 로블록스에서 각 스크립트로 인식할 수 있다.
  • 폴더에 여러 가지 정보를 넣는 것도 가능한데, init.meta.json을 통해 해당 폴더가 적절한 클래스로 변경하거나 속성 값들을 설정할 수 있다.

 

 

- 완료

 

  • Rojo로 빌드된 프로젝트로 바로 작업할 수 있도록 모든 환경을 포팅
  • 폴리곤 데이터의 집합체(맵, 무기) 등은 파일로 추출하여 Rojo에서 그대로 사용
  • 서버 클라이언트 통신에 쓰이는 RemoteEvents, RemoteValues를 폴더로 정리 < 간단한 값이기 때문에 Rojo의 .model.json 을 사용하여 간단히 정의
  • Gui 구조 정리, 부모 Gui에 따라서 적절하게 크기 변경
  • 스트립트 별로 따로 관리하고 필요에 따라 스크립트를 복제하여 각 객체에 부착 / 가능하면 객체(도구, 무기 등)은 스크립트를 포함하지 않도록
  • 슬롯(인벤토리, 장착 슬롯) 기능을 위한 컨테이너 TArray, TList 작성

 

https://github.com/HwangJeongIn/ProjectR

 

GitHub - HwangJeongIn/ProjectR: Roblox Battle Royal

Roblox Battle Royal. Contribute to HwangJeongIn/ProjectR development by creating an account on GitHub.

github.com

 

 

- 참고 자료

 

https://rojo.space/docs/v0.5/sync-details/

 

Sync Details | Rojo

This page aims to describe how Rojo turns files on the filesystem into Roblox objects.

rojo.space

 

* 중간 완료된 작업(기록용)

 

- 입력

* 클라이언트에 KeyBinder 추가, 특정 키에 대해서 기능을 바인딩할 수 있도록 인터페이스 제공


- 게임 데이터

* 클라이언트 Gui에 모든 정보를 패킷을 통해 받으면 네트워크 성능이 안좋다고 판단하여 어쩔 수 없이 클라이언트에도 일부 게임 데이터 공개, CommonGameDataManager를 공용, ServerGameDataManager를 서버 전용으로 사용


* 클라이언트에서 적절하게 데이터를 읽어서 Gui로 띄울 수 있도록 기본적인 정보는 클라이언트에서 가지고 있도록 결정


* CommonGlobalStorage에서 공용 정보(방어구, 무기 장착 정보 / 인벤토리 정보 / 플레이어 능력치 합산 정보) 제공

  기본적으로 서버의 ServerGlobalStorage에서 갱신된 것을 클라이언트로 통보하는 형식으로 설계


* ClientGlobalStorage에서 QuickSlot 등록, 해제, 퀵슬롯간 스왑 기능 제공
  클라이언트 전용으로 서버는 퀵슬롯을 모르고 클라이언트에서 쿽슬롯에 있는 아이템 사용 요청만 받는다.

 


- UI

* 월드 객체 툴팁 UI(빌보드), 마우스에 타겟팅되는 도구(무기 방어구)의 정보를 띄워주는 기능 구현
* 일반 툴팁 UI 작성 완료
* EquipSlot, Inventroy UI 작성 완료

 

* 로블록스 클라이언트 서버 모델

 

- 로블록스 게임 접속

 

여러 기기에서 게임에 시작할 때 로블록스 컴퓨터에 접속하게 된다. 기기는 클라이언트 로블록스 컴퓨터는 서버다. 

 

 

- 서버 => 클라이언트

 

게임 플레이하는 동안 서버는 지속적으로 클라이언트를 업데이트 한다. 

예를 들어 서버용 스크립트인 Script가 시간을 바꾸면 서버는 클라이언트에게 통보해준다.  

 

 

- 클라이언트 => 서버

 

클라이언트에서 입력이나 제어(채팅 등)가 발생했을 때 클라이언트는 서버에게 업데이트 요청을 보내고 서버는 업데이트를 수행하고 모든 플레이어에게 통보해준다.

 

@ 예시1 : 이동 입력 처리

 

클라이언트에서 입력을 처리하는 과정은 다음과 같다.

  • 게임을 시작하면 로블록스 플레이어 스크립트에는 PlayerModule이라는 것이 생성된다.
  • PlayerModule 내부에 ControlModule이 있고 이 모듈에서 입력 처리도 담당한다.
  • ControlModule 내부에는 Keyboard가 있는데 Keyboard에서 입력을 받아서 MoveVector를 계산하여 보관해둔다.
  • ControlModule의 OnRenderStepped 함수에서 Keyboard에서 미리 계산해 두었던 MoveVector를 사용하여 LocalPlayer의 Move함수를 호출한다.
  • Player.Move는 코드가 가려져있지만 아마 서버에게 MoveVector로 시뮬레이션을 요청하는 코드가 들어가 있을 것이다. 

 

 

@ 예시2 : 채팅

 

  • 게임을 시작하면 로블록스 플레이어 스크립트에는 ChatScript라는 것이 생성된다.
  • ChatScript 내부에 ChatMain가 있고 ChatMain은 채팅 바가 포커스를 잃을 때 chatBarFocusLost라는 함수로 바인딩하여 처리하고 있다.
  • 채팅 바가 포커스를 잃을 때 엔터키가 눌러진 상황이라면 채팅 바에 있는 메시지를 가공한다.
  • 가공된 메시지는 MessageSender에서 MessageSender의 SendMessage를 호출한다.
  • SendMessage에서 ReplicatedStorage에 존재하는 SayMessageRequest 원격 이벤트(RemoteEvent(RPC와 비슷))를 통해서 메시지와 채널을 넣어서 서버로 요청한다.
  • SayMessageRequest 원격 이벤트를 바인딩하고 있는 서버는 해당 정보를 기반으로 채팅을 처리해준다.

 

 

- 클라이언트 코드와 서버 코드 작성

 

@ 클라이언트

클라이언트에서는 플레이어 입력을 추적해야하고, 정보를 특정 플레이어(본인 LocalPlayer)에게 Gui를 통해 보여줘야 한다. 클라이언트 코드는 LocalScript에서 수행된다. 당연히 서버와 관련된 ServerScriptService와 같은 곳에는 LocalScript를 수행할 수 없다.

 

@ 서버

서버에서는 게임 로직, 데이터 저장(DB), 각종 업데이트, 생성 등을 수행하고 모든 클라이언트에게 통보해야한다. 서버 코드는 Script에서 수행된다. 클라이언트와 관련된 PlayerGui와 같은 곳에서 사용할 수 없다.

 

 

- RemoteFunction, RemoteEvent

 

RemoteFunction, RemoteEvent는 각 서버와 클라이언트에서 클라이언트와 서버로 통신하기 위한 수단이다. 서버와 클라이언트에서 모두 접근할 수 있는 ReplicatedStorage에 넣고 해당 이벤트나 함수가 들어왔을 때 어떻게 처리할 것인지 적절하게 함수를 바인딩하면 된다. 다만 클라이언트에서 서버로 올라온 이벤트나 함수는 믿을 수 없기 때문에 한번 검증 후 사용한다. 예를 들어 플레이어가 스킬 사용을 서버에게 요청하면 해당 플레이어가 스킬을 사용할 수 있는 상태(플레이어 상태, 착용한 도구 상태, 소모 자원 상태 등)인지 서버에 있는 데이터 기반으로 검증 후 허가 해야한다.

 

 

 

 

+ Recent posts