프로세스 메모리 구조
Fragmentation
언리얼의 메모리 관리(심화)
enum class
UI 생성주기
P2P/Listen/Dedicated Server
작성한 코드는 어셈블리어로 번역되어 프로세스 메모리의 Code부분에 저장
PC(Program Counter)는 이 기계어들의 메모리주소를 읽으며 실행
ReadOnly고 런타임중 수정하려하면 OS가 에러 일으킴
전역변수 또는 static 변수 중에서 초기화되면(값이 0이 아님) Data에, 초기화 안 되면(값이 0) BSS에 0으로 저장
전역은 위험하니까 UGameInstance나 UGameSingleton을 사용하기.
얘네는 힙에 생성되고, 게임 수명동안 존재함
함수 실행되며 메모리 차지하는 부분
함수 끝나고 돌아갈 지점인 Return Address도 Stack에 저장됨
함수가 호출/종료되면서 스택메모리가 새로 할당되거나 해제되는데, 이 과정이 엄청 빠름
왜냐면 SP라는 레지스터가 있는데 얘가 스택에 할당된 최상단 메모리 주소를 가리킴.
CPU는 이 SP를 더하거나 빼면서 메모리를 할당, 해제함.
그래서 스택은 메모리가 연속적으로 사용되며, 할당과 해제가 빠름
가상메모리 관점에서는 이러함. 실제 메모리 관점에서 보면 paging을 이용하여 스택메모리에 할당함. 실제 스택메모리는 연속적으로 할당되지만, 같은 프로세스의 메모리가 연속적으로 존재한다고는 할 수 없음.

