Nota
O acesso a esta página requer autorização. Pode tentar iniciar sessão ou alterar os diretórios.
O acesso a esta página requer autorização. Pode tentar alterar os diretórios.
Observação
Este tópico faz parte da série de tutoriais Criar um jogo simples da Plataforma Universal do Windows (UWP) com DirectX. O tópico nesse link define o contexto da série.
Agora que nosso jogo tem seus visuais 3D no lugar, é hora de se concentrar em adicionar alguns elementos 2D para que o jogo possa fornecer feedback sobre o estado do jogo para o jogador. Isso pode ser feito adicionando opções de menu simples e componentes de heads-up display sobre a saída do pipeline de gráficos 3D.
Observação
Se ainda não tiver transferido o código mais recente do jogo para este exemplo, aceda ao jogo de exemplo Direct3D . Este exemplo faz parte de uma grande coleção de exemplos de recursos UWP. Para obter instruções sobre como descarregar o exemplo, consulte Aplicações de exemplo para desenvolvimento Windows.
Objetivo
Usando o Direct2D, adicione vários gráficos e comportamentos da interface do usuário ao nosso jogo UWP DirectX, incluindo:
- Heads-up display, incluindo retângulos de limite do controlador move-look
- Menus de estado do jogo
A sobreposição da interface do usuário
Embora existam muitas maneiras de exibir texto e elementos da interface do usuário em um jogo DirectX, vamos nos concentrar no uso do Direct2D. Também usaremos DirectWrite para os elementos de texto.
Direct2D é um conjunto de APIs de desenho 2D usadas para desenhar primitivos e efeitos baseados em pixels. Ao começar com Direct2D, é melhor manter as coisas simples. Layouts complexos e comportamentos de interface precisam de tempo e planejamento. Se o seu jogo requer uma interface de usuário complexa, como as encontradas em jogos de simulação e estratégia, considere usar XAML.
Observação
Para saber mais sobre como desenvolver uma interface de usuário com XAML em um jogo UWP DirectX, veja Estendendo o jogo de exemplo.
O Direct2D não foi projetado especificamente para interfaces de usuário ou layouts como HTML e XAML. Ele não fornece componentes da interface do usuário, como listas, caixas ou botões. Ele também não fornece componentes de layout como divs, tabelas ou grades.
Para este jogo de exemplo, temos dois componentes principais da interface do usuário.
- Um display de informação para a pontuação e controlos no jogo.
- Uma sobreposição usada para exibir o texto do estado do jogo e opções como informações de pausa e opções de início de nível.
Usando o Direct2D para um heads-up display
A imagem a seguir mostra o heads-up display do jogo para a amostra. É simples e organizado, permitindo que o jogador se concentre em navegar no mundo 3D e atirar em alvos. Uma boa interface ou heads-up display nunca deve complicar a capacidade do jogador de processar e reagir aos eventos do jogo.
A sobreposição consiste nos seguintes primitivos básicos.
-
DirectWrite texto no canto superior direito que informa o jogador de
- Acertos bem-sucedidos
- Número de remates que o jogador fez
- Tempo restante no nível
- Número de nível atual
- Dois segmentos de linha que se cruzam usados para formar uma cruz
- Dois retângulos nos cantos inferiores para os limites do controlador move-look .
O estado de exibição heads-up no jogo da sobreposição é desenhado no método GameHud::Render da classe GameHud. Dentro desse método, a sobreposição Direct2D que representa nossa interface do usuário é atualizada para refletir as alterações no número de acertos, tempo restante e número de nível.
Se o jogo tiver sido inicializado, adicionamos TotalHits(), TotalShots()e TimeRemaining() a um buffer de swprintf_s e especificamos o formato de impressão. Podemos então desenhá-lo usando o método DrawText . Fazemos o mesmo para o indicador de nível atual, desenhando números vazios para mostrar níveis incompletos como ➀, e números preenchidos como ➊ para mostrar que o nível específico foi concluído.
O trecho de código seguinte analisa o processo do método GameHud::Render para
- Criar um bitmap usando **ID2D1RenderTarget::DrawBitmap**
- Seccionando áreas da interface do usuário em retângulos usando D2D1::RectF
- Usando DrawText para criar elementos de texto
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
...
}
}
}
Dividindo ainda mais o método, esta parte do método GameHud::Render desenha os retângulos de movimento e disparo com ID2D1RenderTarget::DrawRectanglee cruzetas usando duas chamadas para 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
);
}
No método GameHud::Render , armazenamos o tamanho lógico da janela do jogo na windowBounds variável. Isso usa o GetLogicalSize método da classe DeviceResources .
auto windowBounds = m_deviceResources->GetLogicalSize();
Obter o tamanho da janela do jogo é essencial para a programação da interface do usuário. O tamanho da janela é dado em uma medida chamada DIPs (pixel independentes do dispositivo), onde um DIP é definido como 1/96 de uma polegada. O Direct2D dimensiona as unidades de desenho para pixels reais quando o desenho ocorre, fazendo isso usando a configuração de pontos por polegada (DPI) do Windows. Da mesma forma, ao desenhar texto usando DirectWrite, você especifica DIPs em vez de pontos para o tamanho da fonte. Os DIPs são expressos como números de ponto flutuante.
Exibindo informações do estado do jogo
Além do ecrã de visualização frontal, o jogo de amostra tem uma camada sobreposta que representa seis estados do jogo. Todos os estados apresentam um grande retângulo preto primitivo com texto para o jogador ler. Os retângulos e as miras do controlador move-look não são desenhados porque não estão ativos nessas situações.
A sobreposição é criada usando a GameInfoOverlay class, o que nos permite alternar o texto exibido para alinhar com o estado do jogo.
A sobreposição é dividida em duas secções: Status e Action. A seção Status é dividida em retângulos de Título e Corpo. A secção Ação tem apenas um retângulo. Cada retângulo tem uma finalidade diferente.
-
titleRectanglecontém o texto do título. -
bodyRectanglecontém o corpo do texto. -
actionRectanglecontém o texto que informa o jogador para tomar uma ação específica.
O jogo tem seis estados que podem ser definidos. O estado do jogo é transmitido usando a parte Status da sobreposição. Os retângulos de estado são atualizados usando vários métodos que correspondem aos seguintes estados.
- Carregando
- Estatísticas de início inicial/pontuação alta
- Início do nível
- Jogo pausado
- Fim do jogo
- Jogo ganho
A parte Action da sobreposição é atualizada usando o método GameInfoOverlay::SetAction, permitindo que o texto da ação seja definido como um dos seguintes.
- Toque para voltar a jogar...
- "Nível de carregamento, por favor aguarde ..."
- "Toque para continuar..."
- Nenhum
Observação
Ambos os métodos serão discutidos mais detalhadamente na seção Representação do estado do jogo.
Dependendo do que está acontecendo no jogo, os campos de texto da seção Status e Ação são ajustados. Vejamos como inicializamos e desenhamos o overlay para esses seis estados.
Inicializando e desenhando a sobreposição
Os seis Estados Status têm algumas coisas em comum, tornando os recursos e métodos de que precisam muito semelhantes. - Todos eles usam um retângulo preto no centro da tela como fundo. - O texto exibido é o Título ou o Corpo. - O texto usa a fonte Segoe UI e é desenhado em cima do retângulo traseiro.
O jogo de exemplo tem quatro métodos que entram em jogo ao criar a sobreposição.
GameInfoOverlay::GameInfoOverlay
O construtor GameInfoOverlay::GameInfoOverlay inicializa a sobreposição, preservando a superfície de bitmap que utilizaremos para mostrar informações ao jogador. O construtor obtém uma fábrica do ID2D1Device objeto que lhe é passado, que ele utiliza para criar um ID2D1DeviceContext ao qual o próprio objeto de sobreposição pode desenhar. IDWriteFactory::CreateTextFormat
GameInfoOverlay::CreateDeviceDependentResources
GameInfoOverlay::CreateDeviceDependentResources é o nosso método para criar pincéis que serão usados para desenhar o nosso texto. Para fazer isso, obtemos um objeto ID2D1DeviceContext2 que permite a criação e o desenho de geometria, além de funcionalidades como renderização de malha de tinta e gradiente. Em seguida, criamos uma série de pincéis coloridos usando ID2D1SolidColorBrush para desenhar os seguintes elementos da interface do usuário.
- Pincel preto para fundos retangulares
- Pincel branco para texto de status
- Pincel laranja para texto de ação
DeviceResources::SetDpi
O método DeviceResources::SetDpi define os pontos por polegada da janela. Este método é chamado quando o DPI é alterado e precisa ser reajustado, o que acontece quando a janela do jogo é redimensionada. Depois de atualizar o DPI, esse método também chamaDeviceResources::CreateWindowSizeDependentResources para garantir que os recursos necessários sejam recriados sempre que a janela for redimensionada.
GameInfoOverlay::CriarRecursosDependentesDoTamanhoDaJanela
O método GameInfoOverlay::CreateWindowsSizeDependentResources é onde todo o nosso desenho é realizado. A seguir está um esboço das etapas do método.
Três retângulos são criados para separar o texto da UI para o Título, Corpoe Ação.
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 );Um Bitmap é criado chamado
m_levelBitmap, levando em conta o DPI atual usando CreateBitmap.m_levelBitmapé definido como nosso destino de renderização 2D usando ID2D1DeviceContext::SetTarget.O bitmap é limpo tornando-se cada pixel preto usando ID2D1RenderTarget::Clear.
ID2D1RenderTarget::BeginDraw é chamado para iniciar o desenho.
DrawText é chamado para desenhar o texto armazenado em
m_titleString,m_bodyStringem_actionStringno retângulo apropriado usando o ID2D1SolidColorBrush correspondente.ID2D1RenderTarget::EndDraw é chamado para encerrar todas as operações de desenho no
m_levelBitmap.Outro Bitmap é criado usando CreateBitmap nomeado
m_tooSmallBitmappara usar como fallback, mostrando apenas se a configuração de exibição for muito pequena para o jogo.Repita o processo para desenhar em
m_levelBitmapparam_tooSmallBitmap, desta vez apenas desenhando a cordaPausedno corpo.
Agora, tudo o que precisamos é de seis métodos para preencher o texto dos nossos seis estados de sobreposição!
Representando o estado do jogo
Cada um dos seis estados de sobreposição no jogo tem um método correspondente no objeto GameInfoOverlay. Esses métodos desenham uma variação da sobreposição para comunicar informações explícitas ao jogador sobre o jogo em si. Esta comunicação é representada com um Título e Corpo cadeia de caracteres. Como o exemplo já configurou os recursos e o layout para essas informações quando foi inicializado e com o método
A parte Status da sobreposição é definida com uma chamada para um dos seguintes métodos.
| Estado do jogo | Método de definição de status | Campos de status |
|---|---|---|
| Carregando | GameInfoOverlay::SetGameLoading |
Título Carregando Recursos Corpo Imprime incrementalmente "." para implicar atividade de carregamento. |
| Estatísticas de início inicial/pontuação alta | GameInfoOverlay::SetGameStats |
Título Maior Pontuação CorpoConcluído Níveis Concluídos # Total de Pontos # Total de Tiros # |
| Início do nível | GameInfoOverlay::SetLevelStart |
Título Nível # Corpo Descrição objetiva do nível. |
| Jogo pausado | GameInfoOverlay::SetPause |
Título Jogo Pausado Corpo Nenhum |
| Fim do jogo | GameInfoOverlay::SetGameOver |
Título Jogo sobre Níveis de Corporal Concluído # Total de Pontos # Total de Tiros # Níveis Concluídos # Pontuação Alta # |
| Jogo ganho | GameInfoOverlay::SetGameOver |
Título Você GANHOU! Níveis Concluídos # Total de Pontos # Total de Tiros # Níveis Concluídos # Pontuação Alta # |
Com o método GameInfoOverlay::CreateWindowSizeDependentResources , o exemplo declarou três áreas retangulares que correspondem a regiões específicas da sobreposição.
Com essas áreas em mente, vamos examinar um dos métodos específicos do estado, GameInfoOverlay::SetGameStats, e ver como a sobreposição é renderizada.
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);
}
}
Usando o contexto de dispositivo Direct2D que o objeto GameInfoOverlay inicializou, este método preenche com preto os retângulos de título e corpo, utilizando o pincel de plano de fundo. Desenha o texto "High Score" no retângulo de título e uma string contendo as informações atualizadas do estado do jogo no retângulo do corpo usando o pincel de texto branco.
O retângulo de ação é atualizado por uma chamada subsequente para GameInfoOverlay::SetAction a partir de um método no objeto GameMain, que fornece as informações necessárias sobre o estado do jogo para que GameInfoOverlay::SetAction determine a mensagem correta para o jogador, tal como "Toque para continuar".
A sobreposição para qualquer estado é escolhida no método GameMain::SetGameInfoOverlay desta forma:
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;
}
}
Agora o jogo tem uma maneira de comunicar informações de texto para o jogador com base no estado do jogo, e temos uma maneira de mudar o que é exibido para eles ao longo do jogo.
Próximos passos
No próximo tópico, Adicionando controles, examinamos como o jogador interage com o jogo de amostra e como a entrada altera o estado do jogo.