오늘의 할일
1. 코드카타 정리
2. 세션 정리
문제 설명
숫자나라 기사단의 각 기사에게는 1번부터 number까지 번호가 지정되어 있습니다. 기사들은 무기점에서 무기를 구매하려고 합니다.
각 기사는 자신의 기사 번호의 약수 개수에 해당하는 공격력을 가진 무기를 구매하려 합니다. 단, 이웃나라와의 협약에 의해 공격력의 제한수치를 정하고, 제한수치보다 큰 공격력을 가진 무기를 구매해야 하는 기사는 협약기관에서 정한 공격력을 가지는 무기를 구매해야 합니다.
예를 들어, 15번으로 지정된 기사단원은 15의 약수가 1, 3, 5, 15로 4개 이므로, 공격력이 4인 무기를 구매합니다. 만약, 이웃나라와의 협약으로 정해진 공격력의 제한수치가 3이고 제한수치를 초과한 기사가 사용할 무기의 공격력이 2라면, 15번으로 지정된 기사단원은 무기점에서 공격력이 2인 무기를 구매합니다. 무기를 만들 때, 무기의 공격력 1당 1kg의 철이 필요합니다. 그래서 무기점에서 무기를 모두 만들기 위해 필요한 철의 무게를 미리 계산하려 합니다.
기사단원의 수를 나타내는 정수 number와 이웃나라와 협약으로 정해진 공격력의 제한수치를 나타내는 정수 limit와 제한수치를 초과한 기사가 사용할 무기의 공격력을 나타내는 정수 power가 주어졌을 때, 무기점의 주인이 무기를 모두 만들기 위해 필요한 철의 무게를 return 하는 solution 함수를 완성하시오.
#include <string>
#include <vector>
using namespace std;
int solution(int number, int limit, int power) {
int answer = 0; // 필요한 철의 총 무게
for (int i = 1; i <= number; i++) {
int divisor_count = 0; // 이번 기사의 약수 개수 (무기 공격력)
// 약수 개수 구하기 (제곱근까지만 검사)
for (int j = 1; j * j <= i; j++) {
// 나누어 떨어지면 j는 약수
if (i % j == 0) {
divisor_count++;
if (j * j < i) {
divisor_count++;
}
}
}
// 무기 공격력 제한(limit) 검사
if (divisor_count > limit) {
// 제한수치를 초과하면 공격력 더하기
answer += power;
} else {
// 제한을 넘지 않으면 원래 자기 공격력만큼 철 더하기
answer += divisor_count;
}
}
return answer;
}
AI가 작성한 코드
#include <vector>
using namespace std;
int solution(int number, int limit, int power) {
int answer = 0;
// 1. 모든 기사의 번호(1~number)에 대한 약수 개수를 저장할 배열을 0으로 초기화합니다.
vector<int> divisors(number + 1, 0);
// 2. '배수'를 건너뛰며 약수 개수를 한 번에 누적합니다. (에라토스테네스의 체 응용)
for (int i = 1; i <= number; i++) {
// i의 배수들은 무조건 i를 약수로 가집니다.
// 1씩 증가하지 않고 i만큼 크게 크게 점프(j += i)하며 카운트를 올립니다.
for (int j = i; j <= number; j += i) {
divisors[j]++;
}
}
// 3. 완성된 약수 개수 배열을 순회하며 정답을 계산합니다.
for (int i = 1; i <= number; i++) {
if (divisors[i] > limit) {
answer += power;
} else {
answer += divisors[i];
}
}
return answer;
}
발상의 전환: "내 약수가 뭐지?" -> "나는 누구의 약수지?"
기존 코드처럼 한 명씩 붙잡고 약수를 찾으려면, 10만 명을 대상으로 매번 제곱근 연산과 사칙연산을 수행해야 합니다. 하지만 이 코드는 시점을 완전히 뒤집습니다.
i = 2일 때: 2, 4, 6, 8... 100,000 등 2의 배수들 자리로 큼직하게 점프(j += i)하며 "나 2인데, 너희들 약수에 나 추가해!" 하고 방문 도장(divisors[j]++)만 찍고 빠집니다.
시간 복잡도 의 고속도로 연산
안쪽 for문의 실행 횟수를 수학적으로 계산해 보면 N/1 + N/2 + N/3 ... + N/N이 되며, 이는 조화수열의 합 공식에 의해 이라는 경이로운 속도를 냅니다. 언리얼 엔진 같은 환경에서 수많은 객체의 스탯을 일괄 계산할 때, 이처럼 무거운 사칙연산(%) 대신 단순 배열 접근과 덧셈 연산으로 구조를 바꾸는 것이 프레임(FPS) 방어의 핵심입니다.
문제 설명
로또 6/45(이하 '로또'로 표기)는 1부터 45까지의 숫자 중 6개를 찍어서 맞히는 대표적인 복권입니다. 아래는 로또의 순위를 정하는 방식입니다.
| 순위 | 당첨 내용 |
|---|---|
| 1 | 6개 번호가 모두 일치 |
| 2 | 5개 번호가 일치 |
| 3 | 4개 번호가 일치 |
| 4 | 3개 번호가 일치 |
| 5 | 2개 번호가 일치 |
| 6(낙첨) | 그 외 |
로또를 구매한 민우는 당첨 번호 발표일을 학수고대하고 있었습니다. 하지만, 민우의 동생이 로또에 낙서를 하여, 일부 번호를 알아볼 수 없게 되었습니다. 당첨 번호 발표 후, 민우는 자신이 구매했던 로또로 당첨이 가능했던 최고 순위와 최저 순위를 알아보고 싶어 졌습니다.
알아볼 수 없는 번호를 0으로 표기하기로 하고, 민우가 구매한 로또 번호 6개가 44, 1, 0, 0, 31 25라고 가정해보겠습니다. 당첨 번호 6개가 31, 10, 45, 1, 6, 19라면, 당첨 가능한 최고 순위와 최저 순위의 한 예는 아래와 같습니다.
당첨 번호 31 10 45 1 6 19 결과
최고 순위 번호 31 0→10 44 1 0→6 25 4개 번호 일치, 3등
최저 순위 번호 31 0→11 44 1 0→7 25 2개 번호 일치, 5등
순서와 상관없이, 구매한 로또에 당첨 번호와 일치하는 번호가 있으면 맞힌 걸로 인정됩니다.
알아볼 수 없는 두 개의 번호를 각각 10, 6이라고 가정하면 3등에 당첨될 수 있습니다.
3등을 만드는 다른 방법들도 존재합니다. 하지만, 2등 이상으로 만드는 것은 불가능합니다.
알아볼 수 없는 두 개의 번호를 각각 11, 7이라고 가정하면 5등에 당첨될 수 있습니다.
5등을 만드는 다른 방법들도 존재합니다. 하지만, 6등(낙첨)으로 만드는 것은 불가능합니다.
민우가 구매한 로또 번호를 담은 배열 lottos, 당첨 번호를 담은 배열 win_nums가 매개변수로 주어집니다. 이때, 당첨 가능한 최고 순위와 최저 순위를 차례대로 배열에 담아서 return 하도록 solution 함수를 완성해주세요.
#include <string>
#include <vector>
using namespace std;
vector<int> solution(vector<int> lottos, vector<int> win_nums) {
vector<int> answer;
int zero_count = 0; // 알아볼 수 없는 번호(0)의 개수
int match_count = 0; // 당첨 번호와 일치하는 개수
for (int i = 0; i < lottos.size(); i++) {
// 번호가 지워졌다면(0) zero_count++
if (lottos[i] == 0) {
zero_count++;
}
// 번호가 보인다면 당첨 번호와 하나씩 비교
else {
for (int j = 0; j < win_nums.size(); j++) {
if (lottos[i] == win_nums[j]) {
match_count++;
break;
}
}
}
}
int rank[7] = {6, 6, 5, 4, 3, 2, 1};
// 최저 등수: 지워진 번호가 전부 꽝일 때 (확실하게 맞춘 개수만 인정)
int min_rank = rank[match_count];
// 최고 등수: 지워진 번호가 기적처럼 전부 당첨 번호였을 때
int max_rank = rank[match_count + zero_count];
answer.push_back(max_rank);
answer.push_back(min_rank);
return answer;
}
AI가 작성한 코드
#include <vector>
using namespace std;
vector<int> solution(vector<int> lottos, vector<int> win_nums) {
// 1. 당첨 번호(1~45)를 O(1)로 단번에 조회하기 위한 체크용 배열입니다.
bool is_win[46] = {false};
for (int win : win_nums) {
is_win[win] = true;
}
int zero_count = 0;
int match_count = 0;
// 2. 이중 for문을 완전히 없애고, 단일 루프로 탐색합니다.
for (int num : lottos) {
if (num == 0) {
zero_count++;
} else if (is_win[num]) {
// 안쪽 for문으로 일일이 찾지 않고, is_win 배열을 찔러서 단번에 당첨 여부를 확인합니다.
match_count++;
}
}
// 3. 프로그램이 실행될 때 메모리에 아예 고정되어 버리는 constexpr 상수 배열을 씁니다.
constexpr int rank[] = {6, 6, 5, 4, 3, 2, 1};
// 4. push_back 연산(동적 할당)을 쓰지 않고, 중괄호 초기화 리스트로 즉시 배열을 생성해 반환합니다.
return {rank[match_count + zero_count], rank[match_count]};
}
배열을 해시 테이블처럼 사용 (O(N) 최적화):
로또 번호가 1부터 45까지라는 점을 이용해 is_win이라는 46칸짜리 출석부를 만들었습니다. 당첨 번호를 미리 true로 체크해 두면, 내가 가진 로또 번호가 당첨인지 확인할 때 안쪽 for문을 돌며 하나씩 비교할 필요 없이 is_win[num]으로 한 방에 확인(조회 시간 O(1))이 끝납니다. 반복문 안에 반복문이 있던 구조를 평평하게 펴버린 것입니다.
push_back 소멸과 return {...}:
vector에 push_back으로 데이터를 넣을 때마다 컴퓨터는 내부적으로 "공간이 충분한가? 모자라면 더 큰 공간으로 이사해야지"라는 무거운 연산을 수행합니다. 하지만 정답이 무조건 2개(최고 등수, 최저 등수)라는 것을 알고 있으므로, return {최고, 최저}; 형태의 초기화 리스트(Initializer List)를 사용해 불필요한 사이즈 체크와 이사 과정을 완전히 생략했습니다.
constexpr 컴파일 타임 상수:
등수 변환 배열인 rank 앞에 constexpr을 붙임으로써, 함수가 수만 번 호출되더라도 배열을 메모리에 새로 만들지 않고 컴파일된 실행 파일 자체에 데이터가 박혀있게 만들어 극한의 효율을 뽑아냈습니다.
-> Level에는 WorldSettings 속성이 있고, WorldSettings에는 GameMode와 GameState 정보가 있음.
이를 통해 GameMode와 GameState 액터를 생성.
중요한 것은 GameMode 액터는 전체 컴퓨터에서 딱 한 곳(Server)에만 존재.
그래서 GameMode와 Server를 동일시해도 됨.
-> 클라이언트는 서버의 IP 주소와 포트번호로 접속 시도. 서버는 접속 시도하는 클라이언트에게 Level 정보를 넘김.
클라이언트도 해당 Level을 열고, Level을 여는데 성공했다고 데디 서버에게 알립니다. (패킷 주고받음)
-> Level을 여는데 성공한 클라이언트 전용 PlayerState, PlayerController, PlayerCharacter가 서버에 생성.
이것이 다시 Client1에 복제. GameState도 복제.
게임모드는 서버에만 있기 때문에 복제 안됨.
-> 또 다른 클라이언트도 접속. 마찬가지로 서버는 접속 시도하는 또 다른 클라이언트에게 Level 정보를 넘김.
클라이언트도 해당 Level을 열고, Level을 여는데 성공했다고 데디 서버에게 알림
-> Client2 전용 PlayerState, PlayerController, PlayerCharacter가 서버에 생성.
이것이 다시 Client2에 복제. GameState도 복제.
이때, 클라이언트 간의 PlayerState와 PlayerCharacter도 복제되면서 서로가 보이게 됨
이때, 클라이언트 컨트롤러들이 데디케이트 서버에 모두 있지만 각 클라이언트 공인에는 본인 것만 있음.`
Ctrl + T -> 솔루션 간편 찾기
기존 로직의 문제점 : 위젯 객체는 Owning Client에서만 생성되어야함 (각 플레이어의 UI 개별화)
-> Owning Client를 판별 -> IsLocalController() 함수를 호출
// CXPlayerController.cpp의 BeginPlay()
if (IsLocalController() == false)
{
return;
}
Editor Preferences에서 Allow Late Joining 체크(Add another cliner 버튼으로 클라이언트 추가 가능), Always on top 체크(새 클라이언트 화면이 항상 위에 뜸), Multiplayer Options[Launch Seperate Server 체크, Run Under One Process 체크 해제, Net Mode -> Play as Client로 설정], 플레이 버튼 우측에 점 세개 기호 클릭 -> Number of Players에 2를 설정
NetMode : 해당 게임 프로세스가 네트워크 상에서 어떤 역할을 하고 있는지
예시) 데미지를 입어서 HP가 줄어드는 로직
-> 클라이언트 컴퓨터에서 한다면 해킹에 취약해짐. 따라서 데미지 처리 로직은 서버 컴퓨터에서만 처리. (중요한 로직은 서버, 그다지 안 중요한 로직은 클라)
작성하고 있는 로직이 서버에서 돌고 있는지 클라이언트에서 돌고 있는지를 알아야 하는데 그때 필요한 것이 NetMode!
같은 코드가 여러 PC(서버 PC, 내 PC, 다른 사람의 PC)에서 동작
NetMode : 월드 클래스에 들어있는 속성 값
NetRole : 액터 클래스에 들어있는 속성 값
-> 단위의 차이
// World.cpp
ENetMode UWorld::InternalGetNetMode() const
{
if ( NetDriver != NULL ) // 넷드라이버가 설정되어 있으면
{
const bool bIsClientOnly = IsRunningClientOnly();
return bIsClientOnly ? NM_Client : NetDriver->GetNetMode(); // 클라 아니면 서버 둘 중 하나
}
}
// UNetDriver.cpp
ENetMode UNetDriver::GetNetMode() const
{
#if WITH_EDITOR
if (World && World->WorldType == EWorldType::PIE && IsServer())
{
FWorldContext* WorldContext = GEngine->GetWorldContextFromWorld(World);
if (WorldContext && WorldContext->RunAsDedicated) // 데디로 실행했다면
{
return NM_DedicatedServer; // 데디 서버 반환.
}
}
#endif
return (IsServer() ? (GIsClient ? NM_ListenServer : NM_DedicatedServer) : NM_Client);
// 서버인데, 클라이언트로 참여하고 있다? 리슨서버. 서버인데 참여하고 있지 않다? 데디서버.
}
// UNetDriver.cpp
bool UNetDriver::IsServer() const
{
// Client connections ALWAYS set the server connection object in InitConnect()
// @todo ONLINE improve this with a bool
return ServerConnection == NULL;
// 클라이언트는 무조건 서버 커넥션을 가지고 있음
}
서버에 클라이언트가 접속하면 서버에는 ClientConnection 객체가 추가, 반대로 클라이언트에는 ServerConnection 객체가 생성 -> 두 PC는 UNetConnection 객체를 통해 통신
UNetDriver는 생성된 UNetConnection 객체를 소유하고 관리
서버 PC에 생성된 UNetDriver는 접속한 클라이언트의 수만큼 UNetConnection을 관리(따라서 클라이언트 커넥션 관리)
클라이언트 간에 커넥션 절대 불가!
클라이언트 PC에 생성된 UNetDriver는 ServerConnection 단 하나 관리
// UNetDriver.h
UCLASS(...)
class UNetDriver : public UObject, public FExec
{
/** Connection to the server (this net driver is a client) */
UPROPERTY()
TObjectPtr<class UNetConnection> ServerConnection;
/** Array of connections to clients (this net driver is a host) - unsorted, and ordering changes depending on actor replication */
UPROPERTY()
TArray<TObjectPtr<UNetConnection>> ClientConnections;
}
NetDriver -> 멀티플레이어 할거야 하면 NetDriver부터 깔아야함 (멀티플레이어 시작한다의 대명사)
데디케이트 서버 경우, 서버 커넥션이 Nullptr이다. 다만 클라이언트 커넥션은 2개.
클라이언트 서버 경우, 서버 커넥션이 Nullptr이 아닌데, 클라이언트 커넥션은 없다.
// AActor.cpp
UNetConnection* AActor::GetNetConnection() const
{
return Owner ? Owner->GetNetConnection() : nullptr;
// 여기서 오너는 액터/폰/컨트롤러 등등이 될 수 있음.
// 코드에서 볼 수 있듯이 Owner가 지정되어 있지 않으면 통신이 안되게끔 되어 있음.
}
class UNetConnection* APawn::GetNetConnection() const
{
// if have a controller, it has the net connection
if ( Controller ) // 컨트롤러가 있다면
{
return Controller->GetNetConnection(); // 컨트롤러의 GetNetConnection()을 호출.
}
return Super::GetNetConnection(); // 없다면 AActor::GetNetConnection() 함수 호출.
}
UNetConnection* APlayerController::GetNetConnection() const
{
// A controller without a player has no "owner"
return (Player != NULL) ? NetConnection : NULL;
// 플레이어가 존재한다면 NetConnection 객체를 반환.
}
// ChatX.h
#pragma once
#include "CoreMinimal.h"
class ChatXFunctionLibrary
{
public:
// 우리만의 PrintString 만들기
static void MyPrintString(const AActor* InWorldContextActor, const FString& InString, float InTimeToDisplay = 1.f, FColor InColor = FColor::Cyan)
{
if (IsValid(GEngine) == true && IsValid(InWorldContextActor) == true)
{
if (InWorldContextActor->GetNetMode() == NM_Client || InWorldContextActor->GetNetMode() == NM_ListenServer)
{
GEngine->AddOnScreenDebugMessage(-1, InTimeToDisplay, InColor, InString);
}
else // 데디케이트 서버를 위한 것
{
UE_LOG(LogTemp, Log, TEXT("%s"), *InString);
}
}
}
static FString GetNetModeString(const AActor* InWorldContextActor)
{
FString NetModeString = TEXT("None");
// 서버인지 아닌지 판단
if (IsValid(InWorldContextActor) == true)
{
ENetMode NetMode = InWorldContextActor->GetNetMode();
if (NetMode == NM_Client)
{
NetModeString = TEXT("Client");
}
else
{
if (NetMode == NM_Standalone)
{
NetModeString = TEXT("StandAlone");
}
else
{
NetModeString = TEXT("Server");
}
}
}
return NetModeString;
}
};
// CXPlayerController.cpp
void ACXPlayerController::PrintChatMessageString(const FString& InChatMessageString)
{
//UKismetSystemLibrary::PrintString(this, ChatMessageString, true, true, FLinearColor::Red, 5.0f);
FString NetModeString = ChatXFunctionLibrary::GetNetModeString(this);
FString CombinedMessageString = FString::Printf(TEXT("%s: %s"), *NetModeString, *InChatMessageString);
ChatXFunctionLibrary::MyPrintString(this, CombinedMessageString, 10.f);
}
// CXPlayerController.cpp
void ACXPlayerController::PrintChatMessageString(const FString& InChatMessageString)
{
//FString NetModeString = ChatXFunctionLibrary::GetNetModeString(this);
//FString CombinedMessageString = FString::Printf(TEXT("%s: %s"), *NetModeString, *InChatMessageString);
//ChatXFunctionLibrary::MyPrintString(this, CombinedMessageString, 10.f);
ChatXFunctionLibrary::MyPrintString(this, InChatMessageString, 10.f);
}
현재 실행 중인 월드의 속성인 NetMode(네트워크 모드)를 이해하는 것은 매우 중요
-> 현재 게임이 서버인지, 클라이언트인지, 싱글 플레이인지에 따라 실행되는 로직이 달라지기 때문
| NetMode | 로컬 플레이어 존재 여부 | 원격 클라이언트 접속 허용 | 주요 특징 |
|---|---|---|---|
| StandAlone | O | X | 서버/클라이언트 로직을 혼자 모두 처리 (싱글 게임) |
| Client | O | X (본인이 접속함) | 서버 로직은 무시하고, 복제된 프록시를 렌더링 |
| Listen Server | O | O | 서버 역할을 하면서 방장도 플레이어로 참여 |
| Dedicated Server | X | O | 플레이어 없이 오직 세션 호스팅만 전담 (MMO 등) |
<NetMode에 따른 액터 위치>
서버에만 존재하는 액터 -> 게임 모드
서버와 모든 클라이언트에 존재하는 액터 -> 배경 액터와 폰
서버와 클라이언트에만 존재하는 액터 -> 플레이어 컨트롤러
클라이언트에만 존재하는 언리얼 오브젝트 -> UI
서버에 스폰 된 액터가 가진 NetRole 속성 값은 언제나 Authority
(서버에 스폰 된 액터에서 수행될 로직은 권한을 가지고 있다)
NetRole이 Authority인 액터가 클라이언트로 복제된 액터의 NetRole 속성 값은 Proxy(허상)
멀티플레이어 게임에서는 동일한 액터라도 서버에 있는지, 내 화면에 있는지, 다른 사람의 화면에 있는지에 따라 그 권한과 역할이 달라지는데 이를 통제하기 위해 NetRole(네트워크 역할)이라는 개념을 사용
LocalRole이 Authority인데 RemoteRole이 None이라면, 서버에서 스폰되었지만 클라이언트 측으로는 레플리케이션(전송)되지 않는 액터| NetRole | 권한 및 데이터 흐름 | 제어 주체 | 대표적인 예시 |
|---|---|---|---|
| None | 복제 안 됨 (로컬 전용) | 생성된 로컬 클라이언트/서버 | 배경 장식물, 이펙트(Particle) |
| Authority | 절대적 권한 (서버 → 클라 송신) | 서버 | GameMode, 서버에 있는 모든 중요 액터 |
| Autonomous Proxy | 양방향 통신 (서버 ↔ 클라) | 조종 중인 클라이언트 | 내 PlayerController |
| Simulated Proxy | 일방적 수신 (서버 → 클라) | 서버 데이터에 의해 시뮬레이션 | 다른 유저의 캐릭터, NPC |
LocalRole = Authority / RemoteRole = Autonomous ProxyLocalRole = Autonomous Proxy / RemoteRole = Authorityif (HasAuthority()) (내부적으로 LocalRole == ROLE_Authority인지 체크)를 흔히 사용Pawn을 상속받는 CXPawn 생성
// ChatX.h
...
class ChatXFunctionLibrary
{
public:
...
static FString GetRoleString(const AActor* InActor)
{
FString RoleString = TEXT("None");
if (IsValid(InActor) == true)
{
FString LocalRoleString = UEnum::GetValueAsString(TEXT("Engine.ENetRole"), InActor->GetLocalRole());
FString RemoteRoleString = UEnum::GetValueAsString(TEXT("Engine.ENetRole"), InActor->GetRemoteRole());
RoleString = FString::Printf(TEXT("%s / %s"), *LocalRoleString, *RemoteRoleString);
}
return RoleString;
}
};
// CXPawn.h
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Pawn.h"
#include "CXPawn.generated.h"
UCLASS()
class CHATX_API ACXPawn : public APawn
{
GENERATED_BODY()
protected:
virtual void BeginPlay() override;
virtual void PossessedBy(AController* NewController) override;
};
// CXPawn.cpp
#include "CXPawn.h"
#include "ChatX.h"
void ACXPawn::BeginPlay()
{
Super::BeginPlay();
FString NetRoleString = ChatXFunctionLibrary::GetRoleString(this);
FString CombinedString = FString::Printf(TEXT("CXPawn::BeginPlay() %s [%s]"), *ChatXFunctionLibrary::GetNetModeString(this), *NetRoleString);
ChatXFunctionLibrary::MyPrintString(this, CombinedString, 10.f);
}
void ACXPawn::PossessedBy(AController* NewController)
{
Super::PossessedBy(NewController);
FString NetRoleString = ChatXFunctionLibrary::GetRoleString(this);
FString CombinedString = FString::Printf(TEXT("CXPawn::PossessedBy() %s [%s]"), *ChatXFunctionLibrary::GetNetModeString(this), *NetRoleString);
ChatXFunctionLibrary::MyPrintString(this, CombinedString, 10.f);
}
중요한 로직은 서버 컴퓨터에서 실행(AActor::HasAuthority())
-> 'HasAuthority()가 true다' 라고 해서 무조건 플레이어가 아님
입력 관련 로직이나 UI는 Autonomous Proxy에서 수행
-> AController::IsLocalController() 와 APawn::IsLocallyControlled()
각 액터에 따른 특징
1. 게임 모드는 HasAuthority() 함수를 호출할 필요가 없음
2. 폰은 Authority, Autonomous Proxy, Simulated Proxy가 혼재되어 있음
3. UI 관련 로직은 클라이언트에서만 수행
언리얼 엔진 네트워크 코드를 작성하다 보면 GetLocalRole()과 GetRemoteRole() 두 가지를 확인하게 됨. 멀티플레이 환경에서 액터들을 더 정확하게 분류하고, 올바른 대상에게 클라이언트 RPC(원격 프로시저 호출)를 보내기 위해서.
만약 LocalRole만 존재한다면?
서버 월드에 두 종류의 캐릭터가 존재한다고 가정했을 때
서버 관점에서 확인해 보면 캐릭터 A와 B 모두 이 월드를 관장하는 서버가 권한을 쥐고 있으므로 LocalRole이 똑같이 Authority로 찍힘
-> LocalRole 하나만으로는 이 액터가 '접속한 유저가 조종하는 캐릭터'인지 '서버가 만든 NPC'인지 알 수 없음.
이때 RemoteRole(상대방 화면에서는 어떤 역할인가?)을 함께 확인하면 해결
<RPC 라우팅>
서버가 특정 함수를 호출했을 때, 그 함수가 해당 액터를 소유한 오너 클라이언트의 화면에서만 실행되도록 만들려면 서버는 누가 유저 소유의 액터인지 정확히 알아야 함. RemoteRole이 존재하기 때문에 서버는 RemoteRole이 Autonomous Proxy니까 오너 클라이언트가 있으니 이쪽으로 클라이언트 RPC를 쏘고 정확한 대상을 찾아갈 수 있음