힙은 스택보다 크기가 크지만, 속도가 느림
스택과 다르게 할당이 뛰엄뛰엄 되어 있어서 새로 할당시 탐색해야하며, 해제도 단순한 작업이 아님
메모리가 뛰엄뛰엄 있으므로, 총 여유공간은 할당하려는 프로그램보다 큰데 연속적이지 않아서 할당 못하는 External Fragmentation이 발생
그래서 Paging을 이용하여 이를 방지
프로세스의 가상메모리를 Page로, 실제 메모리를 Frame으로 자름. 둘 다 같은 크기(4KB)
그리고 Page단위로 Frame에 할당. 이를 통해 External Fragmentation을 효과적으로 줄임.
OS Kernel같은 경우는 성능과 하드웨어 제어 등을 이유로 페이징을 거치지 않는 연속된 물리 메모리를 요구할 때가 많음.
따라서 메모리가 조각나 있으면 External Fragmentation 발생 가능
Frame크기(4KB)보다 작은 프로세스 메모리가 올라가면 frame 남은 공간은 사용안되고, 공간이 낭비되는 현상
Paging하면서 발생하는 어쩔 수 없는 문제
힙에 동적으로 할당된 애들은 메모리누수와 댕글링포인터라는 위험이 존재하므로, TObjectPtr을 사용하여 GC가 이를 관리함
TObjectPtr은 클래스 멤버변수에서만 사용가능하며, UPROPERTY를 꼭 붙여주어야 엔진과 GC가 인지하고 처리해줌
컴파일 시 UHT가 UPROPERTY매크로를 보고, 이 클래스는 XX위치에 어떤 UObject객체를 가리키는 포인터가 있다는 명단을 생성해 엔진에 등록
런타임 도중 UObject객체가 필요없다 판단되어, GC가 삭제하려 함
그럼 삭제하기 전에, GC는 해당 UObject를 가리키는 포인터를 가지는 객체들의 명단(1번에서 만든거)를 다 스캔
각 객체에 직접 접근하여 포인터를 nullptr로 바꿔버림
그리고 GC가 해당 UObject를 삭제
GC의 관리를 받으려면 UObject를 상속받는 객체여야함
따라서 new가 아니라 NewObject<T>()나 GetWorld()->SpawnActor<T>()로 새 객체를 생성해야함
이 객체들은 힙에 생성되며, GC에 의해 관리받을 수 있음
먼저 루트셋에서 시작
메모리 정리 과정에서 기준이 되는, 참조 그래프의 시작점 역할을 하는 객체들의 집합
루트셋에 들어 있는 객체들은 GC 대상에서 제외되고, 다른 곳에서 참조되지 않아도 삭제 안 됨
게임 인스터스, 월드, 레벨, 컨트롤러같이 엔진이 반드시 유지해야하는 객체들로 구성
레벨전환시 컨트롤러가 사라지는 것처럼 루트셋에 있는 객체가 사라져야할 때는 루트셋에서 제외 후 참조되지 않으면 GC가 처리
AddToRoot 함수 통해 루트셋에 포함, RemoveFromRoot 함수를 통해 제거
루트셋에 있는 모든 객체를 표시(Mark)
이 객체들이 참조하는 모든 UPROPERTY포인터를 따라가며 표시(Mark)
이 과정을 반복하여 루트셋으로부터 연결된 모든 객체를 표시
UPROPERTY없는 포인터는 따라가지 않음
결국 참조되고 있는 객체는 표시되고, 더 이상 참조되지 않는 객체는 표시가 안 됨
GC가 관리하는 모든 UObject객체 중 표시되어 있으면 표시 지우고 넘어감
표시 없으면, 위에 언급한 것처럼 해당 객체를 참조하는 모든 객체의 포인터를 nullptr로 수정
지울 객체의 소멸자 호출
힙에서 메모리 해제
enum보다 더 안정한 열거형으로, 여러 기능이 존재기존 enum은 int로 자동 변환되어 실수로 다른 정수값과 비교하거나 다른 enum과 비교하는 위험이 있었음
enum class는 컴파일 오류를 발생시켜 실수를 예방
기존 enum은 열거자들이 전역 혹은 외부 스코프에 노출되어 이름 충돌 위험이 있음
열거자들이 클래스 내부에 존재해 스코프가 한정되고 이름 충돌이 발생하지 않음
enum class Color { Red, Green, Blue };
enum class TrafficLight { Red, Yellow, Green };
// 접근할 때 항상 enum class 이름과 함께 범위 지정
Color myColor = Color::Red;
TrafficLight light = TrafficLight::Red;
Color::Red), 어떤 enum의 값인지 분명해져 코드 가독성이 좋아짐CreateWidget<UWidgetClass>(...) 해당 코드를 통해 생성되고 아직 화면에는 보이지 않음UObject처럼 생성자가 호출되어, 변수 초기화 및 컴포넌트 생성이 이루어짐NativePreConstruct()NativeConstruct()NativeTick()NativeDestruct()NativeConstruct()에서 수행한 리소스 연결 및 바인딩을 여기서 해제해줘야 함// CXChatInput.cpp 위젯클래스의 cpp파일
// viewport에 보일 때 호출됨. 여기서 바인딩 및 리소스 연결작업 해줌
void UCXChatInput::NativeConstruct()
{
Super::NativeConstruct();
if (EditableTextBox_ChatInput->OnTextCommitted.IsAlreadyBound(
this, &ThisClass::OnChatInputTextCommitted) == false)
{
EditableTextBox_ChatInput->OnTextCommitted.AddDynamic(
this, &ThisClass::OnChatInputTextCommitted);
}
}
// viewport에서 사라질때 호출. 바인딩 되었던 것들 해제
void UCXChatInput::NativeDestruct()
{
Super::NativeDestruct();
if (EditableTextBox_ChatInput->OnTextCommitted.IsAlreadyBound(
this, &ThisClass::OnChatInputTextCommitted) == true)
{
EditableTextBox_ChatInput->OnTextCommitted.RemoveDynamic(
this, &ThisClass::OnChatInputTextCommitted);
}
}
// OnTextCommitted와 연결된 함수
void UCXChatInput::OnChatInputTextCommitted(const FText& Text,
ETextCommit::Type CommitMethod)
{
if (CommitMethod == ETextCommit::OnEnter)
{
// 엔터 눌렀을 때 수행할 동작들 //
}
}
OnTextCommitted
엔터를 누르거나 포커스가 다른 곳으로 이동하는 등 사용자가 입력을 마친 시점에 호출됨
OnTextChanged
사용자가 문자하나를 입력하거나 지우거나하는 매 순간마다 호출됨
ETextCommit::Type CommitMethod)
입력된 텍스트가 어떻게 확정되었는지 나타내는 열거형 값
OnEnter : 엔터 눌러서 입력 확정OnUserMovedFocus : 마우스로 입력창 말고 다른 곳 클릭해 입력 확정OnCleared : 텍스트 입력을 지웠을 때Default : 이 이벤트 이외에 특별한 이벤트 없이 텍스트가 확정되었을 때
각 컴퓨터(peer)가 서버이자 동시에 클라이언트 역할을하는 네트워크
각 peer는 자신의 데이터를 제공하는 서버역할과 다른 피어로부터 데이터를 받는 클라이언트 역할을 함
중앙 서버 없이 각각 연결되어 네트워크를 운영
초기에는 하나의 peer만(server) 데이터를 가지고 있어, 이를 다른 peer들(client)에게 제공
받은 peer들(server)은 다시 다른 peer(client)에게 데이터 제공 가능
네트워크 참여자가 많아질수록 시스템 확장성과 배포속도가 증가 (Self-Scalability)
각 peer가 다 서버이므로, 하나가 다운되도 네트워크에 문제 없음
예시) 다크소울, 토렌트

