콘솔창은 화면을 지웠다가 내용을 다시 쓰고하는 방식이기에 화면이 깜빡거리는 현상이 있다. 

이 문제를 해결하기 위해 2중 버퍼링 구조를 만든다. 이번 포스팅은 그 작업에 기초가되는 ScreenBuffer 클래스를 만든다.

 

1. ScreenBuffer 클래스 생성

 

더보기
더보기
	ScreenBuffer::ScreenBuffer(const Vector2& screenSize) : screenSize(screenSize)
	{
		// 콘솔 버퍼 생성.
		screenBuffer = CreateConsoleScreenBuffer(
			GENERIC_READ | GENERIC_WRITE,
			FILE_SHARE_READ | FILE_SHARE_WRITE,
			nullptr, 
			CONSOLE_TEXTMODE_BUFFER,
			nullptr
		);


		// 제대로 생성됐는지 확인
		assert(screenBuffer != INVALID_HANDLE_VALUE);

		// 화면 창 크기 설정.
		SMALL_RECT rect = {};
		rect.Top = 0;
		rect.Left = 0;
		rect.Right = static_cast<short>(screenSize.x -1);
		rect.Bottom = static_cast<short>(screenSize.y - 1);
		BOOL result = SetConsoleWindowInfo(screenBuffer, TRUE, &rect);

		// 창 크기 설정 확인.
		assert(result == TRUE);

		// 화면 버퍼 크기 설정 및 예외처리.
		COORD coord = {};
		coord.X = static_cast<short>(screenSize.x);
		coord.Y = static_cast<short>(screenSize.y);
		result = SetConsoleScreenBufferSize(screenBuffer, coord);
		assert(result == TRUE);
		
		// 커서 끄기(커서 깜빡임 방지).
		CONSOLE_CURSOR_INFO info;
		GetConsoleCursorInfo(screenBuffer, &info);

		info.bVisible = FALSE;
		SetConsoleCursorInfo(screenBuffer, &info);


	}

 

1. Windows콘솔에 사용할 새로운 화면 버퍼를 생성

		// 콘솔 버퍼 생성.
		screenBuffer = CreateConsoleScreenBuffer(
			GENERIC_READ | GENERIC_WRITE,
			FILE_SHARE_READ | FILE_SHARE_WRITE,
			nullptr, 
			CONSOLE_TEXTMODE_BUFFER,
			nullptr
		);

 

2. 콘솔 창 크기 설정

 

		// 화면 창 크기 설정.
		SMALL_RECT rect = {};
		rect.Top = 0;
		rect.Left = 0;
		rect.Right = static_cast<short>(screenSize.x -1);
		rect.Bottom = static_cast<short>(screenSize.y - 1);
		BOOL result = SetConsoleWindowInfo(screenBuffer, TRUE, &rect);

 

3. 콘솔 버퍼 크기 설정

콘솔 창은 실제 보이는 영역이고 콘솔 버퍼는 화면 밖까지 포함한 전체 텍스트 저장 공간이다.

이 두개를 같은 크기로 설정한다.

		// 화면 버퍼 크기 설정 및 예외처리.
		COORD coord = {};
		coord.X = static_cast<short>(screenSize.x);
		coord.Y = static_cast<short>(screenSize.y);
		result = SetConsoleScreenBufferSize(screenBuffer, coord);

 

저번 작업에서 Renderer와 Vector2 클래스를 만들었고, 이번에는 실제 Actor가 콘솔에 그려지도록 만드는 작업이다.

 

1. Actor 렌더링할 정보 추가

// 화면에 그릴 글자(이미지)
std::string image;

// 글자 색상.
Color color = Color::White;

// 글자 길이.
int width = 0;

// 그리기 정렬 순서.
int sortingOrder = 0;

// 엑터 위치.
Vector2 position;

 

2. Actor Draw에서 Renderer::Submit(..) 호출하여 새롭게 렌더링 할 정보 렌더큐에 추가

	void Actor::Draw()
	{
		// 비활성화 상태라면 처리 안함.
		if (!IsActive())
		{
			return;
		}

		// 렌더러에서 그릴 데이터 전달.
		Renderer::Get().Submit(image, position, color, sortingOrder);

	}

 

3. TestActor를 통해 실제 이미지를 화면에 이동/표시 처리

#include <Windows.h> 선언해, 키입력에 따른 콘솔 Draw 출력처리

void TestActor::Tick(float deltaTime)
{
	Actor::Tick(deltaTime);
	// ESC 키 종료.
	if (Input::Get().GetKeyDwon(VK_ESCAPE))
	{
		QuitGame();
	}

	// 방향키 이동.
	if (Input::Get().GetKey(VK_LEFT) && position.x > 0)
	{
		position.x -= 1;
	}

	if (Input::Get().GetKey(VK_RIGHT) && position.x < 39)
	{
		position.x += 1;
	}

	if (Input::Get().GetKey(VK_UP) && position.y > 0)
	{
		position.y -= 1;
	}

	if (Input::Get().GetKey(VK_DOWN) && position.y < 24)
	{
		position.y += 1;
	}
}

입력/업데이트 중심 엔진에서 "좌표 + 콘솔출력"을 담당하는 렌더링 기반 계층

1. Vector2 클래스

좌표 기반 x,y정보와  zero, one, right, up 함수등을 가지고 있는 클래스

 

2. 색상 클래스

렌더 코드에서 매직 넘버 제거를 위한, 

namespace Craft
{
	enum class CRAFT_API Color : WORD
	{
		Blue = FOREGROUND_BLUE,
		Green = FOREGROUND_GREEN,
		Red = FOREGROUND_RED,
		Yellow = Red | Green,
		Cyan = Green | Blue,
		White = Red | Green | Blue,
		BrightWhite = White | FOREGROUND_INTENSITY
	}; 
}

3. Renderer 클래스

