반응형

하드코딩으로 액터를 배치하던 방식을 개선해, 텍스트 파일에 작성된 맵 데이터를 읽어 게임 월드를 자동으로 생성하는 구조를 구현했다. 

1. 맵 데이터 준비

##########
#........#
#........#
#..p..b..#
#.....b..#
#........#
#........#
#......t.#
#......t.#
##########

각 문자는 하나의 타일 또는 액터를 의미한다.

# : 벽

. : 바닥

p : 플레이어

b : 박스

t : 목표 지점

2. 하드코딩 방식의 문제

기존에는 코드에서 액터를 직접 생성해야 했다.

SpawnActor<Wall>(Vector2(0, 0));
SpawnActor<Wall>(Vector2(1, 0));
SpawnActor<Ground>(Vector2(1, 1));

이 방식은 맵이 커질수록 코드가 길어지고, 스테이지를 변경할 때마다 소스 코드를 수정해야 하는 문제가 있다.

3. GameLevel의 역할

TestLevel 대신에 GameLevel로 변경, 게임 시작 레벨이다.

class GameLevel : public Level
{
public:
	virtual void OnInitialized() override;
	virtual void Draw() override;

private:
	void LoadMap(const std::string& filename);

private:
	int targetScore = 0;
	bool isGameClear = false;
};

주요 멤버의 역할은 다음과 같다.

- targetScore : 맵에 배치된 목표 지점 개수

- isGameClear : 게임 클리어 여부

- OnInitialized() : 레벨 초기화 시 맵 로드

- LoadMap() : 파일을 읽고 액터 생성

레벨 초기화 시 맵을 로드한다.

void GameLevel::OnInitialized()
{
	LoadMap("Map.txt");
}

 

그리고 Main.cpp 에서는 GameLevel을 생성하도록 변경한다.

Engine::AddNewLevel<GameLevel>();

4. 맵 파일 읽기

맵 파일은 std::ifstream을 이용해 읽는다.

void GameLevel::LoadMap(const std::string& filename)
{
	std::ifstream file(filename, std::ios::binary);

	if (file.is_open() == false)
	{
		return;
	}

	file.seekg(0, std::ios::end);
	const std::streamsize fileSize = file.tellg();
	file.seekg(0, std::ios::beg);

	std::string buffer(static_cast<size_t>(fileSize), '\0');
	file.read(buffer.data(), fileSize);

	// buffer 파싱
}

파일 전체를 하나의 문자열로 읽은 다음, 문자를 하나씩 순회하며 맵을 해석한다.

5. 문자 단위 파싱과 좌표 계산

맵 데이터를 읽을 때 현재 위치를 나타내는 x,y 좌표를 사용한다.

Vector2 position;

for (const char mapCharacter : buffer)
{
	if (mapCharacter == '\r')
	{
		continue;
	}

	if (mapCharacter == '\n')
	{
		position.x = 0;
		position.y++;
		continue;
	}

	// 현재 문자 해석
	position.x++;
}

 

Windows의 줄바꿈은 보통 \r\n 으로 구성된다.

\r을 무시하지 않으면 줄바꿈마다 x좌표가 한 칸 증가하게된다. 그 결과 다음 줄의 액터 위치가 어긋날 수 있다.

 

6. 문자와 액터 매핑

각 문자는 switch문을 사용해 액터 생성 로직으로 변환한다.

switch (mapCharacter)
{
	case '#':
		AddActor<Wall>(position);
		break;

	case '.':
		AddActor<Ground>(position);
		break;

	case 'p':
		AddActor<Ground>(position);
		AddActor<Player>(position);
		break;

	case 'b':
		AddActor<Ground>(position);
		AddActor<Box>(position);
		break;

	case 't':
		AddActor<Target>(position);
		targetScore++;
		break;
}

문자와 액터의 매핑 구조는 다음과 같다.

문자생성 액터

# Wall
. Ground
p Ground + Player
b Ground + Box
t Target

 

7. 플레이어와 박스에 Ground를 함께 생성하는 이유

