사용자 인터페이스 추가
참고 항목
이 항목은 DirectX를 사용하여 간단한 UWP(유니버설 Windows 플랫폼) 게임 만들기 자습서 시리즈의 일부입니다. 해당 링크의 항목은 시리즈의 컨텍스트를 설정합니다.
이제 게임에 3D 비주얼이 있으므로 게임이 플레이어에게 게임 상태에 대한 피드백을 제공할 수 있도록 일부 2D 요소를 추가하는 데 집중해야 합니다. 이는 3D 그래픽 파이프라인 출력 위에 간단한 메뉴 옵션과 헤드업 디스플레이 구성 요소를 추가하여 수행할 수 있습니다.
참고 항목
이 샘플의 최신 게임 코드를 다운로드하지 않은 경우 Direct3D 샘플 게임으로 이동합니다. 이 샘플은 UWP 기능 샘플의 큰 컬렉션의 일부입니다. 샘플을 다운로드하는 방법에 대한 지침은 Windows 개발을 위한 샘플 애플리케이션을 참조하세요.
목표
Direct2D를 사용하여 다음을 포함하여 UWP DirectX 게임에 다양한 사용자 인터페이스 그래픽 및 동작을 추가합니다.
- 이동-보기 컨트롤러 경계 직사각형을 포함한 헤드업 디스플레이
- 게임 상태 메뉴
사용자 인터페이스 오버레이
DirectX 게임에서 텍스트 및 사용자 인터페이스 요소를 표시하는 방법에는 여러 가지가 있지만 여기서는 Direct2D 사용에 중점을 둘 것입니다. 또한 텍스트 요소에 대해 DirectWrite를 사용할 것입니다.
Direct2D는 픽셀 기반 기본 그래픽 및 효과를 그리는 데 사용되는 2D 그리기 API입니다. Direct2D를 시작할 때 작업을 단순하게 유지하는 것이 가장 좋습니다. 복잡한 레이아웃 및 인터페이스 동작에는 시간과 계획이 필요합니다. 시뮬레이션 및 전략 게임에서 볼 수 있는 것과 같은 복잡한 사용자 인터페이스가 게임에 필요한 경우 대신 XAML을 사용하는 것이 좋습니다.
참고 항목
UWP DirectX 게임에서 XAML을 사용하여 사용자 인터페이스를 개발하는 방법에 대한 자세한 내용은 샘플 게임 확장을 참조하세요.
Direct2D는 HTML 및 XAML과 같은 사용자 인터페이스 또는 레이아웃을 위해 특별히 설계되지 않았습니다. 목록, 상자 또는 버튼와 같은 사용자 인터페이스 구성 요소를 제공하지 않습니다. 또한 div, 테이블 또는 그리드와 같은 레이아웃 구성 요소를 제공하지 않습니다.
이 샘플 게임에는 두 가지 주요 UI 구성 요소가 있습니다.
- 점수 및 게임 내 컨트롤을 위한 헤드업 디스플레이
- 게임 상태 텍스트 및 일시 중지 정보 및 수준 시작 옵션과 같은 옵션을 표시하는 데 사용되는 오버레이
헤드업 디스플레이에 Direct2D 사용
다음 이미지는 샘플에 대한 게임 내 헤드업 디스플레이를 보여 줍니다. 간단하고 깔끔하여 플레이어가 3D 세계를 탐색하고 타겟을 쏘는 데 집중할 수 있습니다. 뛰어난 인터페이스나 주의 표시는 플레이어가 게임에서 이벤트를 처리하고 대응하는 능력을 복잡하게 만들면 안 됩니다.
오버레이는 다음과 같은 기본 프리미티브로 구성됩니다.
- 플레이어에게 알려주는 오른쪽 상단 모서리의 DirectWrite 텍스트
- 성공적인 조회수
- 플레이어가 만든 샷의 수
- 수준에 남은 시간
- 현재 수준 번호
- 십자선을 형성하는 데 사용되는 두 개의 교차 선 세그먼트
- 이동-보기 컨트롤러 경계의 하단 모서리에 있는 두 개의 직사각형.
오버레이의 게임 내 헤드업 디스플레이 상태는 GameHud 클래스의 GameHud::Render 메서드에서 그려집니다. 이 메서드 내에서 UI를 나타내는 Direct2D 오버레이가 업데이트되어 히트 수, 남은 시간 및 수준 번호의 변경 내용을 반영합니다.
게임이 초기화된 경우 swprintf_s 버퍼에 TotalHits()
, TotalShots()
및 TimeRemaining()
을 추가하고 인쇄 형식을 지정합니다. 그런 다음 DrawText 메서드를 사용하여 그릴 수 있습니다. 현재 수준 표시기에 대해서도 동일한 작업을 수행합니다. 빈 숫자는 ➊과 같이 완료되지 않은 수준을 표시하고 채워진 숫자는 특정 수준이 완료되었음을 표시하는 것과 같습니다.
다음 코드 조각은 다음을 위한 GameHud::Render 메서드 프로세스를 안내합니다.
- **ID2D1RenderTarget::DrawBitmap **을 사용하여 비트맵 만들기
- D2D1::RectF를 사용하여 UI 영역을 직사각형으로 분할
- DrawText를 사용하여 텍스트 요소 만들기
void GameHud::Render(_In_ std::shared_ptr<Simple3DGame> const& game)
{
auto d2dContext = m_deviceResources->GetD2DDeviceContext();
auto windowBounds = m_deviceResources->GetLogicalSize();
if (m_showTitle)
{
d2dContext->DrawBitmap(
m_logoBitmap.get(),
D2D1::RectF(
GameUIConstants::Margin,
GameUIConstants::Margin,
m_logoSize.width + GameUIConstants::Margin,
m_logoSize.height + GameUIConstants::Margin
)
);
d2dContext->DrawTextLayout(
Point2F(m_logoSize.width + 2.0f * GameUIConstants::Margin, GameUIConstants::Margin),
m_titleHeaderLayout.get(),
m_textBrush.get()
);
d2dContext->DrawTextLayout(
Point2F(GameUIConstants::Margin, m_titleBodyVerticalOffset),
m_titleBodyLayout.get(),
m_textBrush.get()
);
}
// Draw text for number of hits, total shots, and time remaining
if (game != nullptr)
{
// This section is only used after the game state has been initialized.
static const int bufferLength = 256;
static wchar_t wsbuffer[bufferLength];
int length = swprintf_s(
wsbuffer,
bufferLength,
L"Hits:\t%10d\nShots:\t%10d\nTime:\t%8.1f",
game->TotalHits(),
game->TotalShots(),
game->TimeRemaining()
);
// Draw the upper right portion of the HUD displaying total hits, shots, and time remaining
d2dContext->DrawText(
wsbuffer,
length,
m_textFormatBody.get(),
D2D1::RectF(
windowBounds.Width - GameUIConstants::HudRightOffset,
GameUIConstants::HudTopOffset,
windowBounds.Width,
GameUIConstants::HudTopOffset + (GameUIConstants::HudBodyPointSize + GameUIConstants::Margin) * 3
),
m_textBrush.get()
);
// Using the unicode characters starting at 0x2780 ( ➀ ) for the consecutive levels of the game.
// For completed levels start with 0x278A ( ➊ ) (This is 0x2780 + 10).
uint32_t levelCharacter[6];
for (uint32_t i = 0; i < 6; i++)
{
levelCharacter[i] = 0x2780 + i + ((static_cast<uint32_t>(game->LevelCompleted()) == i) ? 10 : 0);
}
length = swprintf_s(
wsbuffer,
bufferLength,
L"%lc %lc %lc %lc %lc %lc",
levelCharacter[0],
levelCharacter[1],
levelCharacter[2],
levelCharacter[3],
levelCharacter[4],
levelCharacter[5]
);
// Create a new rectangle and draw the current level info text inside
d2dContext->DrawText(
wsbuffer,
length,
m_textFormatBodySymbol.get(),
D2D1::RectF(
windowBounds.Width - GameUIConstants::HudRightOffset,
GameUIConstants::HudTopOffset + (GameUIConstants::HudBodyPointSize + GameUIConstants::Margin) * 3 + GameUIConstants::Margin,
windowBounds.Width,
GameUIConstants::HudTopOffset + (GameUIConstants::HudBodyPointSize + GameUIConstants::Margin) * 4
),
m_textBrush.get()
);
if (game->IsActivePlay())
{
// Draw the move and fire rectangles
...
// Draw the crosshairs
...
}
}
}
메서드를 더 세분화하면 GameHud::Render 메서드의 이 부분이 ID2D1RenderTarget::DrawRectangle이 있는 직사각형과 ID2D1RenderTarget::DrawLine에 대한 두 번의 호출을 사용하는 십자형을 사용하여 이동 및 실행 사각형을 그립니다.
// Check if game is playing
if (game->IsActivePlay())
{
// Draw a rectangle for the touch input for the move control.
d2dContext->DrawRectangle(
D2D1::RectF(
0.0f,
windowBounds.Height - GameUIConstants::TouchRectangleSize,
GameUIConstants::TouchRectangleSize,
windowBounds.Height
),
m_textBrush.get()
);
// Draw a rectangle for the touch input for the fire control.
d2dContext->DrawRectangle(
D2D1::RectF(
windowBounds.Width - GameUIConstants::TouchRectangleSize,
windowBounds.Height - GameUIConstants::TouchRectangleSize,
windowBounds.Width,
windowBounds.Height
),
m_textBrush.get()
);
// Draw the cross hairs
d2dContext->DrawLine(
D2D1::Point2F(windowBounds.Width / 2.0f - GameUIConstants::CrossHairHalfSize,
windowBounds.Height / 2.0f),
D2D1::Point2F(windowBounds.Width / 2.0f + GameUIConstants::CrossHairHalfSize,
windowBounds.Height / 2.0f),
m_textBrush.get(),
3.0f
);
d2dContext->DrawLine(
D2D1::Point2F(windowBounds.Width / 2.0f, windowBounds.Height / 2.0f -
GameUIConstants::CrossHairHalfSize),
D2D1::Point2F(windowBounds.Width / 2.0f, windowBounds.Height / 2.0f +
GameUIConstants::CrossHairHalfSize),
m_textBrush.get(),
3.0f
);
}
GameHud::Render 메서드에서 게임 창의 논리적 크기를 windowBounds
변수에 저장합니다. 이는 DeviceResources 클래스의 GetLogicalSize
메서드를 사용합니다.
auto windowBounds = m_deviceResources->GetLogicalSize();
게임 창의 크기를 구하는 것은 UI 프로그래밍에 필수적입니다. 창의 크기는 DIP(디바이스 독립 픽셀)라는 측정 단위로 표시되며, 여기서 DIP는 1/96인치로 정의됩니다. Direct2D는 Windows DPI(인치당 도트 수) 설정을 사용하여 그리기가 발생할 때 그리기 단위를 실제 픽셀로 조정합니다. 마찬가지로 DirectWrite를 사용하여 텍스트를 그리는 경우에도 글꼴 크기에 대한 포인트가 아닌 DIP를 지정합니다. DIP는 부동 소수점 숫자로 표현됩니다.
게임 상태 정보 표시
헤드업 디스플레이 외에도 샘플 게임에는 6가지 게임 상태를 나타내는 오버레이가 있습니다. 모든 상태에는 플레이어가 읽을 수 있는 텍스트가 있는 큰 검은색 사각형 기본 요소가 있습니다. 이동-보기 컨트롤러 직사각형 및 십자형은 이러한 상태에서 활성화되지 않기 때문에 그려지지 않습니다.
오버레이는 GameInfoOverlay 클래스를 사용하여 만들어지므로 게임 상태에 맞게 표시할 텍스트를 전환할 수 있습니다.
오버레이는 상태와 작업의 두 섹션으로 나뉩니다. 이 Status 섹션은 Title 및 Body 직사각형 모양의 프레임으로 세분화됩니다. 작업 섹션에는 직사각형이 하나만 있습니다. 각 직사각형은 다른 목적을 가지고 있습니다.
titleRectangle
은 제목 텍스트를 포함합니다.bodyRectangle
은 본문을 포함합니다.actionRectangle
에는 플레이어에게 특정 작업을 취하도록 알리는 텍스트가 포함되어 있습니다.
게임에는 설정할 수 있는 6가지 상태가 있습니다. 오버레이의 상태 부분을 사용하여 전달된 게임의 상태입니다. 상태 직사각형은 다음 상태에 해당하는 여러 메서드를 사용하여 업데이트됩니다.
- 로드
- 초기 시작/고득점 통계
- 수준 시작
- 게임이 일시 중지됨
- 게임 종료
- 게임 승리
오버레이의 작업 부분은 GameInfoOverlay::SetAction 메서드를 사용하여 업데이트되어 작업 텍스트를 다음 중 하나로 설정할 수 있습니다.
- "다시 재생하려면 탭합니다..."
- "수준 로딩 중, 잠시만 기다려 주세요..."
- "계속하려면 탭합니다..."
- None
참고 항목
이 두 가지 방법 모두 게임 상태 표현 섹션에서 자세히 설명합니다.
게임의 진행 상황에 따라 상태 및 작업 섹션 텍스트 필드가 조정됩니다. 이 6가지 상태에 대한 오버레이를 초기화하고 그리는 방법을 살펴보겠습니다.
오버레이 초기화 및 그리기
6개의 상태에는 몇 가지 공통점이 있어 필요한 리소스와 메서드가 매우 유사합니다. - 모두 화면 중앙에 있는 검은색 직사각형을 배경으로 사용합니다. - 표시되는 텍스트는 제목 또는 본문 텍스트입니다. - 텍스트는 Segoe UI 글꼴을 사용하며 후면 사각형 위에 그려집니다.
샘플 게임에는 오버레이를 만들 때 작동하는 네 가지 방법이 있습니다.
GameInfoOverlay::GameInfoOverlay
GameInfoOverlay::GameInfoOverlay 생성자는 오버레이를 초기화하여 플레이어에게 정보를 표시하는 데 사용할 비트맵 표면을 유지합니다. 생성자는 전달된 ID2D1Device 개체에서 팩터리를 가져옵니다. 이는 오버레이 개체 자체가 그릴 수 있는 ID2D1DeviceContext를 만드는 데 사용합니다. IDWriteFactory::CreateTextFormat
GameInfoOverlay::CreateDeviceDependentResources
GameInfoOverlay::CreateDeviceDependentResources는 텍스트를 그리는 데 사용할 브러시를 만드는 방법입니다. 이를 위해 기하 도형을 만들고 그릴 수 있는 ID2D1DeviceContext2 개체와 잉크 및 그라데이션 메시 렌더링과 같은 기능을 가져옵니다. 그런 다음 ID2D1SolidColorBrush를 사용하여 일련의 컬러 브러시를 만들어 다음의 UI 요소를 그립니다.
- 직사각형 배경용 검정 브러시
- 상태 텍스트용 흰색 브러시
- 작업 텍스트용 주황색 브러시
DeviceResources::SetDpi
DeviceResources::SetDpi 메서드는 창의 인치당 도트 수를 설정합니다. 이 메서드는 DPI가 변경될 때 호출되며 게임 창의 크기가 조정될 때 발생하는 재조정이 필요합니다. DPI를 업데이트한 후 이 메서드는 DeviceResources::CreateWindowSizeDependentResources도 호출하여 창 크기를 조정할 때마다 필요한 리소스가 다시 만들어지도록 합니다.
GameInfoOverlay::CreateWindowsSizeDependentResources
GameInfoOverlay::CreateWindowsSizeDependentResources 메서드는 모든 그리기가 이루어지는 곳입니다. 다음은 방법 단계의 개요입니다.
제목, 본문 및 작업 텍스트의 UI 텍스트를 구분하기 위해 세 개의 직사각형이 만들어집니다.
m_titleRectangle = D2D1::RectF( GameInfoOverlayConstant::SideMargin, GameInfoOverlayConstant::TopMargin, overlaySize.width - GameInfoOverlayConstant::SideMargin, GameInfoOverlayConstant::TopMargin + GameInfoOverlayConstant::TitleHeight ); m_actionRectangle = D2D1::RectF( GameInfoOverlayConstant::SideMargin, overlaySize.height - (GameInfoOverlayConstant::ActionHeight + GameInfoOverlayConstant::BottomMargin), overlaySize.width - GameInfoOverlayConstant::SideMargin, overlaySize.height - GameInfoOverlayConstant::BottomMargin ); m_bodyRectangle = D2D1::RectF( GameInfoOverlayConstant::SideMargin, m_titleRectangle.bottom + GameInfoOverlayConstant::Separator, overlaySize.width - GameInfoOverlayConstant::SideMargin, m_actionRectangle.top - GameInfoOverlayConstant::Separator );
CreateBitmap을 사용하여 현재 DPI를 고려하여
m_levelBitmap
이라는 비트맵이 만들어집니다.m_levelBitmap
은 ID2D1DeviceContext::SetTarget을 사용하여 2D 렌더링 대상으로 설정됩니다.비트맵은 ID2D1RenderTarget::Clear를 사용하여 검은색으로 만든 모든 픽셀로 지워집니다.
ID2D1RenderTarget::BeginDraw는 그리기를 시작하기 위해 호출됩니다.
DrawText를 호출해
m_titleString
,m_bodyString
, 및m_actionString
해당하는 ID2D1SolidColorBrush를 사용한 적절한 직사각형 프레임에 저장된 텍스트를 그립니다.ID2D1RenderTarget::EndDraw는
m_levelBitmap
에서 모든 그리기 작업을 중지하기 위해 호출됩니다.m_tooSmallBitmap
이라는 이름의 CreateBitmap을 사용하여 다른 비트맵이 만들어져 디스플레이 구성이 게임에 비해 너무 작은 경우에만 표시됩니다.m_tooSmallBitmap
에 대해m_levelBitmap
에 그리는 과정을 반복합니다. 이번에는 본문에Paused
문자열만 그립니다.
이제 6가지 오버레이 상태의 텍스트를 채우는 6가지 방법만 있으면 됩니다.
게임 상태 표현
게임의 6가지 오버레이 상태 각각에는 GameInfoOverlay 개체에 해당 메서드가 있습니다. 이러한 메서드는 오버레이의 변형을 그려 플레이어에게 게임 자체에 대한 명시적 정보를 전달합니다. 이 통신은 제목 및 본문 문자열로 표시됩니다. 샘플은 초기화될 때 GameInfoOverlay::CreateDeviceDependentResources 메서드를 사용하여 이 정보에 대한 리소스와 레이아웃을 이미 구성했기 때문에 오버레이 상태별 문자열만 제공하면 됩니다.
오버레이의 상태 부분은 다음 메서드 중 하나를 호출하여 설정됩니다.
게임 상태 | 상태 설정 방법 | 상태 필드 |
---|---|---|
로드 | GameInfoOverlay::SetGameLoading | 제목 리소스 로드 중 본문 로딩 작업을 암시하는 "."를 증분식으로 인쇄합니다. |
초기 시작/고득점 통계 | GameInfoOverlay::SetGameStats | 타이틀 높은 점수 본문 수준 완료 # 총 포인트 # 총 샷 # |
수준 시작 | GameInfoOverlay::SetLevelStart | 제목 수준 # 본문 수준 목표 설명입니다. |
게임이 일시 중지됨 | GameInfoOverlay::SetPause | 제목 게임 일시 중지됨 본문 없음 |
게임 종료 | GameInfoOverlay::SetGameOver | 본문 수준을 통해 타이틀 게임 완료 # 총 포인트 # 총 샷 # 레벨 완료 # 높은 점수 # |
게임 승리 | GameInfoOverlay::SetGameOver | 당신이 이긴 제목 ! 신체 수준 완료 # 총 포인트 # 총 샷 # 레벨 완료 # 높은 점수 # |
GameInfoOverlay::CreateWindowSizeDependentResources 메서드를 사용하여 샘플은 오버레이의 특정 영역에 해당하는 3개의 직사각형 영역을 선언했습니다.
이러한 영역을 염두에 두고 상태별 방법인 GameInfoOverlay::SetGameStats 중 하나를 살펴보고 오버레이가 그려지는 방법을 살펴보겠습니다.
void GameInfoOverlay::SetGameStats(int maxLevel, int hitCount, int shotCount)
{
int length;
auto d2dContext = m_deviceResources->GetD2DDeviceContext();
d2dContext->SetTarget(m_levelBitmap.get());
d2dContext->BeginDraw();
d2dContext->SetTransform(D2D1::Matrix3x2F::Identity());
d2dContext->FillRectangle(&m_titleRectangle, m_backgroundBrush.get());
d2dContext->FillRectangle(&m_bodyRectangle, m_backgroundBrush.get());
m_titleString = L"High Score";
d2dContext->DrawText(
m_titleString.c_str(),
m_titleString.size(),
m_textFormatTitle.get(),
m_titleRectangle,
m_textBrush.get()
);
length = swprintf_s(
wsbuffer,
bufferLength,
L"Levels Completed %d\nTotal Points %d\nTotal Shots %d",
maxLevel,
hitCount,
shotCount
);
m_bodyString = std::wstring(wsbuffer, length);
d2dContext->DrawText(
m_bodyString.c_str(),
m_bodyString.size(),
m_textFormatBody.get(),
m_bodyRectangle,
m_textBrush.get()
);
// We ignore D2DERR_RECREATE_TARGET here. This error indicates that the device
// is lost. It will be handled during the next call to Present.
HRESULT hr = d2dContext->EndDraw();
if (hr != D2DERR_RECREATE_TARGET)
{
// The D2DERR_RECREATE_TARGET indicates there has been a problem with the underlying
// D3D device. All subsequent rendering will be ignored until the device is recreated.
// This error will be propagated and the appropriate D3D error will be returned from the
// swapchain->Present(...) call. At that point, the sample will recreate the device
// and all associated resources. As a result, the D2DERR_RECREATE_TARGET doesn't
// need to be handled here.
winrt::check_hresult(hr);
}
}
이 메서드는 GameInfoOverlay 개체가 초기화한 Direct2D 디바이스 컨텍스트를 통해 배경 브러시를 사용하여 제목 및 본문 사각형을 검은색으로 채웁니다. '높은 점수' 문자열의 텍스트를 제목 사각형에 그리고 흰색 텍스트 브러시를 사용하여 게임 상태 정보를 본문 사각형에 업데이트하는 문자열을 그립니다.
작업 사각형은 다음에 GameMain 개체의 메서드에서 GameInfoOverlay::SetAction을 호출하여 업데이트하며, 이 사각형은 GameInfoOverlay::SetAction에서 플레이어에게 적합한 메시지(예: "계속하려면 탭하세요.")를 결정하는 데 필요한 게임 상태 정보를 제공합니다.
주어진 상태에 대한 오버레이는 다음과 같이 GameMain::SetGameInfoOverlay 메서드에서 선택됩니다.
void GameMain::SetGameInfoOverlay(GameInfoOverlayState state)
{
m_gameInfoOverlayState = state;
switch (state)
{
case GameInfoOverlayState::Loading:
m_uiControl->SetGameLoading(m_loadingCount);
break;
case GameInfoOverlayState::GameStats:
m_uiControl->SetGameStats(
m_game->HighScore().levelCompleted + 1,
m_game->HighScore().totalHits,
m_game->HighScore().totalShots
);
break;
case GameInfoOverlayState::LevelStart:
m_uiControl->SetLevelStart(
m_game->LevelCompleted() + 1,
m_game->CurrentLevel()->Objective(),
m_game->CurrentLevel()->TimeLimit(),
m_game->BonusTime()
);
break;
case GameInfoOverlayState::GameOverCompleted:
m_uiControl->SetGameOver(
true,
m_game->LevelCompleted() + 1,
m_game->TotalHits(),
m_game->TotalShots(),
m_game->HighScore().totalHits
);
break;
case GameInfoOverlayState::GameOverExpired:
m_uiControl->SetGameOver(
false,
m_game->LevelCompleted(),
m_game->TotalHits(),
m_game->TotalShots(),
m_game->HighScore().totalHits
);
break;
case GameInfoOverlayState::Pause:
m_uiControl->SetPause(
m_game->LevelCompleted() + 1,
m_game->TotalHits(),
m_game->TotalShots(),
m_game->TimeRemaining()
);
break;
}
}
이제 게임에는 게임 상태에 따라 플레이어에게 텍스트 정보를 전달할 수 있는 방법이 있으며 게임 전체에서 플레이어에게 표시되는 내용을 전환할 수 있습니다.
다음 단계
다음 항목인 컨트롤 추가에서는 플레이어가 샘플 게임과 상호 작용하는 방법 및 입력을 통해 게임 상태를 변경하는 방법에 대해 살펴봅니다.