엑터가 "바로 출력"하지 않고 "그릴 명령"을 등록해서 렌더러가 프레임 단위로 명령을 모아 일괄로 처리하도록 하는 클래스

	class CRAFT_API Renderer {
		// 화면에 그릴 데이터를 명령으로 모아둘 구조체.
		struct RenderCommand {
			std::string image;

			// 위치.
			Vector2 position;

			// 색상.
			Color color = Color::White;

			// 그리그 순서.
			int soringOrder = -1;
		};

엔진코드(DLL)과 게임코드(EXE)로 분리하여 개발한다. 이렇게 하는 이유는 엔진은 재사용 가능한 라이브러리이며, 게임은 엔진을 사용하는 클라이언트 구조이다.

 

 

1. 전처리기 정의

 

구성형식

 

DLL작성하는 프로젝트에서 템플릿에서 외부로 넘어가면 위험하다.

2.빌드 이벤트 세팅

빌드 전

xcopy*.h ..\includes\CraftEngine\ /e /y /i

빌드옵션 :

- /Y : 덮어쓰기

- /I : 대상이 존재하지 않을 때, 대상을 "폴더"로 간주하고 복사(폴더가 없으면 생성)

-/E : 하위 폴더를 모두 복사

 

빌드 후

xcopy $(OutDir)CraftEngine.dll ..\Library\CraftEngine\$(Configuaration)\ /e /y /i
xcopy $(OutDir)CraftEngine.lib ..\Library\CraftEngine\$(Configuaration)\ /e /y /i

 

 

리빌드 후 확인

폴더경로 그대로 .h 파일 복사된거 확인

dll과lib확인

 

3. 컨텐츠 프로젝트 추가

 

새로 추가한 프로젝트에 CraftEngine경로를 include처리한다.

 

 

이 CraftEngine은 includes밑에 새로 복사된 Engine.h이다.

 

dll 링크옵션

목표 : 키 입력을 프레임 단위로 해석해서 게임 로직에 전달할 수 있게 처리

1. Input 클래스 추가

// 이전 프레임에는 키가 안눌렸다가 현재 프레임에 눌렸을때 1번만 호출
bool GetKeyDwon(int keyCode);

// 이전 프레임에는 키가 눌렸다가 현재 프레임에 키 입력이 해제되면 1번만 호출
bool GetKeyUp(int keyCode);

// 현재 프레임에 입력이 눌리면 계속 호출.
bool GetKey(int keyCode);

static Input& Get();

 

KeyState 구조체 생성

// 키 입력 상태 구조체.
struct KeyState 
{
	// 현재 프레임에 키가 눌렸는지.
	bool isKeyDown = false;
	// 이전 프레임에 키가 눌렸는지.
	bool wasKeyDown = false;
};

 

입력 수집방식(Windows API)

Input::ProcessInput():
keyStates[ix].isKeyDown = (GetAsyncKeyState(ix) & 0x8000) != 0;

의미:

- GetAsyncKeyState의 최상위 비트로 눌림 여부 확인

- 0~255 전체 키를 매 프레임 스캔

 

2. Engine 클래스 

 

ProcessInput  : 현재 프레임에 어떤 키값이 입력됐는지 업데이트처리하고

SavePreviousInputStates  : 다음 프레임에 이전 프레임에 어떤 키가 입력되어있는지 처리

void Engine::ProcessInput()
{
    assert(input);
    input->ProcessInput();
}

void Engine::SavePreviousInputStates()
{
    assert(input);
    input->SavePreviousStates();
}

 

1. Level 클래스 :

Level 엔진이 관리하는 요소, Level안에는 오브젝트의 가장 기본인 Actor들을 관리하도록

	void Level::ProcessAddAndDestoryActors()
	{
		// 액터 제거 처리.
		for (auto iterator = actorList.begin(); iterator != actorList.end();)
		{
			//제거 요청 여부.
			if ((*iterator)->HasExpired())
			{
				iterator = actorList.erase(iterator);
				continue;
			}

			++iterator;
		}

		// 액터 추가 처리.

		// 추가요청된 목록이 있는지 확인.
		if (addRequestedActorList.empty())
		{
			return;
		}

		for (const std::shared_ptr<Actor>& actor : addRequestedActorList)
		{
			actorList.emplace_back(actor);
		}

		// 정리.
		addRequestedActorList.clear();
	}

 

2. Actor 클래스 :

Actor가 프레임 이벤트를 받음

	void Actor::BeginPlay()
	{
		// 중복 호출 방지를 위해 설정.
		hasBeganPlay = true;
	}
	void Actor::Tick(float deltaTime)
	{
	}
	void Actor::Draw()
	{
	}
	void Actor::Destroy()
	{
		// 액터 삭제 예약
		// 다음 프레임에 엑터가 레벨에서 제거됨.
		hasExpired = true;
	}
	void Actor::QuitGame()
	{
		// 엔진 종료 요청.
		Engine::Get().Quit();
	}

	std::shared_ptr<Craft::Level> Actor::GetOwner()
	{
		return owner.lock();
	}

	void Actor::SetOwner(std::weak_ptr<Level> newOwner)
	{
		owner = newOwner;
	}

 

 

3. Engine 코드 수정

주요함수 : ProcessInput(), OnInitialized(). BeginPlay(), Tick(), Draw(). 

 

mainLevel과 nextLevel로 관리하여 안전하게 레벨 교체 처리

// 레벨 전환 처리
if (nextLevel)
{
 기존 레벨 정리.
if (mainLevel)
{
	mainLevel.reset();
}
	// 이전 프레임에 전환 요청된 레벨을 메인 레벨로 설정
	mainLevel = std::move(nextLevel);
	// 정리.
	nextLevel.reset();
}
if (mainLevel)
{
	mainLevel->ProcessAddAndDestoryActors();
}

 

 

4. 프로젝트 구조.

Engine -> Level -> Acotr 이벤트 전달구조

 

5. 학습요소

- CPP는 프로젝트 상대경로 안되는 경우

속성 -> C/C++ -> 일반 -> 추가 포함 디렉터리 : $(ProjectDir)\;

 

-  액터 생성 함수 템플릿

Actor 파생 타입만 생성하도록 제한하고, 생성자 인자를 전달해서 shared_ptr로 만든 뒤, 관리 리스트에 넣고 Owner까지 설정하는 Factory 함수

template<typename T, typename ...Args,
typename = std::enable_if_t<std::is_base_of<Actor, T>::value>> std::shared_ptr<T> SpawnActor(Args... args)
{
	// 새로운 액터 객체 생성
	std::shared_ptr<T> newActor = std::make_shared<T>(std::forward<Args>(args)...);

	//추가 요청 목록에 추가
	addRequestedActorList.emplace_back(newActor);

	// 오너십 설정.
	newActor->SetOwner(shared_from_this());

	return newActor;
}

Args... : 생성자 인자를 가변 개수로 받을 수 있음

enable_if + is_base_of : T가 Actor를 상속한 클래스일 때만 이 함수가 컴파일 가

enable_shared_from_this : 객체 자기 자신을 가리키는 shared_ptr을 안전하게 얻기위한 도우미 클래스 (객채 자체가 shared_ptr으로 관리되고 있어야함)

1. CPP 기반 프로젝트생성

2. 최종빌드 경로, 중간 파일 경로 지정

3. 메인 루프 기본 함수 구성

namespace Craft
{
	class Engine {

		public:
			Engine();
			virtual ~Engine();

			// 게임 루프 실행 함수
			void Run();

			// 엔진 종료함수
			void Quit();

		protected:
			// 입력 처리 함수.
			void ProcessInput();
			
			// 초기화 함수
			// 레벨 초기화 함수.
			// 엑터 초기화 함수.
			void OnInitialized();

			// 업데이트 함수.
			void BeginPlay();

			// 렌더링.
			void Tick(float deltaTime);

			// 이전 입력을 저장하는 함수.
			void Draw();

			// 엔진 종료 시 정리 함수.
			void SavePreviousInputStates();

			// 엔진 종료 시 정리
			void ShutDown();
	};
}

 

Engine::Run() 함수

더보기
	Engine.cpp
    
    void Engine::Run()
	{
		// 윈도우즈가 제공하는 고해상도 타이머(하드웨어 타이머)

		// QueryPerformanceFrequency : 타이머의 해상도
		// ex: 밀리세컨트 (1/1000)해상도 = 1000.
		LARGE_INTEGER frequency;
		QueryPerformanceFrequency(&frequency);

		// 현재 시간 확인.
		LARGE_INTEGER counter;
		QueryPerformanceCounter(&counter);

		// 프레임 시간 계산을 위한 변수.
		int64_t currentTime = counter.QuadPart;
		int64_t previousTime = currentTime;

		// 프레임 고정.
		float oneFramTime = 1.0f / setting.framerate;

		while (!isQuit)
		{
			// 입력 처리.
			ProcessInput();

			// 현재 시간 확인.
			QueryPerformanceCounter(&counter);

			// 현재 시간 저장.
			currentTime = counter.QuadPart;

			// 프레임 시간 계산.
			float deltaTime = static_cast<float>(currentTime - previousTime) / static_cast<float>(frequency.QuadPart);

			if (deltaTime >= oneFramTime)
			{
				//레벨 초기화 이벤트 함수.
				OnInitialized();

				//레벨의 액터 초기화 이벤트 함수.
				BeginPlay();

				// 레벨의 액터 업데이트 함수.
				Tick(deltaTime);

				// 업데이트된 결과를 화면에 그리는 함수.
				Draw();

				// 처리된 입력을 이전 프레임 입력으로 저장.
				SavePreviousInputStates();

				// 이전 프레임 시간
				previousTime = currentTime;
			}

		}

		// 정리.
		ShutDown();
	}

알아둘 것

더보기

- QueryPerformanceCounter()가 반환하는 값은 ms, sec가 아니라 tick 개수

- float oneFramTime = 1.0f / setting.framerate : 프레임 고정을 위한 처리

- Engine클래스를 전역 객체를 instance로 처리 (싱글턴)

- struct EngineSetting : 구조체, 엔진 세팅값등을 정의 ( framerate 등등)

 

 

4. 실행 결과물

빌드파일

\ConsoleGameProject\Bin\Debug\CraftEngine

exe파일

중간파일

\ConsoleGameProject\Intermediate\Debug\CraftEngine

 

강의 : 인프런 C++로 만드는 게임 엔진 프레임워크

 

 

언리얼 네트워크 종류

1. 리슨형 : Server / Client 역할을 동시에 

2. 데디케이트 서버 : 서버 전용 인스턴스

 

1. 게임모드의 주요 함수

PreLogin : 클라이언트의 접속 요청을 처리

Login : 접속을 허용한 클라이언트에 대응하는 플레이어 컨트롤러를 만드는 함수

PostLogin : 플레이어 입장을 위해 플레이어에 필요한 기본 설정을 모두 마무리 하는 함수

StartPlay :  게임의 시작을 지시하는 함수

BeginPlay : 게임 모드의 StartPlay를 통해 게임이 시작될 때 모든 액터에서 호출하는 함수 

 

 

Aos 수동 빌드를 성공했고,

그다음은, AOS패키징을 BuildCookRun 명령으로 패키징되도록 해보는것이다.

목표

1. 에디터에서 성공한 설정을 유지

2. 같은 결과를 UAT BuildCookRun으로 뽑기

3. 명령어를 bat파일로 고정

4. 나중에 Jenkins가 그 bat를 호출하게 만들기

UAT(Unreal Automation Tool) : 패키징 자동화

BuildCookRun : 빌드, 쿠킹, 패키징, 배포/실행까지 한 흐름으로 처리할 수 있다.

 

Android 패키징 흐름

1. Build : 코드 컴파일 : 실행 가능한 바이너리 생성
2. Cook : 에셋을 타깃 플랫폼용으로 변환 (텍스처, 머티리얼, 맵 등 Android용으로 변환, Saved/Cooked/Android) 

3. Stage : 배포용 파일 모으기 (실제 배포에 필요한 파일을 한 폴더로 정리)

4. Package : APK/AAB 생성 (Gradle 호출)

 

결과물을 패키징을 위해, 기준이 있어야한다. (결정 값)

- Engine 경로

- Project 경로

- Config : Development

- Texture : ASTC

- Output : APK

- Map : 기본 맵

해당 기준을 정해 놓고, 이상태로 패키징이 됐는지 여부로 성공을 정하기로 했다. 최종 결과물인 Jenkins 자동화에서는 결정 값을 선택할 수 있도록 하는게 목표다.

 

BuildCook할 수 있는 .bat를 만든다.

@echo off
echo ===== UAT BUILD START =====

call "D:\UnrealEngine\UE_5.6\Engine\Build\BatchFiles\RunUAT.bat" BuildCookRun ^
-project="D:\UnrealEngine\Hamsterland\Hamsterland.uproject" ^
-noP4 ^
-platform=Android ^
-targetplatform=Android ^
-clientconfig=Development ^
-build ^
-cook ^
-stage ^
-package ^
-archive ^
-archivedirectory="D:\BuildOutput\Hamsterland" ^
-cookflavor=ASTC

echo.
echo EXIT CODE: %ERRORLEVEL%
pause

 

 

빌드중..

 

오류

* What went wrong:
Could not open settings generic class cache for settings file 'Z:\settings.gradle' (C:\Users\gkswl\.gradle\caches\7.5\scripts\356clt8w0kvd14538qat92yfl).
> BUG! exception in phase 'semantic analysis' in source unit '_BuildScript_' Unsupported class file major version 65

 

UE 5.4 Android 빌드에서 Gradle 7.5가 Java 21 캐시때문에 발생한 원인

 

원인 분석

Unsupported class file major version 65 에서 버전 65는 Java 21을 의미한다.

cook.bat에 JAVA_HOME을 별도로 지정하지 않으면, 시스템에 설치된 Java 또는 Android Studio는 JBR(JetBrains Runtime)이라는 자체 DK를 번들로 포함하고 있다.

 

Android Studio가 업데이트되면서 내장 JBR도 Java 21로 올라갔는데, UE 5.4가 사용하는 Gradle 7.5는 Java 17까지만 지원하기 때문에 충돌이 발생한다.

 

구분 버전
Android Studio 내장 JBR Java21 호환 안됨
Gradle 7.5 지원 최대 Java Java17

 

해결 방법

Eclipse Adoptium JDK 17 설치하고, cook.bat 상단에 JAVA_HOME을 명시했다.

@echo off
set JAVA_HOME=C:\Program Files\Java\jdk-17
set PATH=%JAVA_HOME%\bin;%PATH%

echo ===== UAT BUILD START =====

call "C:\Program Files\Epic Games\UE_5.4\Engine\Build\BatchFiles\RunUAT.bat" BuildCookRun ^
-project="D:\UnrealEngine\Hamsterland\Hamsterland.uproject" ^
-noP4 ^
-platform=Android ^
-targetplatform=Android ^
-clientconfig=Development ^
-build ^
-cook ^
-stage ^
-package ^
-archive ^
-archivedirectory="D:\BuildOutput\Hamsterland" ^
-cookflavor=ASTC ^
-stdout ^
-fullstdoutlogoutput

echo.
echo EXIT CODE: %ERRORLEVEL%
pause

 

set JAVA_HOME을 bat 안에서 지정하는 방식이기 때문에, 시스템 환경변수를 건드리지 않아도 된다.

bat 실행 범위 안에서만 적용된다.

 

빌드 성공

BUILD SUCCESSFUL in 1m 11s
68 actionable tasks: 67 executed, 1 up-to-date
...
AutomationTool exiting with ExitCode=0 (Success)
EXIT CODE: 0

 

  • Hamsterland-arm64.apk — 기기에 설치할 APK
  • Hamsterland-arm64.obb — 게임 데이터 파일
  • Install_Hamsterland-arm64-Development.bat — 기기에 자동 설치해주는 배치파일

 

Unreal 프로젝트 AOS빌드에서 발생한 오류

현재 빌드에 사용된 Java 버전이 너무 최신이라서 Gradle/Unreal 조합이 못 읽는 상황 65(JAVA21)

현재 프로젝트가 Android Studio 내장 jbr또는 환경 변수 JAVA_HOME이 Java21쪽을 가리키고 있는 이슈

 

해결방법

1. JDK 17 설치

2. 환경변수 수정, JAVA_HOME 을 JDK 17 설치 경로로 변경, Path 에 %JAVA_HOME%\bin 등록

3. Unreal 내부 JDK Path도 동일하게 변경(Project Settings -> Android의 JDK Path)

 

안드로이드 스튜디오 설치


언리얼 에디터 경로연결

SDK, NDK, JDK

SDK : 최종 결과물 만드는 역할 (Android API)

NDK : C++ -> Android용 바이너리(.so)로 변환 (게임 코드 컴파일)

JDK : 빌드 실행 엔진 (Gradle 실행, 전체 Android 빌드 프로세스 담당) 


타겟 플랫폼(Target Platforms)  설치


빌드

 

 

 

[Unreal Engine] -> C++ 코드(NDK) => .so 생성 -> Android 프로젝트 구성(SDK) => (JDK + Gradle) = APK 생성

 

 

#ASTC

텍스처 압축 방식(이미지 압축 포맷)

이번에는 Unity Timeline을 사용해서
두 캐릭터가 이동 → 짧은 전투 → 마무리까지 이어지는 컷씬을 만들어봤다.

동영상 서비스가 종료되어 해당 콘텐츠를 재생할 수 없습니다.

 

캐릭터 2명 동시 제어

카메라는 1개만 사용

컷씬 중에 오디오 / 파티클 / UI 페이드 / 컬링 레이어까지 같이 제어

전체 구성 요약

이번 컷씬 구성은 아래 기준으로 잡았다.

각 캐릭터는 Animator Track을 분리해서 관리하여
서로 다른 타이밍의 공격 / 피격 애니메이션을 독립적으로 제어 하도록 했다.

 

카메라

여러 카메라를 전환하는 방식도 있지만, 이번에는 메인 카메라 1개만 유지하는 구조로 갔다.

대신, 카메라 위치 / 각도 애니메이션 필요 시 컬링 레이어 변경

시작전, 컷씬 종료 후에 처리 해줘야한다.

사운드

Timeline의 Audio Track을 사용해서: 전투 시작 사운드, 공격 타격음 효과음 반복 구간을 전부 시간 축 기준으로 배치했다.

이펙트

Control Track을 활용해서: 타격 이펙트, 히트 파티클 를 처리했다. 코드에서 Play() 호출하는 구조보다
연출 수정이 빠르다고 생각했다.

UI Fade & 컷씬 전환 처리

컷씬 시작/종료 시: UI Fade In / Out, 컷씬 진입 시 입력 차단, 컷씬 종료 후 게임 상태 복구

이런 부분은 Timeline → 코드 트리거 방식으로 처리했다.

 

작업하면서 고민했던 포인트들

1. 캐릭터 이동은 Timeline vs 코드?

  • 이동 경로가 연출의 일부 → Timeline
  • 게임 로직 기반 이동 → 코드

이번 컷씬은 연출 중심이라
이동도 애니메이션 + Timeline으로 처리했다.

 

2. 왜 카메라를 하나만 썼나?

실무 기준으로 보면: 카메라 여러 개 → 전환 버그, 상태 복구 이슈 증가

 

느낀점

이번 작업에서 가장 중요한 포인트는 이거였다.

Timeline을 ‘연출 도구’가 아니라 시스템의 일부’로 설계했다는 점

캐릭터 / 카메라 / 사운드 / UI가 분리돼 있고 imeline은 이들을 시간 축에서 조합 하고

컷씬 종료 후에도 기존 게임 흐름에 영향 최소화 하도록 했다.

 

다음에는 TimeLine 중간에 UI버튼 인터렉션(Skip, QTE) 하는 기능을 넣어 볼 예정이다.

 

Unity Timeline

에디터에서 만든 컷씬 데이터를 런타임에서 PlayableGraph로 실행하는 시스템

 

 

간단한 예제부터, 컷씬 만들어보기

1. Timeline으로 Directional Light 색생변경

큐브 충돌 시 라이트 조명을 변경되도록 했다.

 

public class LightControlAsset : PlayableAsset
{
    public ExposedReference<Light> light;
    public Color color = Color.white;
    public float intensity = 1f;

    public override Playable CreatePlayable(PlayableGraph graph, GameObject owner)
    {
        var playable = ScriptPlayable<LightControlBehaviour>.Create(graph);

        var behaviour = playable.GetBehaviour();
        behaviour.light = light.Resolve(graph.GetResolver());
        behaviour.color = color;
        behaviour.intensity = intensity;
        return playable;
    }
}

ScriptPlayable은 사용자 정의 PlayableBehaviour를 PlayableGraph에서 실행 가능하게 감싸는 래퍼
graph.GetResolver는 Timeline에서 ExposedReference로 바인딩된 실제 씬 오브젝트를 런타임에 해석하기 위한 컨텍스다

[Serializable]
public class LightControlBehaviour : PlayableBehaviour
{
    public Light light;
    public Color color = Color.white;
    public float intensity = 1f;
    public float bounceIntensity = 1f;
    public float range = 10f;

    public override void ProcessFrame(Playable playable, FrameData info, object playerData)
    {
        //Light light = playerData as Light;
        if (light != null)
        {
            light.color = color;
            light.intensity = intensity;
            light.bounceIntensity = bounceIntensity;
            light.range = range;
        }
    }
}

 

2. 간단 연출 컷씬

 

 

추가적으로 알야할것

주요 키워드정리

PlayableAsset : 컷씬 데이터 (에셋, 직렬화 대상)
PlayableBehaviour  : 실행 로직 (런타임 객체)
Playable : Behaviour를 감싼 실행 단위
PlayableGraph  : 전체 실행 그래프 (Timeline 재생기)
Track : Playable을 어떻게 묶고 연결할지 정의
FrameData : 매 프레임 실행 컨텍스트 정보

PlayableAsset / PlayableBehaviour 씬 오브젝트가 아니다.

PlayableAsset -> ScriptableObject (에셋, 디스크에 있음), 에셋메모리

PlayableBehaviour -> 타임에 새로 생성되는 객체 (Managed Heap)

GC 대상은 PlayableBehaviour, 컷씬 종료 시 Graph 파괴, 같이 정리된다.

ProcessFrame은 언제(호출 주기) 호출될까?

PlayableGraph 평가 시
→ Director.Update()
→ Graph.Evaluate(deltaTime)
→ 각 Playable의 ProcessFrame 호출

MonoBehaviour.Update랑 같지 않음

 

ExposedReference란?

씬 오브젝트를 에셋에서 직접 참조하기 위한 우회 통로

 

 

참고

https://unity.com/kr/blog/engine-platform/extending-timeline-practical-guide

시스템 콜이란?

나무위키에 따르면

사용자 프로그램이 운영체제 커널에게 하드웨어,OS 자원에 대한 작업을 요청하는 공식적인 인터페이스이다.


이전에 User Mode, Kernel Mode를 알아야한다.

유저모드 User Mode 란?

일반 애플리케이션이 실행되는 모드이고 제한된 권한만 가진다.

게임 클라이언트, 브라우저, 에디터, 일반 프로그램

 

커널모드 Kernel Mode 란?

운영체제 핵심 코드가 실행되는 모드, 최고 권한 보유

메모리관리, 프로세스 스케줄링, 파일 시스템, 네트워크제어

모드를 2개로 나누는 이유

보안, 안정성, 자원 관리다.

만약에 모든 프로그램이 CPU,메모리,디스크, 네트워크에 직접 접근할 수 있다면

어떤 일이 벌이질까?

 

잘못된 포인터와 메모리 관리를 잘못하게 된다면,

OS전체 메모리에 오염될 수 있다.

즉, 한 프로그램의 버그가 시스템 전체가 망가질 수 있다는 얘기다.

 

근데, 프로그램을 개발하다보면, 디스크 접근해서 파일도  읽어오고,

네트워크도 쓰고있다. 

 

분명 유저모드에서는 디스크, 네트워크 접근은 안된다고 했는데??

 

시스템 콜 (System Call)의 역할

시스템 콜은 유저 모드 프로그램이 커널모드에게 요청을 전달하는 공식 통로라고 생각하면 된다.

read(fd, buffer, size);

 

이 한 줄의 코드는 아래와 같은 일이 발생한다.

1. 유저 모드에서 read()호출

2. CPU명령어를 통해 시스템 콜 발생

3. CPU 실행 모드 전환(User -> Kernel)

4. 커널이 요청 검증

5. 실제 디스크 접근 및 데이터 복사

6. 결과 반환 후 UserMode 복귀

 

시스템 콜은 비용이 비싸다

시스템 콜은 단순한 함수 호출이 아니다.

CPU모드 전환, 레지스터 저장/복구, 보안검사가 진행되는데

그래서 비용이 비싸다.

 

시스템 콜이 자주 일어나면?

- 성능저하 : 모드 전환 비용 누적, 캐시 오염, 파이프라인 플러시

- 컨텍스트 스위칭 증가 : IO대기, 블로킹 시스템 콜

- 프레임 드랍/ 지연발생 : 파일 IO / 네트워크 send/recv

 

시스템 콜을 줄이는 방법

시스템 콜은 "필요할 때만" 써야 하고, 핵심은 횟수를 줄이거나(배치/캐시), 메인 스레드에서 빼거나(비동기/스레드 분리)

커널 진입 자체를 피하는것이다.

 

1. 배치 처리 / 버퍼링 : 작은 작업을 여러 번 호출하지 말고 모아서 한번에 요청하도록 한다.

2. 캐시로 "확인/조회" 호출 줄이기 : 파일 존재 확인도 시스템 콜이 될 수 있음, 매프레임 조회X -> 이벤트기반으로 변경

3. 비동기 IO + 작업 스레드로 메인 루프에서 제거 : 시스템 콜을 없애지는 못해도, 메인 프레임 블락을 막지 않도록

4. 메모리 매핑(mmap)활용 

'STUDY > 운영체제' 카테고리의 다른 글

[OS] 데이터 레이스, 동기화와 락  (0) 2026.01.08
[컴퓨터 구조] 캐시이론 Locality  (0) 2021.09.23
운영체제) 교착상태 (deadlock)  (0) 2019.08.16