하나의 컴퓨터만(host) 서버와 클라이언트 역할을 하고, 나머지는(Guest) 클라이언트 역할만 함
Host는 서버로서 게임을 관리도하면서 플레이를 동시에 하고, 나머지는 플레이만 함
간단한 구조이지만, 호스트가 게임을 종료하면 서버가 사라지는 단점과 일명 "방장사기맵"이라 불리는 클라이언트보다 유리한 점이 존재.
또한 네트워크 부하가 P2P와 달리 호스트에게 집중되는 문제도 있음
예시) 어몽어스, 마인크래프트

클라이언트와 별도로 독립된 전용 서버가 1개 존재하고, 나머지는 클라이언트
전용서버는 화면 렌더링, 캐릭터 조작 등 클라이언트에서 하는 일을 하지 않고, 오직 클라이언트 간 게임 상태 동기화, 권한 관리, 게임 룰 적용 등 서버 로직을 전담
클라 없이 서버작업만 하므로 안정적, 성능이 뛰어남, 많은 접속자들을 처리 가능하여 대규모 MMO게임에 적합
서버는 항상 온라인으로 존재해 모든 클라이언트들은 이 서버에 언제든지 접속 가능
권한이 서버에 집중되어 치트나 해킹에 대해 강력한 방어 가능
단 서버가 다운되면 게임 전체 접속이 불가능해지는 단점
가장 신뢰할 수 있고 안정한 네트워크 구조
예시) 배틀그라운드

서버에서 게임 시작을 위해 서버 프로세스를 실행
Open {Level명} ? Listen 명령어가 인자로 전달되어 해당 레벨을 열고, Listen으로 인해 Socket이 생성됨.
Listen이 없다면 싱글플레이로, Socket이 생성되지 않음

그러면 해당 레벨에 등록되어 있는 GameMode, GameState 등을 차례차례 생성
GameMode는 이 서버에만 딱 한 개 존재

클라이언트가 서버의 IP주소와 Port번호로 접속을 요청
그러면 서버는 클라에게 레벨 정보를 넘기고, 클라는 레벨을 열고 성공했음을 서버에게 알림

서버가 클라에게 알림을 받으면 해당 클라의 PlayerCharacter, PlayerController, PlayerState 등을 서버에 만듦
그리고 서버는 GameState까지 복사해서 클라에게 전달하여 서버와 동기화 시킴
클라는 이에 대해 수정권한은 없고 읽기 권한만 가짐

이전과 똑같이 먼저 서버IP, Port로 요청을 보내고 서버는 클라에게 레벨정보를 넘김
클라는 복사본이 아니라 실제로 레벨을 열고(실선) 성공했음을 서버에게 알리고, 똑같이 서버는 클라의 플레이어관련 데이터를 만들고 복사본을 넘김 (캐릭터, 컨트롤러, State, GameState..)
그리고 각 클라는 다른 클라이언트들의 PlayerState, PlayerCharacter도 복사본으로 받게된다. 내 화면에 다른 사람 캐릭터도 보여야 하므로.
컨트롤러는 애초에 안 보이는거니까 필요 없음
클라의 자기 캐릭터의 복제본은 "Autonomous Proxy" 역할로, 클라에서 자신의 입력을 처리하고, 해당 결과는 서버로 보낼 수 있는 역할을 함
하지만 남의 캐릭터 복제본은 "Simulated Proxy"로, 서버에서 받은 그대로를 보여주기만 하고, 입력을 처리하지 않음
즉, 클라이언트가 입력(방향키)을 처리하여 서버에 신호를 보내고, 입력의 결과(이동)는 서버가 결정하여 클라이언트들에게 알려주는 구조
W키를 누르면 클라이언트는 신호를 받아 로컬로 처리 (입력 지연 최소화)
입력의 결과인 이동을 서버에게 전달하여 서버가 이동요청에 대한 신뢰성을 검증함
서버는 검증 후 자기 월드에서 객체를 실제 월드에서 이동시켜 갱신하고, 이 결과를 모든 클라이언트에게 복제해주어 동기화를해줌