플레이어와 박스는 바닥 위에서 배치되는 이동 오브젝트다. 하나의 문자만 보고 플레이어나 박스만 생성하면 바닥이 존재하지 않게 된다.

 

렌더링 순서는 sortingOrder로 조정한다.

- Ground : 낮은 순서

- Box : 중간 순서

- Player : 높은 순서

 

8. 기본 액터 클래스 구성

클래스표시 문자색상정렬 순서

Wall # 기본 색상 기본
Ground 빈 문자열 기본 색상 기본
Player P 초록색 5
Box B 빨간색 3
Target T 파란색 기본

 

각 액터에는 기존 강의에서 구현한 커스텀 RTTI매크로도 적용한다.

class Player : public Actor
{
	TYPE_DECLARATIONS(Player, Actor)
};
반응형
반응형

C++에 dynamic_cast에 의존하지 않고, 엔진에서 직접 타입 확인과  형변환을 처리하는 커스텀(RTTI) 시스템 구현

핵심은 모든 엔진 객체의 공통 부모 클래스인 CraftObject에 타입 ID 체계를 만들고, 상속 관계를 따라 타입을 확인할 수 있도록 하는 것이다.

1. 커스텀 RTTI가 필요한 이유

일반 C++에서는 dynamic_cast를 사용해 객체의 실제 타입을 확인할 수 있다.

TestActor* testActor = dynamic_cast<TestActor*>(actor);

 

하지만 엔진에서는 다음과 같은 이유로 자체 타입 시스템을 사용한다.

- C++ RTTI 사용 여부를 직접 제어할 수 있음

- 엔진 객체의 타입 확인 방식을 통일할 수 있음

- 상속 구조에 맞는 타입 검사 로직을 직접 설계할 수 있음

- 엔진의 캐스팅 규칙을 명확하게 관리할 수 있음

 

2. CraftObject 설계

모든 타입 시스템 객체의 최상위 부모로 CraftObject를 추가했다.

class CRAFT_API CraftObject
{
public:
	virtual size_t GetType() const = 0;

	virtual bool Is(size_t id) const
	{
		return false;
	}

	template<typename T>
	bool IsTypeOf() const
	{
		return Is(T::TypeId());
	}
};

 

각 함수의 역할

- GetTYpe() : 현재 객체의 정확한 타입 ID 반환

- Is(size_t id) : 현재 타입 또는 부모 타입인지 확인

- IsTypeOf<T>() : 템플릿을 이용한 타입 확인

// 예시
if (actor->IsTypeOf<TestActor>())
{
	// TestActor 타입인 경우
}

3. shared_ptr 기반 Cast 구현

타입을 확인한 뒤 안전하게 static_pointer_cast 를 수행하는 캐스팅 함수 추가

template<typename T, typename U>
std::shared_ptr<T> Cast(const std::shared_ptr<U>& object)
{
	if (!object)
	{
		return nullptr;
	}

	if (object->Is(T::TypeId()))
	{
		return std::static_pointer_cast<T>(object);
	}

	return nullptr;
}

std::static_pointer_cast :  C++에서 std::shared_ptr 객체의 타입을 컴파일 타임에 안전하게 변환(캐스팅)해 주는 함수

// 예시
std::shared_ptr<TestActor> testActor = actor->Cast<TestActor>(actor);

if (testActor != nullptr)
{
	// 캐스팅 성공
}

먼저 Is() 로 타입을 확인하기 때문에, 타입이 다르면 nullptr 반환

4. TYPE_DECLARATIONS 매크로

각 클래스에 필요한 타입 관련 함수를 반복해서 작성하지 않도록 매크로 추가

#define TYPE_DECLARATIONS(Type, ParentType) \
	using super = ParentType; \
protected: \
	static size_t TypeIdClass() \
	{ \
		static int runtimeTypeId = 0; \
		return reinterpret_cast<size_t>(&runtimeTypeId); \
	} \
public: \
	static size_t TypeId() \
	{ \
		return Type::TypeIdClass(); \
	} \
	virtual size_t GetType() const override \
	{ \
		return Type::TypeIdClass(); \
	} \
	virtual bool Is(size_t id) const override \
	{ \
		return (id == TypeIdClass()) ? true : ParentType::Is(id); \
	}

 