캐시 라인에서 False sharing까지 공부하다가 자주 언급되는 키워드들이 있어서

AoS와 SoA를 알게 되었다, 약자만 봐서는 무슨 내용인지 몰랐다. 

 

내용은 간단하게 설명하자면, 메모리 배치 방식의 따라 캐시효율과 성능이 달라진다는 점이다.

 

AoS (Array of Structures)

struct Entity
{
    float x, y, z;
    float vx, vy, vz;
};

vector<Entity> entities[N];

구조체를 담은 배열이다. 프로그래밍할때 자연스럽게 설계하는 방식이며,

특히 객체지향 프로그래밍에 적합한 방식이다. 

 

객체 단위 구조라 코드 가독성이 좋다는 특징이 있다.

다만 SoA보다 캐시 효율이 떨어질 수 있다.

 

SoA (Structure of Arrays)

struct Entities
{
    float x[N], y[N], z[N];
    float vx[N], vy[N], vz[N];
};

데이터 단위 구조이며 동일 타입 데이터가 연속으로 배치되어

캐시 효율이 좋은 구조이다.

하지만 코드 가독성이 매우 낮고, 객체 단위 접근이 불편하다는 특징이 있다.

 


SoA는 왜 AoS보다 캐시 효율이 좋을까?

예를 들어, 모든 엔티티의 x 좌표에 vx를 더하는 연산을 생각해 보자.

 

