TIL 2025.03.10 기록
0. 개요
오늘은 결국 언리얼 소스 코드 빌드를 다시 하고.. 챌린지반 수업과 네트워크 강의를 이어서 들었다.
확인해보니.. 데디케이트 서버를 빌드하기 위해서는 소스 코드 빌드를 통해 언리얼 엔진 자체를 빌드 해야하는 것 같았다.
정확한 것은 이후 수업을 진행하다보면 정확히 알 수 있을 것 같았다.
소스 코드 빌드하는게 시간이 너무 오래 걸려서 기다리면서 좀 정리하고, 다운할 것들을 밀 다운해두는 시간을 갖었다.
+ 챌린지반 과제 진행
오늘도 분반 수업 정리와 네트워크 강의 정리를 하고 가려한다.
1. 챌린지반 수업
<브루트포스 요약>
시간 복잡도가 커지기 때문에 무조건 좋은 방법만은 아니다.
하지만, n의 크기가 작을 때는 문제 해결에 대해서는 아주 강한 무기이다.
< 부르트포스의 장점 >
- 직관적이고 간단하다.
구현이 비교적 쉬우니, 실수를 줄이고 안정적인 코드를 짜기 쉽다. - 실행 속도가 n이 작은 경우 크게 문제되지 않는다.
작은 범위에 대한 문제에서는 최적화한 방법보다 더 빨리 구현해 낼 수 있기 때문에 시간이 정해진 테스트나 대회라면 의외로 최고의 무기가 될 수 있다. - 정답을 보장한다.
모든 경우를 탐색하므로, 정확도 면에서는 가장 확실하다.
< 부르트포스의 단점 >
- 시간 복잡도가 기하급수적으로 증가한다.
n이 조금만 커져도 지수적으로 연산이 늘어나기 때문
그래서, n 범위를 보고 잘 판단해야 한다. - 메모리나 스택 오버플로우 등의 문제가 발생할 수 있다.
특히 재귀를 이용할 경우 너무 깊은 재귀는 실행이 불가능하다.
< 브루트포스 기법 정리 >
- n의 범위를 파악하기
- 가능한 모든 경우의 수를 만드는 아이디어를 떠올리기
반복문 중첩 / bitmasking / next_permutation / 백트래킹 - 모든 경우마다 조건을 체크해 답을 갱신하기
최댓값, 최솟값, 조건을 만족하는 경우의 개수 등
< 문제 풀이 >
문제 1
Q. 길이가 n(1 ≤ n ≤ 15)인 배열(또는 벡터)이 있는데 이 배열로부터 만들어질 수 있는 모든 부분집합의 합을 전부 구하여, 부분집합의 합 중에서 유일한 값들의 개수를 출력하는 프로그램을 짜보시오. (20분)
n=3, 배열이 [1,2,3]이면 가능한 부분집합과 그 합은: - 공집합: 합 0 - [1]: 합 1 - [2]: 합 2 - [3]: 합 3 - [1,2]: 합 3 - [1,3]: 합 4 - [2,3]: 합 5 - [1,2,3]: 합 6 따라서, 합은 {0,1,2,3,3,4,5,6}이고 중복 제거하면 {0,1,2,3,4,5,6} → 답: 7
#include <vector>
#include <set>
using namespace std;
int main()
{
vector<int> arr = { 1, 2, 3 };
set<int> TempSet;
for (int i = 0; i < (1 << arr.size()); ++i)
{
int Sum= 0;
for (int j = 0; j < arr.size(); ++j)
{
if (i & (1 << j))
{
Sum+= arr[j];
}
}
TempSet.insert(Sum);
}
return 0;
}
문제 2
프로그래머스
SW개발자를 위한 평가, 교육, 채용까지 Total Solution을 제공하는 개발자 성장을 위한 베이스캠프
programmers.co.kr
#include <string>
#include <vector>
using namespace std;
int solution(vector<vector<int>> sizes) {
int answer = 0;
int max_X = 0;
int max_Y = 0;
for (int i = 0; i < sizes.size(); ++i)
{
if (sizes[i][0] < sizes[i][1])
{
swap(sizes[i][0], sizes[i][1]);
}
max_X = max(max_X, sizes[i][0]);
max_Y = max(max_Y, sizes[i][1]);
}
answer = max_X * max_Y;
return answer;
}
문제 3
프로그래머스
SW개발자를 위한 평가, 교육, 채용까지 Total Solution을 제공하는 개발자 성장을 위한 베이스캠프
programmers.co.kr
#include <string>
#include <vector>
#include <algorithm>
using namespace std;
vector<int> solution(vector<int> answers)
{
vector<int> answer;
vector<int> Num1 = { 1, 2, 3, 4, 5 };
vector<int> Num2 = { 2, 1, 2, 3, 2, 4, 2, 5 };
vector<int> Num3 = { 3, 3, 1, 1, 2, 2, 4, 4, 5, 5 };
vector<int> score = { 0, 0, 0 };
int maxScore = 0;
for (int i = 0; i < answers.size(); ++i)
{
if (answers[i] == Num1[i % Num1.size()])
score[0]++;
if(answers[i] == Num2[i % Num2.size()])
score[1]++;
if (answers[i] == Num3[i % Num3.size()])
score[2]++;
}
maxScore = *max_element(score.begin(), score.end());
for (int i = 0; i < score.size(); ++i)
{
if (maxScore == score[i])
answer.push_back(i + 1);
}
return answer;
}
2. 언리얼 네트워크와 객체 통신 이해하기
< 언리얼 Replication이란? >
Replication은 서버와 클라이언트 복제이다.
더 깊이 들어가보면, Network Framework API를 상속받아 구현한 구현체이다.
Replication 아래를 보면 RepNotify PropertyReplication등 다양하게 구성되어 있다.
근데, 실제적으로 우리가 실무에서 쓰는 것은 RPC와 Actor Replication 두 가지 밖에 없다.
액터의 움직임이나 회전, 이런 것들은 Actor Replication으로 자동화 되어있다.
그 자동화되는 것들의 근간을 이루는게 해당 부분들이다.
이런 Actor Replication된 애들이 이동같은 걸 하려면 또 각각의 변수들이 Replication되어야 하기 때문에 Ownership, Property Replication, Actor Replication이 한 덩어리고 RPCs, Rellabillity, RepNotify가 한 덩어리인 셈이다.
크게 이렇게 두 덩어리로 묶어서 봐도 무방할 것 같다.
🌟정리🌟
Replication은 NetworkFramework API를 상속받아 구현한 구현체이다.
하나는 RPCs하고, RepNotify를 하는 것이고, 다른 하나는 자동화해서 Actor Replication을 하는 것이다.
Actor Replication이라고 하는 것은 서버에서 스폰된 액터를 클라이언트의 액터랑 동기화 시킨 것이다.
해당 링크에 들어가보면, 언리얼의 각각 파이프라인을 정리해둔 것이 있어서 보면 도움이 많이 될 것이라고 하신다.
Unreal Schematics | Unreal Engine Community Wiki
Unreal schematics featuring Animation, Blueprint, Character, Engine, Materials, Programming, Rendering & World Building
unrealcommunity.wiki
해당 링크에 들어가보면, 언리얼의 각각 파이프라인을 정리해둔 것이 있어서 보면 도움이 많이 될 것이라고 하신다.
< RepNotify, ActorReplication, RPCs 정리 >
중요한 부분은 RepNotify, ActorReplication, RPCs 이 3가지이다.
- 🌟RepNotify
값이 변경되었을 때, 서버에서 클라이언트로 이벤트를 호출해주는 용도이다.
근데, 서버에서 클라이언트로 호출할 때, 이벤트에 호출할 수 있는 종류를 선택할 수 있게 되어있다.
호출 방법은 자동이다.
용도 : 값이 변경되었을 때, 서버에서 클라이언트로 이벤트를 호출
호출 방법 : 자동
실행 : 클라이언트
네트워크가 연결되어 있으면 신뢰 가능
UPROPERTY(ReplicatedUsing=OnRep_HealthChanged)
int32 PlayerHealth = 100;
UFUNCTION()
void OnRep_HealthChanged();
- 🌟ActorReplication
클라이언트에서 생성하는게 아니라 서버에 스폰해달라고 요청을 하고, 서버에 스폰된 액터와 클라이언트에 스폰된 액터가 동기화되는 것이다.
호출 방법이 자동이며, 서버에 있는 값이 변경되면 클라이언트에게 전달해주는 것이다.
용도 : 변수값의 동기화 및 오너쉽 관리로 서버→클라이언트
호출 방법 : 자동
실행 : 서버에서 값 변경 후 클라이언트에 반영
네트워크가 연결되어 있으면 신뢰 가능
UPROPERTY(Replicated)
int32 PlayerHealth = 100;
위 두개는 호출이 자동이라 제일 중요한건 RPCs이다.
- 🌟🌟RPCs🌟🌟
RPCs가 문제인데, 수동으로 데이터를 내가 업데이트 시키거나 읽거나 푸시를 한다거나 이런 것들이 가능하다.
위 두개는 자동으로 동기화가 되는데, RPCs는 수동이라 동기화를 직접 해주어야 한다.
또 RPCs에는 두가지가 있는데, Reliable이라고 하는건 TCP이다.
때문에 핸드쉐이크하고난 뒤 데이터를 주고 받아 응답까지 받는 과정을 거친다. 때문에 속도가 느릴 수 밖에 없다.
Unreliable은 UDP라서 핸드세이크가 없이 바로 데이터를 전달하고 끝이다.
때문에 속도가 보다 빠르다.
이러한 차이 때문에 Reliable은 게임 플레이나 어떤 액터에 관련된 중요한 것들에 사용한다.
Unreliable의 경우 이펙트(파티클)처럼 꼭 굳이 전달될 필요가 없는 애들에 사용한다.
튜터님 기억에 Unreliable의 경우 크게 잘 사용하지 않았던 것 같다고 하신다.
특히 Lan 파티 플레이 같은 경우 Reliable 하다고 가정하고 개발을 많이 하셨다고 한다.
즉! 대부분 Reliable로 작업하게 될 것이고, 추후에 최적화를 할 때, 하나씩 Unreliable로 바꿔보면서 확인해보는게 초보 단계에서는 좋을 것 같다고 하신다.
용도 : 서버와 클라이언트 간 양방향 함수 호출
호출 방법 : 수동
실행 : 서버, 클라이언트
Reliable / Unreliable 선택 가능
UFUNCTION(Server, Reliable)
void ServerFireWeapon();
UFUNCTION(NetMulticast, Unreliable)
void MulticastPlayExplosionEffect();
< Listen Server와 Dedicated Server를 현업에서 사용하지 않는 이유 >
현업에서는 실용도가 많이 떨어져서 잘 사용하지 않는다.
왜냐하면, MMORPG로 예를 들면 한 서버에 수백명, 수천명이 모이는데, Dedicated Server에는 DB도 없고, 신뢰도가 떨어지는 이슈가 있다.
=> 애초에 MMORPG처럼 수용인원이 많을 때가 목적이 아닌 정해진 인원 내에서 대전을 하거나 하는 등이 목적으로 개발된 것이라고 하신다.
Listen Server도 동일한데,
LAN 파티 플레이를 하는 건데 얘는 그래도 쓰이는게, 한국의 스팀게임의 경우 LAN에서 서로 클라이언트끼리 통신을 하고, 누군가는 서버를 맡고 누군가는 클라이언트를 맡아서 하는 경우엔 사용될 수 있을 것 같다고 하신다.
< Listen Server와 Dedicated Server >
언리얼에서는 네트워크 모드가 두 가지가 기본으로 제공된다.
< Dedicated Server >
사실은 전용 서버라는 의미이다.
해당 전용 서버의 경우 별도로 빌드를 해주어야 한다.
=> VS 2022로 하거나 다른 것을 사용해 빌드하는데, 다음 챕터에서 다룰 예정이라고 하신다.
언리얼 에디터에서는 서버 코드와 클라이언트 코드를 다 탑재한 채로 프로젝트를 만드는데, Dedicated Server를 빌드하면, 서버에 관련된 코드만 걸라서 따로 빌드를 만든다.
그게 Dedicated Server 빌드라고 하는 것이다.
즉, Dedicated Server는 클라이언트 기능을 포함하지 않는다.
그래서 오히려 보안 면에서 좋고, 독립 서버를 갖춘 게임 환경에서는 굉장히 좋은 선택이다.
또, 개발 난이도도 Listen Server처럼 복잡하지 않다.
< Listen Server >
Listen Server는 한쪽이 서버역을 맡고 한쪽이 클라이언트역을 맡기 때문에 계속 중첩이 있는 상황이다.
⇒ 클라이언트 기능을 포함 한다.
때문에 테스트를 하기에도 까다롭고, 클라이언트가 서버의 역할을 같이하다보니, 보안 면에서 굉장히 취약하다.
개발을 할때, 클라이언트와 서버의 기능이 합쳐져 있기 때문에 권한체크를 반드시 해줘야하는 문제가 있다.
< 언리얼의 객체간 통신 >
커스텀 이벤트로 등록 시켜두고 사용하는데,
왜냐하면 A와 B라는 블루프린트간 데이터를 주고 받는 객체간의 통신의 경우 2가지의 방법으로 할 수 있다.
- 커스텀 이벤트를 만들어서 호출하는 방법이 있고
- 이벤트 디스펙처를 사용하는 방법이 있다.
문제는 이벤트 디스펙처 기능은 네트워크 환경에서는 사용할 수 없다.
< 객체 이벤트 호출 >
이처럼 A가 이벤트 특정 객체의 이벤트를 호출하기 위해서는 B와 C를 참조해 형변환한 후 접근하여 이벤트 함수를 직접 호출해주어야 했다.
이 과정에서 예외 처리를 잘못하면, 로컬에서는 크래시가 발생한다.
< 이벤트 디스패처 >
만약 로컬 클라이언트 단에서만 이벤트를 호출하고 뭔갈 하고 싶다하면, 이벤트 디스패쳐를 사용하는 것을 추천하신다고 하신다.
이유는 특정 이벤트를 호출할 때, 특정 객체에서 해당 객체에 접근해 특정 이벤트를 수행해주어야 한다.
이때, 해당 객체가 원하는 클래스로 캐스팅을 거쳐야하고, 이때 NULL인 예외상황이 존재할 수 있기 때문에 예외처리를 잘 해두지 못했다면, 문제가 발생한다.
또 이런 강한 결합(강한 객체 간 연결)을 피하는 코드가 좋은 코드로 인정받기 때문에 언리얼에서 지원해주는 이벤트 디스패쳐를 사용하는게 좋다.
때문에 이벤트 디스패로 객체간 통신하는 것을 추천하신다고 하신다.
⇒ 즉, 로컬은 예외처리를 제대로 못하면 크래시가 발생하기 때문에 이벤트 디스패처 방식을 사용하는걸 추천한다는 것.
기존에는 이벤트를 호출하려면 호출하려는 객체에서 대상 객체를 참조하고 있어야하는 문제가 있다.
하지만 디스패처 기법은 객체는 단순히 디스패처를 가지고 있고, 대상 객체들이 이벤트에 함수를 바인딩을 거는 방식이다.
< 이벤트 디스패처의 장단점 >
이벤트를 생성하고 바인딩 거는데 불편함이 있지만, 특정 객체를 못찾아서 크래시가 발생할 일은 없다는 장점이 있다.
근데, 네트워크 환경에서는 위와같은 에러가 발생하더라도 크래쉬가 발생하는게 아니라 단순히 네트워크 에러가 날 뿐이다.
때문에 로컬에서는 이벤트 디스패처를 많이 이용하도록 하고, 네트워크 환경에서는 단순 이벤트 호출방식을 사용하도록 한다고 하신다.
< Replicate 속성 >
이벤트를 생성하면, Replicate라는 속성이 활성화 된다.
해당 설정을 Not Replicated로 하지 않으면, 바로 RPCs가 된다.
- Not Replicated
네트워크 객체가 아닌 것이다. - Multicast
서버가 모든 클라이언트에 그대로 뿌리는 것이다. - Run on Server
서버에 있는 이벤트 객체인 것이다.
서버에 존재하는 코드로 클라이언트에서 바로 호출할 수 없다. - Run on owning Client
내 로컬에 있는 이벤트 객체이다.
내 클라이언트에서만 있는 내용이다.
< 서버와 클라이언트간 통신 >
플레이어 컨트롤러를 중심으로 게임 요소들과의 관계를 이해해야 한다.
이미지를 보면, 플레이어 컨트롤러는 HUD, Input, PlayerCamera 등을 포함하고 있고, GameMode와 GameState에 들어가 있다.
이를 네트워크 환경으로 가져가면 다음과 같다.
확인해보면, 결국 서버에서 클라이언트로 혹은 클라이언트에서 서버로 데이터를 전달할 때, 반드시 플레이어 컨트롤러를 거치는 수 밖에 없다는 것을 확인할 수 있다.
음.. 약간 플레이어 컨트롤러가 서버와 클라이언트간 중간다리가 되어서 데이터를 전달해주는 것 같다.