TIL_103 : Subsystem, Lambda, list sort

김펭귄·2026년 1월 27일

Today What I Learned (TIL)

목록 보기
103/109

1. Subsystem

  • 특정 "범위(Scope)"에 바인딩된 싱글톤 객체

  • 원하는 구간에서만 존재하여 데이터나 로직을 만들고 싶을 때 사용

  • UGameInstanceSubsystem, UWorldSubsystem를 가장 많이 사용

1.1. 종류

  • UEngineSubsystem

    • 가장 긴 수명을 가지는 subsystem

    • 엔진이 켜지는 순간부터 종료될 때까지 존재

    • 게임 시작 안 하고 에디터만 켜져있어도 존재함

    • 게임 개발보다는 분석, 로그 시스템에 주로 사용

class UMyEngineSubsystem : public UEngineSubsystem
{};
  • UEditorSubsystem

    • 에디터 전용으로, 패키지하면 포함 안 됨

    • 에디터 플러그인, 에셋 처리 등 에디터 커스터마이징에 사용

class UMyEditorSubsystem : public UEditorSubsystem
{};
  • UGameInstanceSubsystem

    • 게임 인스턴스와 수명 동일

    • 게임 시작 시 생성, 종료 시 끝나며 레벨이 전환되도 유지됨

    • 저장/업적/인벤토리 시스템 등 레벨 바뀌어도 유지되어야 하는 데이터나 로직에 사용

class UMyGameInstanceSubsystem : public UGameInstanceSubsystem
{};
  • UWorldSubsystem

    • 월드와 수명 동일

    • 레벨 생성 시 생성되고, 레벨 종료되면 죽음 (레벨마다 별도의 인스턴스)

    • 웨이브 시스템, AI 매니저 등에 사용

class UMyWorldSubsystem : public UWorldSubsystem
{};
  • ULocalPlayerSubsystem

    • 로컬 플레이어와 수명 동일

    • 플레이어마다 별도의 인스턴스가 생성됨

    • 플레이어마다 가지는 UI, Input Manager, Camera 설정 등 개인 설정에 사용

class UMyLocalPlayerSubsystem : public ULocalPlayerSubsystem
{};

1.2. 예제

// MyGameInstanceSubsystem.h
UCLASS()
class UMyGameInstanceSubsystem : public UGameInstanceSubsystem
{
    GENERATED_BODY()

public:
	// 이 두 함수는 꼭 override 해주기
    virtual void Initialize(FSubsystemCollectionBase& Collection) override;
    virtual void Deinitialize() override;
};

// MyGameInstanceSubsystem.cpp
void UMyGameInstanceSubsystem::Initialize(FSubsystemCollectionBase& Collection)
{
    Super::Initialize(Collection);
    UE_LOG(LogTemp, Log, TEXT("Save System Initialized!"));
}

void UMyGameInstanceSubsystem::Deinitialize()
{
    UE_LOG(LogTemp, Log, TEXT("Save System Shutting Down!"));
    Super::Deinitialize();
}
  • Initialize : 액터의 BeginPlay같은 것으로, 초기화 역할해주는 함수

  • Deinitialize : EndPlay같은 함수

// GameInstanceSubsystem
UMySaveSubsystem* SaveSys = GetGameInstance()->GetSubsystem<UMySaveSubsystem>();
SaveSys->SaveGame();

// WorldSubsystem
UMyWaveSubsystem* WaveSys = GetWorld()->GetSubsystem<UMyWaveSubsystem>();
WaveSys->StartNextWave();

// LocalPlayerSubsystem
UMyHUDSubsystem* HUDSys = GetLocalPlayer()->GetSubsystem<UMyHUDSubsystem>();
HUDSys->ShowPauseMenu();
  • 서브 시스템 사용하려면 각 서브시스템의 주인을 통해 접근

  • 싱글톤 객체이므로, GetSubsystem으로 쉽게 인스턴스 받아 사용 가능

  • 엔진이 알아서 생명주기에 맞게 초기화와 소멸시켜줌

  • 클라와 서버 각자 생기므로, 동기화하고 싶으면 RPC통해서 직접 해주어야 함.
    자동으로 동기화 안 됨

2. Lambda

  • 이름이 없는 함수객체를 즉석에서 정의하고 사용하게 해주는 기능

  • 별도의 함수 정의 없이 필요한 곳에서 바로 작성할 수 있어 코드의 흐름을 방해하지 않음

2.1. 람다 구조

[Capture Clause] (Parameters) -> ReturnType { Body }

  • Capture Clause (캡처 절): 외부 변수를 람다 내부(객체 내부)로 가져옴

  • Parameters (매개변수): 일반 함수와 동일한 인자 리스트

  • ReturnType (반환 타입): 생략 시 컴파일러가 추론