AoS

//AoS
for (int i = 0; i < N; ++i)
    entities.x[i] += entities.vx[i];

AoS방식은 x값을 변경하게 된다면,

캐시라인에는 x, y, z, vx, vy, vz까지 같이 존재해 더 많은 캐시라인을 불러와야 한다.

캐시 낭비, 캐시 라인 교체가 빈번해질 수 있다.

//AoS
| x y z vx vy vz | x y z vx vy vz | ...

SoA

//SoA
for (int i = 0; i < N; ++i)
    x[i] += vx[i];

 

반면에, SoA방식은 캐시라인에 연속적으로 존재하는 메모리들을 빠르게 접근할 수 있고,

x에 해당하는 캐시라인과 vx에 해당하는 캐리사인만 필요로 하기 때문에 캐시효율이 좋다는 것이다.

//SoA
| x x x x x x | y y y y y y | ...

캐시특징인 공간 지역성이 극대화된 구조이다.

 

즉,

SoA  :연속 접근 -> 캐시 히트 높음

AoS : 불필요 데이터 포함 -> 캐시낭비


캐시 라인과 파편화 관점

CPU는 메인 메모리 접근 비용을 줄이기 위해 캐시 라인 단위로 데이터를 로드한다.

이때 캐시는 연속된 메모리에 접근할수록, 그리고 같은 캐시 라인을 오래 재사용할수록 높은 효율을 낸다.

 