매크로가 생성하는 주요 기능

- 부모 타입 별칭 생성

- 클래스별 타입 ID 생성

- 현재 객체의 타입 반환

- 현재 타입 또는 부모 타입 여부 확인

5. 타입 ID 생성 방식

각 클래스의 TypeIdClass() 에는 정적 지역 변수가 있다.

static int runtimeTypeId = 0;
return reinterpret_cast<size_t>(&runtimeTypeId);

 

정적 지역 변수는 프로그램 실행 중 하나만 생성되며, 클래스별 함수에 각각 존재한다.

따라서 각 클래스의 runtimeTypeId 주소가 서로 달라지고, 그 주소를 타입ID로 사용할 수 있다.

이 타입 ID는 파일이나 네트워크에 저장하는 값이 아니라, 프로그램 실행 중 타입을 구분하기 위한 임시 식별자다.

6. 상속 체인에서 Is 동작 방식

Actor는 다음과 같이 시스템을 적용한다.

class Actor : public CraftObject
{
	TYPE_DECLARATIONS(Actor, CraftObject)
};

 

TestActor는 Actor 를 부모로 지정

class TestActor : public Actor
{
	TYPE_DECLARATIONS(TestActor, Actor)
};

 

TestActor의 Is()는 먼저 자신의 타입 ID를 확인한다.

if (id == TestActor::TypeId())
{
	return true;
}

 

자신의 타입이 아니라면 부모인 Actor::Is(id) 에 확인을 위임한다.

이 구조로 TestActor 객체는 다음 타입 질문에 모두 True를 반환하게 된다.

testActor->IsTypeOf<TestActor>();
testActor->IsTypeOf<Actor>();

 

7. super 별칭

매크로에는 부모 클래스 super라는 이름으로 사용할 수 있도록 별칭을 추가

using super = ParentType;

 

실제 사용 예시

void TestActor::Tick(float deltaTime)
{
	super::Tick(deltaTime);
}

Unreal Engine의 GENERATED_BODY()가 생성하는 Super 별칭과 비슷한 사용 방식이지만,

이번 강의의 super는 직접 만든 일반 C++ 매크로 기능이다.

8. TYPE_DECLARATIONS를 사용하지 않으면?

파생 클래스에 매크로를 추가하지 않으면 해당 클래스의 타입 정보가 등록되지 않는다.

class TestActor : public Actor
{
	// TYPE_DECLARATIONS가 없음
};

그러면 Actor 정보만 사용하게 된다. 

반응형
반응형

코드에 하드 코딩 되어있는 엔진 설정값(fram rate, scrren size 등을) 파일로 로드하여 적용 하도록 하는 작업

1. Engine클래스

- Setting.txt 파일로 해당 파일을 읽는 코드 작성

현재  Setting.txt에 정의된 항목

# targer framerate
framerate = 120;
# screen width
width = 60;
# screen height
height = 25;

 

- 빌드 후 이벤트를 이용해서, 엔진 빌드할때 Config.txt파일을 게임코드도 확인 할 수 있도록 처리.

 

여기에 있던 엔진 쪽 경로에 폴더를 ConsoleGameProject\Config 게임코드에서 참조하는 ConsoleGameProject\Bin\Debug\Config 경로로 빌드하면 자동으로 txt파일을 복사하도록 설정하는 작업

반응형
반응형

2중 버퍼링을 구현하기 위해, ScreenBuffer 클래스에서는 Clear와 Draw를 구현, 

1. ScreenBuffer 클래스

Clear 함수

 

Draw 함수

 

2. Renderer 클래스

배열 2개로 교체하면서 사용하도록.

DrawRenderQueue 함수

- 화면 경계 클리핑

- sortingOrder기반 문자 셀 우선순위 처리

- ScreenBuffer 출력 + Present 버퍼 스왑

반응형
반응형

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

이 문제를 해결하기 위해 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

+ Recent posts