2.2. 람다 동작 원리

  • 람다를 보고 컴파일러가 익명의 클래스를 생성

  • 캡처 절에 있는 변수는 이 클래스의 멤버변수가 됨

  • 람다의 매개변수와 함수부는 이 클래스의 operator() 연산자 오버로딩 되어 정의됨

  • 이렇게 생성된 인스턴스를 Closure라고 부름

  • 캡처 절은 객체 내부에서 값으로 존재하게 되고, 매개인자는 외부에서 넣어주는 값으로 차이가 존재함

int limit = 10; // 외부 변수
auto checkLimit = [limit](int n) { 
    // limit: 임시 객체에 멤버변수로 존재하게 됨. 항상 매개변수로 넣어줄 필요 없음
    // n: 매개변수 (함수를 쓸 때마다 바뀔 숫자)
    return n > limit;
};

checkLimit(5);  // false (5는 n에 들어감)
checkLimit(15); // true  (15는 n에 들어감)

2.3. 캡처 절

  • [] : 외부 변수를 캡처하지 않음

  • [=] : 모든 외부 변수를 값에 의한 복사(Copy)로 캡처 (읽기 전용)

  • [&] : 모든 외부 변수를 참조(Reference)로 캡처

[x, &y]

  • xconst로 임시 객체 내부로 값 복사, y는 참조로 객체 내부로 가져옴

  • 외부에서 x를 변경하더라고, 임시 객체 내부에선 값 변경 안 됨. 복사한 것이기 때문

  • 외부에서 y를 변경하면 내부도 변경됨. 참조자이기 때문

  • 내부에서 y변경하면, 외부도 변경됨(참조자)

  • 내부에서 x를 수정하면 컴파일 에러 발생함. 변수와, 함수가const이기 때문

  • 그래서 mutable키워드를 사용하면, 내부에서x값을 변경 가능함 (외부 x는 변화 없음)

int count = 0;

// mutable이 없으면 count++ 에서 컴파일 에러 발생
auto increment = [count]() mutable {
    count++; // 람다 내부의 복사본 count를 수정
    std::cout << "Inside: " << count << std::endl;
};

increment(); // Inside: 1
increment(); // Inside: 2
std::cout << "Outside: " << count << std::endl; // Outside: 0 (원본은 그대로)

2.4. 댕글링 참조 문제

  • 람다가 캡처한 참조나 포인터가, 람다가 실제로 실행되는 시점에 이미 해제되어 유효하지 않은 메모리를 가리키는 상태
auto getLambda() {
    int local_val = 10; 
    return [&local_val]() { return local_val + 5; }; 
} // 함수가 종료되면서 local_val은 메모리에서 사라짐!

auto myLambda = getLambda();
myLambda(); // 에러! 이미 사라진 local_val의 메모리에 접근 (Undefined Behavior)
  • 값 복사 캡쳐를 이용하여 해결 (복사비용과 객체크기 커지면 안 좋다는 단점 존재)

  • 스마트 포인터이용해 캡쳐

auto p = std::make_shared<int>(42);
auto lambda = [p]() { // p의 참조 횟수(Reference Count)가 증가하여 안전함
    std::cout << *p << std::endl;
};

3. list sort

  • list 자료구조는 다른 컨테이너들과 다르게 <algorithm>std::sort를 사용하지 못 함

  • 대신에 자신의 멤버변수 sort를 이용

3.1. 반복자 차이

  • std::sort는 랜덤 액세스 반복자로 접근이 가능한 컨테이너이어야 함

  • 하지만, list의 경우 랜덤 액세스가 불가능하고, 양방향 반복자만 지원하여서 std::sort가 사용이 불가능하다

3.2. 데이터 이동 방식

  • std::sort는 요소의 값을 직접 복사하거나, 교환시켜 정렬시킴 (데이터가 크면 비용이 크다는 단점 존재)

  • list::sort는 값의 복사, 이동 없이 노드 사이의 포인터만 바꿔주는 방식으로 정렬을 함

  • std::sort를 사용 못하는 이유는 아니지만, list::sort는 실제 데이터는 메모리의 위치에 그대로 있고, 전/후의 연결정보만 바꾸면서 정렬하기에 매우 효율적인 정렬 방법이다

3.3. 알고리즘 차이

  • std::sort는 보통 퀵소트를 사용하지만, list::sort는 병합 정렬 방식을 채택

  • 병합 정렬은 동일한 값의 순서가 유지되는 안정 정렬

  • 퀵소트는 피벗을 이용한 분할 정복 알고리즘

    1. 피벗 선택: 배열에서 원소 하나를 고릅니다. (보통 중간)

    2. 분할(Partition): 피벗을 기준으로 작은 데이터는 왼쪽, 큰 데이터는 오른쪽으로 이동. 이 과정이 끝나면 피벗은 자기의 최종 정렬 위치에 고정

    3. 재귀(Recursion): 피벗을 제외한 왼쪽 부분과 오른쪽 부분에 대해 반복

profile
반갑습니다

0개의 댓글