반대로 메모리 상에서 데이터 흩어져 있거나, 접근 패턴이 불규칙한 경우에는 캐시 라인이

자주 교체되며 캐시미스가 증가한다.

 

AoS는 위 설명과 같이 불필요한 데이터 캐시로드와 라인 활용도가 떨어지고,

SoA 구조는 동일한 속성의 데이터가 메모리 상에 연속적으로 배치되어 CPU가 필요한

데이터만 효율적으로 캐시에 적재해, 캐시 공간 지역성이 이점을 가져간다.


언제 어떤 방식을 선택해야 할까?

SoA설계방식이 AoS보다 더 캐시친화적이고, 성능이 좋다고 하지만

가독성이나 OOP프로그래밍에는 적합하지 않은 부분이 많다. 성능보다는

생산성이나 유지보수성이 더 중요한 경우가 대부분이다.

 

당연히 사용용도에 맞게 적합한 방법을 선택하는 것이 중요하겠다.

일반적인 게임 로직이나 객체관리가 더 중요한 부분에서는 AoS를,

대랑 연산, 데이터 지향 코드나 극도의 성능을 요구하는 부분에서는 부분 SoA를 사용하는 것이

현실적인 선택일 것 같다.

 

 

'STUDY' 카테고리의 다른 글

2D충돌 알고리즘 AABB, OBB, SAT  (0) 2025.12.09
visual SVN 설치  (0) 2019.06.17

