게임그래픽에서 타일이란 용어를 자주 듣게 되는데 이 개념이 무엇일까?
이번 강좌는 타일의 구조에 대한 얘기를 해보자.
타일의 사전적인 개념은 ‘반복되어지는 패턴(문양)’이다. 이것은 쉽게
욕실의 벽이나 바닥을 연상하면 된다.
하지만 게임에서의 타일은 이 개념처럼 간단하지는 않다.
예를 들어 숲과 바다 지형을 타일화 한다면 기본적으로 숲을 표현할 수
있는 base 타일이 하나 있어야 하겠고 역시 마찬가지로 바다를 표현할 수
있는 Base 타일이 있어야 한다. 이 두 타일을 반복적으로 나열하게 되면
욕실의 타일과 같은 개념이다. 그러나 게임의 배경은 이 두 지형이 만나는
부분이 생기게 마련이고 이 연결부분을 어떻게 처리해주어야 할지가 고민이
다. 그렇다면 어떤 모양이든 다양하게 만들어서 붙이면 될 거라고 생각할
수 있으나 그렇다면 타일의 효용성이 떨어지게 될 것이고 맵 에디터로
맵을 찍을 수 있는 규칙성조차 없어지기 때문에 곤란하다. 그렇다면 적은
용량으로 이 문제를 해결할 수 있는 타일의 기본 구조는 무엇일까?
이번 강좌에서 자세히 알아보자.
왜 타일 배경을 쓰는가!
앞서 말한 타일의 효용성이라는 것이 적은 양으로도 큰 이미지를 만들어
낼 수 있다는 점(메모리 사용의 효율성)과 Map Editor를 통해 편리하게
자동생성 할 수 있고 여러 모양으로 대량생산이 가능하다는 점이다.
그래서 많은 유닛이나 오브젝트가 등장하고 빠른 속도를 필요로하는 게임에
서는 필수적인 요소라고 볼 수 있다.
장르별로 볼 때 슈팅, 전략시뮬레이션, 롤플레잉 등의 게임에 많은 수가
이 방식을 사용하고 있다. 물론 기계화된 모양 때문에 부자연스러운 이미지
가 생산 될 수 있다는 단점도 있지만 이를 극복하기 위해 타일의 모양(정사
각형, 직사각형, 마름모등)도 다양해지고 기본 모양에서 변화된 타일의
개수 역시 많아지고, 높은 언덕이나 절벽 등을 표현하기 위해 오브젝트
타일(map타일과 스프라이트가 결합된 형태)도 많이 사용하고 있다.
우리 엔진에서는 이러한 타일 오브젝트를 렌더링 하기 위해 따로 타일맵렌더러라는 컴포넌트를 제작했다. 기본적인 렌더링구조는 spriteRenderer와 같지만 이미지를 그려줄때 이미지 소스의 시작위치와 끝위치를 정하는곳이 다르다.
if (mTexture->IsAlpha())
{
BLENDFUNCTION func = {};
func.BlendOp = AC_SRC_OVER;
func.BlendFlags = 0;
func.AlphaFormat = AC_SRC_ALPHA;
func.SourceConstantAlpha = 255; // 0(transparent) ~ 255(Opaque)
AlphaBlend(hdc
, pos.x, pos.y
, mTileSize.x * mSize.x * scale.x
, mTileSize.y * mSize.y * scale.y
, mTexture->GetHdc()
, mIndex.x * mTileSize.x, mIndex.y * mTileSize.y
, mTileSize.x
, mTileSize.y
, func);
}
else
{
//https://blog.naver.com/power2845/50147965306
TransparentBlt(hdc
, pos.x, pos.y
, mTileSize.x * mSize.x * scale.x
, mTileSize.y * mSize.y * scale.y
, mTexture->GetHdc()
, mIndex.x * mTileSize.x, mIndex.y * mTileSize.y
, mTileSize.x
, mTileSize.y
, RGB(255, 0, 255));
}
C++
복사
그리고 타일이미지의 모음에서 어떤이미지를 그려줄것인지 세팅해주는 값들을 헤더에 추가해 주었다.
class TilemapRenderer : public Component
{
public:
TilemapRenderer();
~TilemapRenderer();
void Initialize() override;
void Update() override;
void LateUpdate() override;
void Render(HDC hdc) override;
void SetTexture(graphics::Texture* texture) { mTexture = texture; }
void SetSize(math::Vector2 size) { mSize = size; }
Vector2 GetIndex() { return mIndex; }
void SetIndex(Vector2 index) { mIndex = index; }
public:
static Vector2 TileSize;
static Vector2 OriginTileSize;
static Vector2 SelectedIndex;
private:
Vector2 mTileSize;
graphics::Texture* mTexture;
Vector2 mSize;
Vector2 mIndex;
C++
복사
타일을 그리기 위한 렌더러는 준비 됬다.
이제 타일맵을 만들기위한 툴을 만들어보자.
ToolScene을 새로 생성해주고 해당 씬에 타일을 깔기위한 그리드(격자)를 그려주자.
void ToolScene::renderGreed(HDC hdc)
{
for (size_t i = 0; i < 50; i++)
{
Vector2 pos = renderer::mainCamera->CaluatePosition
(
Vector2(TilemapRenderer::TileSize.x * i, 0.0f)
);
MoveToEx(hdc, pos.x, 0, NULL);
LineTo(hdc, pos.x, 1000);
}
for (size_t i = 0; i < 50; i++)
{
Vector2 pos = renderer::mainCamera->CaluatePosition
(
Vector2(0.0f, TilemapRenderer::TileSize.y * i)
);
MoveToEx(hdc, 0, pos.y, NULL);
LineTo(hdc, 1000, pos.y);
}
}
C++
복사
winAPI에서는 moveToEx함수로 선을 그려줄수 있다.
추가적으로 오른쪽에 윈도우를 한개 더 생성하여 타일이미지 모음을 그러주자.
window를 생성해줄때 기존 윈도우를 만들고 추가적으로 다음과 같은 함수를 호출해주자
앞서 첫시간에 우리가 윈도우를 띄우는 과정과 거의 동일하다.
BOOL InitToolScene(HINSTANCE hInstance)
{
ya::Scene* activeScene = ya::SceneManager::GetActiveScene();
std::wstring name = activeScene->GetName();
if (name == L"ToolScene")
{
HWND ToolHWnd = CreateWindowW(L"TILEWINDOW", L"TileWindow", WS_OVERLAPPEDWINDOW,
0, 0, CW_USEDEFAULT, 0, nullptr, nullptr, hInstance, nullptr);
//Tile 윈도우 크기 조정 -- TOOL
ya::graphics::Texture* texture
= ya::Resources::Find<ya::graphics::Texture>(L"SpringFloor");
RECT rect = { 0, 0, texture->GetWidth(), texture->GetHeight() };
AdjustWindowRect(&rect, WS_OVERLAPPEDWINDOW, false);
UINT toolWidth = rect.right - rect.left;
UINT toolHeight = rect.bottom - rect.top;
SetWindowPos(ToolHWnd, nullptr, 672, 0, toolWidth, toolHeight, 0);
ShowWindow(ToolHWnd, true);
UpdateWindow(ToolHWnd);
}
return TRUE;
}
C++
복사
이제 클릭하는 위치에 맞추어 타일오브젝트를 생성해주면 된다. 나누기 연산자를 이용해 해당 타일 안에 있는 경우에는 모두 같은 좌표로 처리하여 타일크기에 맞게 규칙적으로 오브젝트가 배치되게끔 해준다.
void ToolScene::LateUpdate()
{
Scene::LateUpdate();
if (Input::GetKeyDown(eKeyCode::LButton))
{
createTileObject();
}
if (Input::GetKeyDown(eKeyCode::S))
{
Save();
}
if (Input::GetKeyDown(eKeyCode::L))
{
Load();
}
}
void ToolScene::createTileObject()
{
Vector2 pos = Input::GetMousePosition();
pos = renderer::mainCamera->CaluateTilePosition(pos);
if (pos.x >= 0.0f && pos.y >= 0.0f)
{
int idxX = pos.x / TilemapRenderer::TileSize.x;
int idxY = pos.y / TilemapRenderer::TileSize.y;
Tile* tile = object::Instantiate<Tile>(eLayerType::Tile);
TilemapRenderer* tmr = tile->AddComponent<TilemapRenderer>();
tmr->SetTexture(Resources::Find<graphics::Texture>(L"SpringFloor"));
tmr->SetIndex(TilemapRenderer::SelectedIndex);
tile->SetIndexPosition(idxX, idxY);
mTiles.push_back(tile);
}
else
{
//
}
}
C++
복사
마지막으로 배치된 타일 오브젝트를 파일입출력을 통해서 외장 디스크에 저장한 다음에 실제 게임을 로딩할때 불러와주면 된다. 자세한 파일의 입출력은 깃허브 소스코드를 참고해보자.