어떤 프로세스 안에서 생성된 스레들은 같은 메모리 공간을 공유한다.

스레드가 같은 자원을 공유할 수 있다는 점은 장점이기도 하지만,

반대로 두 스레드가 같은 자원을 동시에 변경하는 경우에는 문제가 된다.

 

동기화 이슈를 강제로 발생시켜보고

어떻게 막으면 좋을지 한번 찾아보았다. 

 

데이터 레이스 예시

//AI에게 부탁한 코드샘플.
static constexpr int kThreads = 8;
static constexpr int kItersPerT = 2'000'000;

volatile bool gStart = false;     // 동시 시작 타이밍 맞추기(간단용)
std::int64_t gShared = 0;         // 의도적 데이터 레이스 대상

void RaceWorker()
{
    while (!gStart) 
    { }

    for (int i = 0; i < kItersPerT; ++i)
    {
        ++gShared; // 데이터 레이스!
    }
}

int main()
{
    std::vector<std::thread> threads;
    threads.reserve(kThreads);

    for (int i = 0; i < kThreads; ++i)
        threads.emplace_back(RaceWorker);

    gStart = true; //가시성

    for (auto& t : threads)
        t.join();

    const std::int64_t expected = static_cast<std::int64_t>(kThreads) * kItersPerT;

    std::cout << "Expected : " << expected << "\n";
    std::cout << "Actual   : " << gShared << "\n";

    if (gShared != expected)
        std::cout << "Race detected (lost updates)\n";
    else
        std::cout << "No mismatch this run (race may still exist)\n";

    return 0;
}

 

 

결과는 8개의 스레드가 동시에 접근하면서 데이터 레이스가 발생했다.

이러한 이슈들을 막기 위해 동기화 도구들이 필요하다.

 

복합연산 이 연산은 단순한 더하기 연산 같지만, 

++gShared; // 데이터 레이스!

 

내부적으로는 아래 load, add, store 3단계가 존재한다.

1. 메모리에서 gShared 읽기 (load)

2. 값 + 1 계산 (add)

3. 메모리에서 다시 쓰기 (store)

 

예를들어, 스레드A가 gShared를 add하고 있는 상태에서

또 스레드B가 add를 할 수 있는 상황이 발생 할 수 있다.

그러면 데이터 레이스가 생긴다.

 

Lock

대표적으로 락이다. 공유 자원에 붙이면 해당 자원에 대한 접근을 동기화할 수 있다.

스레드가 해당 자원을 접근하려면 우선 그 자원에 붙어 있는 락을 획득 해야한다.

 

락은 한 스레드만 쥐고 있을 수 있다.

그래서 공유 자원은 한 번에 한 스레드만 사용할 수 있게 하는것이다.

 

lock은 스레드 전환에 컨텍스트 스위칭과 데드락에 위험이 존재한다.

 

std::mutex gMutex;

void RaceWorker()
{
    while (!gStart) 
    { }

    for (int i = 0; i < kItersPerT; ++i)
    {
        std::lock_guard<std::mutex> lock(gMutex);
        ++gShared; 
    }
}

 

 

 

atomic 연산자

std::atomic<std::int64_t> gShared = 0;

void RaceWorker()
{
    while (!gStart) 
    { }

    for (int i = 0; i < kItersPerT; ++i)
    {
        ++gShared; // 데이터 레이스!
    }
}

 

아토믹 연산자를 사용하면 lock없이도 데이터 레이스를 방지할 수 있다.

아토믹 연산자는 load, add, store하는 3단계가 아닌

CPU레벨에서 하나의 원자 연산으로 보장된다.

 

lock없이도 데이터 레이스에 피할 수 있지만,

여러 변수,조건 + 분기, 컨테이너 조작에 대해서는 적합하지 않다.

 

오늘 포스팅한 샘플처럼 카운트 수정이나

단일 변수 수정에 적합하다.

'STUDY > 운영체제' 카테고리의 다른 글

OS) 시스템 콜이란? (System Call)  (0) 2026.01.22
[컴퓨터 구조] 캐시이론 Locality  (0) 2021.09.23
운영체제) 교착상태 (deadlock)  (0) 2019.08.16

리플렉션이란 프로그램이 런타임에 자기 자신을 조사하는 기능이다.

C++에는 리플렉션이란 기능이 없다. 그래서 언리얼이 리플렉션을 제공한다.

아무래도 개발편의성 측면에서 필요한 시스템이라 판단해서 넣지 않았을까..?

 

언리얼에 리플렉션은, 런타임과 동시에 에디터에서 타입 정보를 보일 수 있게 하고, 조작도 할 수 있게 해주는 시스템이다.

자주쓰는 [UCLASS],[USTRUCT],[UENUM],[UPROPERTY],[UFUCTION]같은 매크로가 대표적인 예이다.

 

언리얼에 리플렉션에는 가능한것들이 에디터에서 변수 노출/수정, 디테일 패널, 직렬화 / 세이브|로드, 블루프린트 노출, GC추적, FindFunction, ProcessEvent같은 동적 호출/ 바인딩 등등이 있다.

 

UHT(Unreal Header Tool) 이 매크로들을 분석하여 코드를 자동으로 생성한다.

이때 생성된 코드가 타입에 관련된 메타정보이다.

 

UHT에 정의를 찾아보면

UHT는 언리얼 빌드 과정에서 C++헤더를 스캔하여, 리플렉션이 필요한 타입의 메타데이터 코드를 자동으로 생성하는 전처리 도구이며 C++컴파일러 이전 단계에서 .h(헤더파일)을 분석하여 정보를 생성한다.

 

그런데 UHT는 어떻게 이런 매크로를 탐지하여 코드를 자동으로 생성할까?

빌드 시 build.cs에서 UBT(Unreal Build Tool)가 실행된다. 그런 다음에 UHT는 위에 언급한 매크로처럼 작성된 패턴을 찾는다. 

이 매크로들은 C++매크로 역할과 UHT인식용 표식 역할을 동시에한다.

 

UCLASS()
class AMyActor : public AActor

 

UHT는 이런 코드를 보고, 이 클래스는 리플렉션 대상이구나. 판단하게된다.

 

그럼 UHT가 생성하는 코드는 무엇일까?

UHT는 .generated.h파일을 생성한다. (ex MyActor.generated.h)

이 안에는 다음과 같은 코드들이 들어간다. 

타입 등록코드(클래스 이름, 프로퍼티 목록, 함수 목록을 엔진 내부 리플렉션 시스템에 등록), 

UPROPERTY : 변수 타입, 이름, 오프셋, 메타 태그 GC추적 여부

UFUNCTION : 메타정보, 호출래핑

RPC / Replication 관련 코드 : 어떤 변수가 어떤 조건에서 어떤 네트워크 정책으로 동기화되는지 정의

GC 지원 코드 : UObject포인터를 UPROPERTY로 표시했는지, 참조 그래프에서 포함시켜 Mark & Sweep 대상으로 만듦

 

즉, UHT는 빌드 시점에 헤더 파일을 검사해서 UCLASS, UPROPERTY같은 매크로가 붙은 타입을 찾아 리플렉션용 메타데이터와 접착 코드(C++과 엔진의 공용 시스템을 연결해주는 중간 코드)를 생성한다.

이렇게 생성된 .generated.h, gen.cpp에는 타입 등록, 프로퍼티 정보, RPC/블루프린트 호출 래퍼,GC추적을 위한 코드가 포함된다.

 

이를 통해 언리얼은 C++타입을 에디터와 런타임에서 동적으로 다룰 수 있게된다.

https://school.programmers.co.kr/learn/courses/30/lessons/42587?language=csharp

 

프로그래머스

SW개발자를 위한 평가, 교육의 Total Solution을 제공하는 개발자 성장을 위한 베이스캠프

programmers.co.kr

 

using System;
using System.Collections.Generic;
public class Solution {
    public int solution(int[] priorities, int location) {
            Queue<(int priority, int index)> q = new Queue<(int, int)>();

            for (int i = 0; i < priorities.Length; i++) 
            {
                q.Enqueue((priorities[i], i));
            }

            int count = 0;

            while (q.Count > 0) 
            {
                var cur = q.Dequeue();

                bool isFind = false;

                foreach (var item in q) 
                {
                    if (cur.priority < item.priority)
                    {
                        isFind = true;
                        break;
                    }
                }

                if (isFind)
                {
                    q.Enqueue(cur);
                }
                else
                {
                    count++;
                    if(cur.index == location)
                        return count;
                }
            }

            return count;
    }
}

 

 

큐를 이용한 시물레이션 문제로,

1. 큐에는 원래 위치와, 우선 순위를 저장한다.

2. 맨앞으로 꺼낸뒤 그 뒤에 우선순위가 더 높은 값이 있는지 찾아본다

3. 우선순위가 높은 문서가 있다면 다시 큐에 넣는다.

4. 없다면 꺼낸 갯수를 증가 시킨다.

5. 위 과정을 큐가 0이 될때까지 반복한다. 

https://school.programmers.co.kr/learn/courses/30/lessons/42586

 

풀이

 List<int> answer = new List<int>();

 int n = progresses.Length;
 int[] days = new int[n];

 for (int i = 0; i<n; i++)
 {
     int remain = 100 - progresses[i];
     days[i] = (remain + speeds[i] - 1) / speeds[i];
 }

 int count = 1;
 int curReleaseDay = days[0];
 for (int i = 1; i< n; i++)
 {
     if (curReleaseDay >= days[i])
     {
         count++;
     }
     else
     {
         answer.Add(count);
         curReleaseDay = days[i];
         count = 1;
     }
 }

 answer.Add(count);

 return answer.ToArray();

 

잘못되 접근 방식 : 시뮬레이션 방식으로 하루 하루 더해서 완료가 되는 시점으로 계산하려고 했음

(문제 자체를 시간에 흐름으로 처리하려고 했음)

 

올바른 접근방식 : 

하루가 지남에 따라 완료 시점을 그때마다 확인하는 것이 아니라, 각 기능이 언제 완료되는가를 먼저 구하는 방식이 맞다.

 

이번 문제에서 알아야할 나눗셈 처리

 

        for (int i = 0; i<n; i++)
        {
            int remain = 100 - progresses[i];
            days[i] = (remain + speeds[i] - 1) / speeds[i];
        }

 

1을 빼는 이유는???

왜 단순히 remain / speed를 쓰면 안될까?

C#의 정수 나눗셈은 항상 소수점을 버린다.(내림)

ex) remain = 80, speed = 30일때, remain / speed = 2가 나온다.

 

Day 1 : 0 -> 30

Day 2 : 30 -> 60

Day 3 : 60 -> 90 (완료)

 

하지만 완료되는 시점은 3이다.

그러면 remain에다가 speed만 더하고 나누면 되지 않나? 생각하지만,  그렇다면 -1은 왜필요할까? 

위 예시 remain = 80, speed = 30인 경우는 맞다.

 

하지만, 나눗셈이 딱 떨어지는경우

ex) remain = 60, speed, 30일때는 맞지 않다.

Day 1 : 0 -> 30

Day 2 : 30 -> 60(완료)  2일걸림

 

그래서 나눠 떨어지지 않는 경우와 나눠 떨어지는 경우

모두 고려해서 -1처리가 필요하다.

 

문제를 마무리하면서 느낀점은 과정을 시뮬레이션 하지말고, 결과가 결정되는 시점을 계산하는 방식으로 접근하자..!!!

헷갈리는 키워드를 정리하기 위해 포스팅하게됐다.

1. async / await

- 비동기 프로그래밍 문법

- 실행 흐름을 잠깐 끊었다가, 끝나면 다시 이어서 실행하는 문법

using System.Threading.Tasks;
using UnityEngine;

public class LoginExample : MonoBehaviour
{
    public async Task<string> LoginAsync(string username, string password)
    {
        Debug.Log("로그인 시작");

        // 1초 대기 (메인 스레드를 블로킹하지 않음)
        await Task.Delay(1000);

        Debug.Log("1초 뒤 실행");

        return "Token";
    }
}

- Task.Delay동안 메인스레드는 멈추지 않는다.

- await는 잠시 대기 일뿐이다.

- await가 끝나면 다시 원래 스레드로 돌아간다. 

즉, 이 함수에서는 메인 스레드 외의 다른 워커 스레드를 사용하지 않고 있다.

 

C# Task는 Unity에서 쓰기에는 적합하지 않다.

- GC/할당부담

- Unity 오브젝트 라이프사이클과 매칭

- 유니티 프레임루프와 연결이 느슨함

그래서 선택지는 UniTask다

 

2. UniTask

UniTask는 유니티의 프레임루프 기준으로 동작하도록 만들어진

Unity친화적인 비동기 라이브러리다.

public async UniTask LoginAsync()
{
    await UniTask.Delay(1000);

    // 여기부터 다시 Unity 메인 스레드
    Debug.Log("1초 뒤 실행");
}

가장 큰 특징중에 하나는 await이후에 다시 유니티 메인스레드로 복귀 한다는 점이다.

(await이후 unity api 접근 가능하다는 말)

 

3. 언제 멀티스레드가 되나?

비동기를 사용하면 멀티스레드가 된다고 생각했는데, 아니었다.

명시적으로 워커 스레드를 사용했을때만 멀티스레드가 된다. 

await UniTask.Delay(1000);
//메인스레드
await UniTask.RunOnThreadPool(() =>
{
     //워커스레드
    HeavyWork();
});

//메인 스레드 복귀

 

4. 비동기와 멀티스레드 비교

비동기

- 메인 스레드를 막지 않고 대기 방식이라는것이 핵심이다

- 스레드는 하나 일 수 있다

- 주로 네트워크나 파일I/O, 타이머

=> 기다리는 방식

 

멀티스레드

- 병렬 실행이 핵심

- 스레드가 여러개

- 실행로직을 분리해 무거운 연산에 적합하다.

=> 실행을 나누는 방식

 

5. 코루틴과 비교

코루틴은 비동기가 아니다. 단지 메인스레드에서 비동기적 흐름을 흉내낸것이 맞다. 유니티에서는

유니티 메인스레드에서 실행흐름을 쪼개서 비동기처럼 보이게 만드는 구조이다.

 

 

6. 멀티스레드는 만능인가?

복잡한 연산을 멀티스레드로 구현하면, 플레이에 프리징을 줄일 수 있다.

하지만 만능은 아님

- 스레드 관리비용

- 컨텍스트 스위칭 오버헤드

- 모바일 기기에서는 발열

- CPU 스레드 수 제한등

이런것들을 잘 고려하지 못한다면 오히려 더 나빠질 수 있다.

 

+ Recent posts