Download PDF
Por Bruno Sonnino
O desenvolvimento de jogos é um tópico sempre quente: todos gostam de jogar jogos, e eles estão entre os mais vendidos em qualquer lista. Mas, quando você fala em desenvolver um bom jogo, performance sempre é um requisito. Ninguém gosta de jogar jogos com paradas ou falhas, mesmo nos dispositivos mais baratos.
Você pode usar muitas linguagens e frameworks para desenvolver um jogo, mas quando você quer performance em um jogo Windows*, nada se compara a Microsoft DirectX* com C++. Com estas tecnologias, você está perto do hardware, podendo usar todos os seus recursos e obter uma excelente performance.
Decidi desenvolver um jogo destes, mesmo sendo primordialmente um desenvolvedor C#. Eu desenvolvi muito em C++ há algum tempo, mas a linguagem é bem diferente do que eu estava acostumado. Além disso, DirectX é um assunto novo para mim, assim este artigo é sobre o desenvolvimento de jogos do ponto de vista de um novato. Desenvolvedores mais experientes deverão desculpar meus erros.
Neste artigo, vou mostrar como desenvolver um jogo chute de pênaltis no futebol. O jogo chuta a bola e o usuário move o goleiro para pegá-la. Nós não iremos iniciar do zero. Nós usaremos o Microsoft Visual Studio* 3D Starter Kit, um início lógico para aqueles que querem desenvolver jogos para o Windows 8.1.
O Microsoft Visual Studio* 3D Starter Kit
Após baixar o Starter Kit, você pode extraí-lo para uma pasta e abrir o arquivo StarterKit.sln. Esta solução tem um projeto para C++Windows 8.1pronto para ser executado. Se você executá-lo, verá algo como a Figura 1.
![]()
Figura 1.Estado inicial do Microsoft Visual Studio* 3D Starter Kit
Este programa demonstra diversos conceitos úteis:
- Cinco objetos são animados: quatro formas girando em volta da chaleira e a chaleira “dançando”
- Cada elemento tem um material diferente: alguns têm cores sólidas e o cubo tem um material que usa um bitmap.
- A luz vem do topo à esquerda da cena.
- O canto inferior direito contém um contador de frames por segundo (FPS).
- Um indicador de placar está posicionado no topo.
- Clicando em um objeto, ele é iluminado e o placar é incrementado.
- Clicando com o botão direito ou arrastando o dedo a partir de baixo abre uma barra de aplicativo com dois botões para mudar a cor da chaleira.
Você pode usar estes recursos para criar qualquer jogo, mas antes você irá ver os arquivos incluídos no kit.
Vamos iniciar com App.xaml e os respectivos arquivos cpp/h. Quando você executa a aplicação em App.xaml ela executa DirectXPage. Em DirectXPage.xaml, você tem um SwapChainPanel e a barra de aplicativo. O SwapChainPanel é uma superfície que hospeda os gráficos DirectX numa página XAML. Ali, você pode adicionar objetos XAML que serão apresentados numa cena Microsoft Direct3D*—isto é conveniente para adicionar botões, textos e outros objetos XAML em um jogo DirectX sem a necessidade de criar os seus controles do zero. O Starter Kit também adiciona um StackPanel, que será usado como placar.
DirectXPage.xaml.cpp
tem a inicialização das variáveis, a ligação aos manipuladores de eventos para redimensionamento e mudança de orientação, os manipuladores para os eventos Click dos botões da barra do aplicativo e o loop de renderização. Todos os objetos XAML são manipulados da mesma maneira que em qualquer outro programa Windows 8. O arquivo também processa o evento Tapped
, verificando se um toque (ou clique do mouse) toca um objeto. Se isso acontecer, o manipulador incrementa o placar para aquele objeto.
Você deve dizer ao programa que o SwapChainPanel
deve renderizar o conteúdo DirectX. Para fazer isso, de acordo com a documentação, você deve “converter a instância de SwapChainPanel
para um IInspectable
ou IUnknown
, então chamar QueryInterface
para obter uma referência para a interface ISwapChainPanelNative
e permite a ponte de interoperabilidade). Então, chame SwapChainPanel
and enables the interop bridge). Then, call ISwapChainPanelNative::SetSwapChain
naquela referência para associar a cadeia de mudança que você implementou com a instância de SwapChainPanel
.” Isto é feito no método CreateWindowSizeDependentResources
em DeviceResources.cpp
.
O loop principal do jogo está em StarterKitMain.cpp
aonde a página e o contador de FPS são renderizados.
Game.cpp
tem o loop do jogo e o teste de colisão. Ele calcula a animação no método Update
e desenha todos os objetos no método Render
. O contador de FPS é renderizado em SampleFpsTextRenderer.cpp
.
Os objetos do jogo estão na pasta Assets.Teapot.fbx
tem a chaleira e GameLevel.fbx
tem as quatro formas que se movem em volta da chaleira dançante.
Com este conhecimento básico do Starter Kit, você pode iniciar a criação de seu jogo.
Adicionando Recursos ao Jogo
Você está desenvolvendo um jogo de futebol, assim o primeiro recurso que deve adicionar é uma bola de futebol. Em Gamelevel.fbx
remova as quatro formas deste arquivo, selecionando uma a uma e teclando Delete. No Solution Explorer, exclua também CubeUVImage.png
porque ele não será necessário; este arquivo é a textura usada para cobrir o cubo, que você acabou de excluir.
O próximo passo é adicionar uma esfera ao modelo. Abra a caixa de ferramentas (se você não puder vê-la, clique em View > Toolbox) e dê um clique duplo na esfera, para adicioná-la ao modelo. Se ela parecer muito pequena, você pode aumentar o zoom clicando no segundo botão na barra de ferramentas do topo do editor, teclando Z para fazer um zoom com o mouse (arrastando para o centro aumenta o zoom) ou usando as teclas de seta para cima ou para baixo. Você também pode teclar Ctrl e usar a roda do mouse para fazer o zoom. Você deve ter algo semelhante à Figura 2.
![]()
Figura 2. Editor de Modelos com uma forma esférica
Esta esfera tem apenas uma cor branca com alguma luz sobre ela. Ela precisa de uma textura de bola de futebol. Minha primeira tentativa foi usar uma grade hexagonal, como aquela da Figura 3.
![]()
Figura 3.Grade Hexagonal para a textura da bola: primeira tentativa
Para aplicar a textura na esfera, selecione-a e, na janela Properties, atribua o arquivo .png
à propriedade Texture1
. Embora isto parecesse uma boa ideia, o resultado não foi tão bom, como você pode ver na Figura 4.
![]()
Figura 4.Esfera com textura aplicada
Os hexágonos estão distorcidos por causa das projeções dos pontos da textura na esfera. Você precisa de uma textura distorcida, como aquela da Figura 5.
![]()
Figura 5. Textura de bola de futebol adaptada à esfera
Quando você aplica esta textura, a esfera começa a parecer com uma bola de futebol. Você deve apenas ajustar algumas propriedades para que ela pareça mais real. Para fazer isto, selecione a bola e edite o efeito Phong na janela Properties. O modelo de iluminação Phong inclui uma luz direcional e luz ambiente e simula propriedades refletivas no objeto. Este é um shader incluído no Visual Studio, que você pode arrastar da barra de ferramentas. Se você quiser saber mais sobre shaders e como desenhá-los usando o editor do Visual Studio, clique no link correspondente na seção “Para mais informações”. Configure as propriedades Red
, Green
, e Blue
abaixo de MaterialSpecular para 0.2 e MaterialSpecularPower para 16. Agora você tem uma bola de futebol com aparência melhor (Figura 6).
![]()
Figura 6. Bola de futebol acabada
Se não quiser criar seus modelos no Visual Studio, você pode usar um modelo pronto da Web. O Visual Studio aceita qualquer modelo nos formatos FBX, DAE, e OBJ: você deve apenas adicioná-los a seus recursos na solução. Como um exemplo, você pode usar um arquivo .obj
como aquele na Figura 7 (este é um modelo gratuito baixado de http://www.turbosquid.com).
![]()
Figura 7. Modelo de bola tridimensional .obj
Animando o Modelo
Com o modelo no lugar, é hora de animá-lo. Antes disso, entretanto, eu quero remover a chaleira, pois ela não é necessária. Na pasta Assets, remova teapot.fbx. Em seguida, exclua o código usado para carregar e animá-la. Em Game.cpp
, a carga dos modelos é feita de forma assíncrona em CreateDeviceDependentResources
:
// Load the scene objects.
auto loadMeshTask = Mesh::LoadFromFileAsync(
m_graphics,
L"gamelevel.cmo",
L"",
L"",
m_meshModels)
.then([this]()
{
// Load the teapot from a separate file and add it to the vector of meshes.
return Mesh::LoadFromFileAsync(
Você deve mudar o modelo e remover a continuação da tarefa, de maneira que só a bola écarregada:
void Game::CreateDeviceDependentResources()
{
m_graphics.Initialize(m_deviceResources->GetD3DDevice(), m_deviceResources->GetD3DDeviceContext(), m_deviceResources->GetDeviceFeatureLevel());
// Set DirectX to not cull any triangles so the entire mesh will always be shown.
CD3D11_RASTERIZER_DESC d3dRas(D3D11_DEFAULT);
d3dRas.CullMode = D3D11_CULL_NONE;
d3dRas.MultisampleEnable = true;
d3dRas.AntialiasedLineEnable = true;
ComPtr<ID3D11RasterizerState> p3d3RasState;
m_deviceResources->GetD3DDevice()->CreateRasterizerState(&d3dRas, &p3d3RasState);
m_deviceResources->GetD3DDeviceContext()->RSSetState(p3d3RasState.Get());
// Load the scene objects.
auto loadMeshTask = Mesh::LoadFromFileAsync(
m_graphics,
L"gamelevel.cmo",
L"",
L"",
m_meshModels);
(loadMeshTask).then([this]()
{
// Scene is ready to be rendered.
m_loadingComplete = true;
});
}
O método ReleaseDeviceDependentResources precisa apenas limpar os modelos:
void Game::ReleaseDeviceDependentResources()
{
for (Mesh* m : m_meshModels)
{
delete m;
}
m_meshModels.clear();
m_loadingComplete = false;
}
O passo seguinte é mudar o método Update, de maneira que apenas a bola é girada:
void Game::Update(DX::StepTimer const& timer)
{
// Rotate scene.
m_rotation = static_cast<float>(timer.GetTotalSeconds()) * 0.5f;
}
Você manipula a velocidade de rotação usando o multiplicador (0.5f). Se quiser que a bola gire mais rápido, basta usar um multiplicador maior. Isto significa que a bola irá girar na velocidade de 0.5/(2 * pi) radianos por segundo. O método Render
renderiza a bola na rotação desejada:
void Game::Render()
{
// Loading is asynchronous. Only draw geometry after it's loaded.
if (!m_loadingComplete)
{
return;
}
auto context = m_deviceResources->GetD3DDeviceContext();
// Set render targets to the screen.
auto rtv = m_deviceResources->GetBackBufferRenderTargetView();
auto dsv = m_deviceResources->GetDepthStencilView();
ID3D11RenderTargetView *const targets[1] = { rtv };
context->OMSetRenderTargets(1, targets, dsv);
// Draw our scene models.
XMMATRIX rotation = XMMatrixRotationY(m_rotation);
for (UINT i = 0; i < m_meshModels.size(); i++)
{
XMMATRIX modelTransform = rotation;
String^ meshName = ref new String(m_meshModels[i]->Name());
m_graphics.UpdateMiscConstants(m_miscConstants);
m_meshModels[i]->Render(m_graphics, modelTransform);
}
}
ToggleHitEffect
não faz nada aqui, a bola não muda o brilho quando for tocada:
void Game::ToggleHitEffect(String^ object)
{
}
Apesar de não querer que a bola mude o brilho, você ainda pode querer saber quando ela foi tocada. Para isso, use este método OnHitObject
modificado:
String^ Game::OnHitObject(int x, int y)
{
String^ result = nullptr;
XMFLOAT3 point;
XMFLOAT3 dir;
m_graphics.GetCamera().GetWorldLine(x, y, &point, &dir);
XMFLOAT4X4 world;
XMMATRIX worldMat = XMMatrixRotationY(m_rotation);
XMStoreFloat4x4(&world, worldMat);
float closestT = FLT_MAX;
for (Mesh* m : m_meshModels)
{
XMFLOAT4X4 meshTransform = world;
auto name = ref new String(m->Name());
float t = 0;
bool hit = HitTestingHelpers::LineHitTest(*m, &point, &dir, &meshTransform, &t);
if (hit && t < closestT)
{
result = name;
}
}
return result;
}
Execute o projeto e verifique que a bola está girando no eixo y. Agora vamos fazer a bola se movimentar.
Movimentando a Bola
Para movimentar a bola, você deve aplicar uma translação nela, por exemplo, movendo-a para cima e para baixo. A primeira coisa a fazer é declarar a variável que guarda a posição atual em Game.h
:
class Game
{
public:
// snip
private:
// snip
float m_translation;
Então no método Update
, calcule a posição atual:
void Game::Update(DX::StepTimer const& timer)
{
// Rotate scene.
m_rotation = static_cast<float>(timer.GetTotalSeconds()) * 0.5f;
const float maxHeight = 7.0f;
auto totalTime = (float) fmod(timer.GetTotalSeconds(), 2.0f);
m_translation = totalTime > 1.0f ?
maxHeight - (maxHeight * (totalTime - 1.0f)) : maxHeight *totalTime;
}
Desta maneira, a bola vai para cima e para baixo a cada 2 segundos. No primeiro segundo, ela se move para cima e, no seguinte, para baixo. O método Render
calcula a matriz resultante e renderiza a bola na nova posição:
void Game::Render()
{
// snip
// Draw our scene models.
XMMATRIX rotation = XMMatrixRotationY(m_rotation);
rotation *= XMMatrixTranslation(0, m_translation, 0);
Se você executar o projeto agora, verá que a bola se move para cima e para baixo a uma velocidade constante. Você deve agora adicionar um pouco de física à bola.
Adicionando Física à Bola
Para adicionar um pouco de física à bola, você deve simular uma força agindo nela, representando a gravidade. Das suas aulas de física (você se lembra delas, não?), você sabe que um movimento acelerado segue as seguintes equações:
s = s0 + v0t + 1/2at2
v = v0 + at
aonde sé a posição no instante t, s0é a posição inicial, v0é a velocidade inicial e aé a aceleração. Para o movimento vertical, aé a aceleração causada pela gravidade (−10 m/s2) e s0é 0 (o movimento da bola se inicia no piso). Assim, as equações tornam-se:
s = v0t -5t2
v = v0 -10t
Você quer alcançar a altura máxima em 1. Na altura máxima, a velocidade é 0. Assim, a segunda equação permite encontrar a velocidade inicial:
0 = v0– 10 * 1 => v0 = 10 m/s
E isto dá a equação de translação da bola:
s = 10t – 5t2
Você deve mudar o método Update
para usar esta equação:
void Game::Update(DX::StepTimer const& timer)
{
// Rotate scene.
m_rotation = static_cast<float>(timer.GetTotalSeconds()) * 0.5f;
auto totalTime = (float) fmod(timer.GetTotalSeconds(), 2.0f);
m_translation = 10*totalTime - 5 *totalTime*totalTime;
}
Agora que a bola se move de uma maneira mais real, é hora de adicionar o campo de futebol.
Adicionando o Campo de Futebol
Para adicionar o campo de futebol, você deve criar uma nova cena. Na pasta Assets, clique com o botão direito para adicionar uma nova cena tridimensional e chame-a de field.fbx
. Da barra de ferramentas, adicione um plano e selecione-o, mudando sua escala X
para 107 e Z
para 60. Configure sua propriedade Texture1
para uma imagem de um campo de futebol. Você pode usar a ferramenta zoom (ou teclar Z) para diminuir o zoom.
Em seguida, você deve carregar o campo em CreateDeviceDependentResources
em Game.cpp
:
void Game::CreateDeviceDependentResources()
{
// snip
// Load the scene objects.
auto loadMeshTask = Mesh::LoadFromFileAsync(
m_graphics,
L"gamelevel.cmo",
L"",
L"",
m_meshModels)
.then([this]()
{
return Mesh::LoadFromFileAsync(
m_graphics,
L"field.cmo",
L"",
L"",
m_meshModels,
false // Do not clear the vector of meshes
);
});
(loadMeshTask).then([this]()
{
// Scene is ready to be rendered.
m_loadingComplete = true;
});
}
Se você executar o programa, verá que o campo pula junto com a bola. Para fazer com que o campo não se mova, altere o método Render
:
// Renders one frame using the Starter Kit helpers.
void Game::Render()
{
// snip
for (UINT i = 0; i < m_meshModels.size(); i++)
{
XMMATRIX modelTransform = rotation;
String^ meshName = ref new String(m_meshModels[i]->Name());
m_graphics.UpdateMiscConstants(m_miscConstants);
if (String::CompareOrdinal(meshName, L"Sphere_Node") == 0)
m_meshModels[i]->Render(m_graphics, modelTransform);
else
m_meshModels[i]->Render(m_graphics, XMMatrixIdentity());
}
}
Com esta mudança, a transformação é aplicada somente na bola. O campo é renderizado sem transformação. Se você executar o código agora, verá que a bola pula no campo, mas “entra” nele em baixo. Corrija este bug usando uma translação de −0.5 no eixo y. Selecione o campo de futebol e mude sua propriedade Translation no eixo y para −0.5. Agora, quando executar a aplicação, você pode ver a bola pulando no campo, como na Figura 8.
![]()
Figura 8.Bola pulando no campo
Configurando a Câmera e a Posição da Bola
A bola está posicionada no centro do campo, mas não é isso o que você quer. Para este jogo, a bola deve estar posicionada na marca do pênalti. Se você olhar no editor de cenas na Figura 9, você pode ver que, para fazer isso, deve efetuar uma translação na bola no eixo x, mudando a posição da bola no método Render
em Game.cpp
:
rotation *= XMMatrixTranslation(63.0, m_translation, 0);
A bola é movida de 63 unidades no eixo x, o que a coloca na marca do pênalti.
![]()
Figura 9.Campo com o eixo X (vermelho) e Z (azul)
Com esta mudança, você não deve ver mais a bola, pois a câmera está posicionada em outra direção – no meio do campo, olhando para o centro. Você deve reposicionar a câmera, de maneira que ela aponte para a linha do gol, o que pode fazer em CreateWindowSizeDependentResources
em Game.cpp
:
m_graphics.GetCamera().SetViewport((UINT) outputSize.Width, (UINT) outputSize.Height);
m_graphics.GetCamera().SetPosition(XMFLOAT3(25.0f, 10.0f, 0.0f));
m_graphics.GetCamera().SetLookAt(XMFLOAT3(100.0f, 0.0f, 0.0f));
float aspectRatio = outputSize.Width / outputSize.Height;
float fovAngleY = 30.0f * XM_PI / 180.0f;
if (aspectRatio < 1.0f)
{
// Portrait or snap view
m_graphics.GetCamera().SetUpVector(XMFLOAT3(1.0f, 0.0f, 0.0f));
fovAngleY = 120.0f * XM_PI / 180.0f;
}
else
{
// Landscape view.
m_graphics.GetCamera().SetUpVector(XMFLOAT3(0.0f, 1.0f, 0.0f));
}
m_graphics.GetCamera().SetProjection(fovAngleY, aspectRatio, 1.0f, 100.0f);
A posição da câmera está entre a marca do meio de campo e a marca do pênalti, olhando para a linha do gol. A nova vista é semelhante à Figura 10.
![]()
Figura 10. Bola reposicionada com a nova posição da câmera
Agora, você deve adicionar o gol.
Adicionando os Postes de Gol
Para adicionar o gol ao campo, você precisa de uma nova cena 3D com o gol. Você pode desenhar seu próprio, ou então obter um modelo pronto. Com o modelo, você deve adicioná-lo à pasta Assets, de maneira que ele possa ser compilado e usado.
O modelo deve ser carregado no método CreateDeviceDependentResources
em Game.cpp
:
auto loadMeshTask = Mesh::LoadFromFileAsync(
m_graphics,
L"gamelevel.cmo",
L"",
L"",
m_meshModels)
.then([this]()
{
return Mesh::LoadFromFileAsync(
m_graphics,
L"field.cmo",
L"",
L"",
m_meshModels,
false // Do not clear the vector of meshes
);
}).then([this]()
{
return Mesh::LoadFromFileAsync(
m_graphics,
L"soccer_goal.cmo",
L"",
L"",
m_meshModels,
false // Do not clear the vector of meshes
);
});
Uma vez carregado, posicione-o e desenhe-o no método Render
em Game.cpp
:
auto goalTransform = XMMatrixScaling(2.0f, 2.0f, 2.0f) * XMMatrixRotationY(-XM_PIDIV2)* XMMatrixTranslation(85.5f, -0.5, 0);
for (UINT i = 0; i < m_meshModels.size(); i++)
{
XMMATRIX modelTransform = rotation;
String^ meshName = ref new String(m_meshModels[i]->Name());
m_graphics.UpdateMiscConstants(m_miscConstants);
if (String::CompareOrdinal(meshName, L"Sphere_Node") == 0)
m_meshModels[i]->Render(m_graphics, modelTransform);
else if (String::CompareOrdinal(meshName, L"Plane_Node") == 0)
m_meshModels[i]->Render(m_graphics, XMMatrixIdentity());
else
m_meshModels[i]->Render(m_graphics, goalTransform);
}
Esta mudança aplica uma transformação ao gol e renderiza-o. A transformação é uma combinação de três transformações: uma escala (multiplicando o tamanho original por 2), uma rotação de 90 graus e uma translação de 85.5 unidades no eixo x e -0.5 unidades no eixo y, devido ao deslocamento que foi aplicado ao campo. Desta maneira o gol é posicionado de frente para o campo, na linha do gol, como mostrado na Figura 11. Note que a ordem das transformações é importante: se você aplica a rotação após a translação, o gol será renderizado em uma posição completamente diferente e você não verá nada.
![]()
Figura 11. Campo com o gol posicionado
Chutando a Bola
Todos os elementos estão posicionados, mas a bola ainda está pulando. É hora de chutá-la. Para fazer isso, você deve afiar seus conhecimentos de física novamente. O chute da bola parece com algo como a Figura 12.
![]()
Figura 12. Esquema de um chute de bola
A bola é chutada com uma velocidade inicial v0, com um ângulo α (se você não se lembra de suas aulas de física, jogue um pouco de Angry Birds para ver isso em ação). O movimento da bola pode ser decomposto em dois movimentos diferentes: o movimento horizontal é um movimento com velocidade constante (eu admito que não há efeito de fricção nem efeitos de vento), e o movimento vertical é semelhante ao que foi usado anteriormente. A equação do movimento horizontal é:
sX = s0 + v0*cos(α)*t
. . . e a do movimento vertical é:
sY = s0 + v0*sin(α)*t – ½*g*t2
Agora você tem duas translações: uma no eixo x e outra no eixo y. Considerando que o chute é a 45 graus, cos(α) = sin(α) = sqrt(2)/2, so v0*cos(α) = v0*sin(α)*t. Você quer que o chute entre no gol, assim a distância deve ser maior que 86 (a linha do gol está a 85.5). Você quer que a bola chegue no gol em 2 segundos, assim quando substitui estes valores na primeira equação, você obtém:
86 = 63 + v0 * cos(α) * 2 ≥ v0 * cos(α) = 23/2 = 11.5
Substituindo os valores nas equações, a equação de translação no eixo yé:
sY = 0 + 11.5 * t – 5 * t2
. . . e no eixo xé:
sX = 63 + 11.5 * t
Com a equação do eixo y, você sabe o tempo que a bola atinge o solo novamente, usando a equação de segundo grau (sim, eu sei que você pensou que nunca usaria ela, mas aí está):
(−b ± sqrt(b2− 4*a*c))/2*a ≥ (−11.5 ± sqrt(11.52– 4 * −5 * 0)/2 * −5 ≥ 0 or 23/10 ≥ 2.3s
Com estas equações, você pode substituir a translação para a bola. Inicialmente, em Game.h
, crie variáveis para armazenar as translações nos três eixos:
float m_translationX, m_translationY, m_translationZ;
Então, no método Update
em Game.cpp
, adicione as equações:
void Game::Update(DX::StepTimer const& timer)
{
// Rotate scene.
m_rotation = static_cast<float>(timer.GetTotalSeconds()) * 0.5f;
auto totalTime = (float) fmod(timer.GetTotalSeconds(), 2.3f);
m_translationX = 63.0 + 11.5 * totalTime;
m_translationY = 11.5 * totalTime - 5 * totalTime*totalTime;
}
O método Render
usa as novas translações:
rotation *= XMMatrixTranslation(m_translationX, m_translationY, 0);
Se você executar o programa agora, verá o gol com a bola entrando no meio dele. Se quiser que a bola vá para outras direções, você deve adicionar um ângulo horizontal para o chute. Você deve fazer isso com uma translação no eixo z.
A Figura 13 mostra que a distância entre a marca do pênalti e o gol é de 22.5 e a distância entre os postes do gol é de 14. Isto faz com que α = atan(7/22.5), ou 17 graus. Você poderia calcular a translação no eixo z, mas para fazer isso mais simples, a bola deve chegar na linha do gol ao mesmo tempo que alcança o poste do gol. Isso significa que deve andar 7/22.5 unidades no eixo z enquanto que anda 1 unidade no eixo x. Assim, a equação no eixo zé:
sz = 11.5 * t/3.2 ≥ sz = 3.6 * t
![]()
Figura 13. Esquema da distância entre o pênalti e o gol
Esta é a translação para alcançar o gol. Qualquer translação com velocidade menor terá um ângulo menor. Para alcançar o gol, a velocidade no eixo deve estar entre −3.6 (poste esquerdo) e 3.6 (poste direito). Se você considerar que a bola deve entrar totalmente no gol, a distância máxima deve ser 6/22.5, e a faixa de velocidades deve estar entre -3 e 3. Com estes valores, você pode configurar o ângulo de chute com este código no método Update
:
void Game::Update(DX::StepTimer const& timer)
{
// Rotate scene.
m_rotation = static_cast<float>(timer.GetTotalSeconds()) * 0.5f;
auto totalTime = (float) fmod(timer.GetTotalSeconds(), 2.3f);
m_translationX = 63.0 + 11.5 * totalTime;
m_translationY = 11.5 * totalTime - 5 * totalTime*totalTime;
m_translationZ = 3 * totalTime;
}
A translação no eixo z será usada no método Render
:
rotation *= XMMatrixTranslation(m_translationX, m_translationY, m_translationZ);….
Você deve ter algo como na Figura 14.
![]()
Figura 14. Chute com um ângulo
Adicionando um Goleiro
Com o movimento da bola e gol no lugar, você deve agora adicionar um goleiro para pegar a bola. O goleiro será um cubo distorcido. Na pasta Assets, adicione um novo item – uma nova cena 3D – e chame-o de goalkeeper.fbx
.
Adicione um cubo da barra de ferramentas e selecione-o. Configure sua escala para 0.3 no eixo x, 1.9 no eixo y e 1 no eixo z. Mude sua propriedade MaterialAmbient
para 1 em Red
e 0 em Blue
e Green
, para deixa-lo na cor vermelha. Mude a propriedade Red em MaterialSpecular
para 1 e MaterialSpecularPower
para 0.2.
Carregue o novo recurso no método CreateDeviceDependentResources
:
auto loadMeshTask = Mesh::LoadFromFileAsync(
m_graphics,
L"gamelevel.cmo",
L"",
L"",
m_meshModels)
.then([this]()
{
return Mesh::LoadFromFileAsync(
m_graphics,
L"field.cmo",
L"",
L"",
m_meshModels,
false // Do not clear the vector of meshes
);
}).then([this]()
{
return Mesh::LoadFromFileAsync(
m_graphics,
L"soccer_goal.cmo",
L"",
L"",
m_meshModels,
false // Do not clear the vector of meshes
);
}).then([this]()
{
return Mesh::LoadFromFileAsync(
m_graphics,
L"goalkeeper.cmo",
L"",
L"",
m_meshModels,
false // Do not clear the vector of meshes
);
});
O próximo passo é posicionar e renderizar o goleiro no centro do gol. Você deve fazer isso no método Render
de Game.cpp
:
void Game::Render()
{
// snip
auto goalTransform = XMMatrixScaling(2.0f, 2.0f, 2.0f) * XMMatrixRotationY(-XM_PIDIV2)* XMMatrixTranslation(85.5f, -0.5f, 0);
auto goalkeeperTransform = XMMatrixTranslation(85.65f, 1.4f, 0);
for (UINT i = 0; i < m_meshModels.size(); i++)
{
XMMATRIX modelTransform = rotation;
String^ meshName = ref new String(m_meshModels[i]->Name());
m_graphics.UpdateMiscConstants(m_miscConstants);
if (String::CompareOrdinal(meshName, L"Sphere_Node") == 0)
m_meshModels[i]->Render(m_graphics, modelTransform);
else if (String::CompareOrdinal(meshName, L"Plane_Node") == 0)
m_meshModels[i]->Render(m_graphics, XMMatrixIdentity());
else if (String::CompareOrdinal(meshName, L"Cube_Node") == 0)
m_meshModels[i]->Render(m_graphics, goalkeeperTransform);
else
m_meshModels[i]->Render(m_graphics, goalTransform);
}
}
Com este código, o goleiro é posicionado no centro do gol, como mostrado na Figura 15 (note que a posição da câmera está diferente para a imagem).
![]()
Figura 15. Goleiro no centro do gol
Agora, você deve fazer o goleiro se movimentar para os lados para pegar a bola. O usuário irá usar as teclas para a direita e para a esquerda para mudar a posição do goleiro.
O movimento do goleiro é limitado pelos postes do gol, posicionados a +7 e −7 unidades no eixo z. O goleiro tem 1 unidade em ambas direções, assim, ele pode ser movimentado até 6 unidades em cada lado.
As teclas pressionadas são interceptadas na página XAML (DirectXPage.xaml
) e serão redirecionadas para a classe Game
. Você deve adicionar um manipulador de eventos KeyDown
em DirectXPage.xaml
:
<Page
x:Class="StarterKit.DirectXPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:StarterKit"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" KeyDown="OnKeyDown">
O manipulador de eventos em DirectXPage.xaml.cpp
é:
void DirectXPage::OnKeyDown(Platform::Object^ sender, Windows::UI::Xaml::Input::KeyRoutedEventArgs^ e)
{
m_main->OnKeyDown(e->Key);
}
m_main
é a instância da classe StarterKitMain
que renderiza as cenas do jogo e do FPS. Você deve declarar um método público em StarterKitMain.h
:
class StarterKitMain : public DX::IDeviceNotify
{
public:
StarterKitMain(const std::shared_ptr<DX::DeviceResources>& deviceResources);
~StarterKitMain();
// Public methods passed straight to the Game renderer.
Platform::String^ OnHitObject(int x, int y) {
return m_sceneRenderer->OnHitObject(x, y); }
void OnKeyDown(Windows::System::VirtualKey key) {
m_sceneRenderer->OnKeyDown(key); }….
Este método redireciona a tecla para o método OnKeyDown
na classe Game
. Agora, você deve declarar o método OnKeyDown
em Game.h
:
class Game
{
public:
Game(const std::shared_ptr<DX::DeviceResources>& deviceResources);
void CreateDeviceDependentResources();
void CreateWindowSizeDependentResources();
void ReleaseDeviceDependentResources();
void Update(DX::StepTimer const& timer);
void Render();
void OnKeyDown(Windows::System::VirtualKey key);….
Este método processa a tecla pressionada e move o goleiro com as setas. Antes de criar o método, você deve declarar um campo privado em Game.h
para armazenar a posição do goleiro:
class Game
{
// snip
private:
// snip
float m_goalkeeperPosition;
A posição do goleiro é inicialmente 0 e será incrementada ou decrementada quando o usuário pressiona uma seta. Se a posição for maior que 6 ou menor que -6, a posição do goleiro não será alterada. Você faz isso no método OnKeyDown
em Game.cpp
:
void Game::OnKeyDown(Windows::System::VirtualKey key)
{
const float MaxGoalkeeperPosition = 6.0;
const float MinGoalkeeperPosition = -6.0;
if (key == Windows::System::VirtualKey::Right)
m_goalkeeperPosition = m_goalkeeperPosition >= MaxGoalkeeperPosition ?
m_goalkeeperPosition : m_goalkeeperPosition + 0.1f;
else if (key == Windows::System::VirtualKey::Left)
m_goalkeeperPosition = m_goalkeeperPosition <= MinGoalkeeperPosition ?
m_goalkeeperPosition : m_goalkeeperPosition - 0.1f;
}
A nova posição do goleiro é usada no método Render
de Game.cpp
, aonde a posição do goleiro é calculada:
auto goalkeeperTransform = XMMatrixTranslation(85.65f, 1.40f, m_goalkeeperPosition);
Com estas mudanças, você pode executar o jogo e ver que o goleiro se movimenta para a direita e para a esquerda quando você pressiona as teclas de setas (Figura 16).
![]()
Figura 16. Jogo com o goleiro em posição
Até agora, a bola se movimenta o tempo todo, mas não é isso que você quer. A bola deveria se mover apenas depois de chutada e parar quando atinge o gol. Da mesma maneira, o goleiro não deve se mexer antes que a bola seja chutada.
Você deve declarar um campo privado, m_isAnimating
em Game.h
, para que o jogo saiba que a bola está se movendo:
class Game
{
public:
// snip
private:
// snip
bool m_isAnimating;
Esta variável é usada nos métodos Update
e Render
em Game.cpp
para que a bola só se mova quando m_isAnimating
é verdadeiro:
void Game::Update(DX::StepTimer const& timer)
{
if (m_isAnimating)
{
m_rotation = static_cast<float>(timer.GetTotalSeconds()) * 0.5f;
auto totalTime = (float) fmod(timer.GetTotalSeconds(), 2.3f);
m_translationX = 63.0f + 11.5f * totalTime;
m_translationY = 11.5f * totalTime - 5.0f * totalTime*totalTime;
m_translationZ = 3.0f * totalTime;
}
}
void Game::Render()
{
// snip
XMMATRIX modelTransform;
if (m_isAnimating)
{
modelTransform = XMMatrixRotationY(m_rotation);
modelTransform *= XMMatrixTranslation(m_translationX,
m_translationY, m_translationZ);
}
else
modelTransform = XMMatrixTranslation(63.0f, 0.0f, 0.0f);
….
A variável modelTransform
foi colocada no topo do loop. As teclas de setas devem ser processadas no método OnKeyDown
somente quando m_isAnimating
é verdadeiro:
void Game::OnKeyDown(Windows::System::VirtualKey key)
{
const float MaxGoalkeeperPosition = 6.0f;
if (m_isAnimating)
{
auto goalKeeperVelocity = key == Windows::System::VirtualKey::Right ?
0.1f : -0.1f;
m_goalkeeperPosition = fabs(m_goalkeeperPosition) >= MaxGoalkeeperPosition ?
m_goalkeeperPosition :
m_goalkeeperPosition + goalKeeperVelocity;
}
}
O passo seguinte é chutar a bola. Isto acontece quando o usuário pressiona a barra de espaço. Declare um novo campo privado, m_isKick
, em Game.h
:
class Game
{
public:
// snip
private:
// snip
bool m_isKick;
Configure este campo para true no método OnKeyDown
em Game.cpp
:
void Game::OnKeyDown(Windows::System::VirtualKey key)
{
const float MaxGoalkeeperPosition = 6.0f;
if (m_isAnimating)
{
auto goalKeeperVelocity = key == Windows::System::VirtualKey::Right ?
0.1f : -0.1f;
m_goalkeeperPosition = fabs(m_goalkeeperPosition) >= MaxGoalkeeperPosition ?
m_goalkeeperPosition :
m_goalkeeperPosition + goalKeeperVelocity;
}
else if (key == Windows::System::VirtualKey::Space)
m_isKick = true;
}
Quando m_isKick
é verdadeiro, a animação é iniciada no Update:
void Game::Update(DX::StepTimer const& timer)
{
if (m_isKick)
{
m_startTime = static_cast<float>(timer.GetTotalSeconds());
m_isAnimating = true;
m_isKick = false;
}
if (m_isAnimating)
{
auto totalTime = static_cast<float>(timer.GetTotalSeconds()) - m_startTime;
m_rotation = totalTime * 0.5f;
m_translationX = 63.0f + 11.5f * totalTime;
m_translationY = 11.5f * totalTime - 5.0f * totalTime*totalTime;
m_translationZ = 3.0f * totalTime;
if (totalTime > 2.3f)
ResetGame();
}
}
A hora inicial do chute é armazenada na variável m_startTime
(declarado como um campo privado em Game.h
) e é usada para comutar o tempo para o chute. Se for acima de 2.3 segundos, o jogo é reinicializado (a bola deve ter alcançado o gol). Você declara o método ResetGame
como privado em Game.h
:
void Game::ResetGame()
{
m_isAnimating = false;
m_goalkeeperPosition = 0;
}
Este método configure m_isAnimating
para false e reinicializa a posição do goleiro. A bola não precisa ser reposicionada, pois ela vai ser desenhada na marca do pênalti se m_isAnimating
é falso. Outra mudança que você deve fazer é o ângulo de chute. Este código fixa o chute próximo ao poste direito:
m_translationZ = 3.0f * totalTime;
Você deve mudar isto, de modo que o chute seja aleatório e o usuário não saiba onde será. Você deve declarar um campo privado m_ballAngle
em Game.h
e inicializá-lo quando a bola for chutada no método Update
:
void Game::Update(DX::StepTimer const& timer)
{
if (m_isKick)
{
m_startTime = static_cast<float>(timer.GetTotalSeconds());
m_isAnimating = true;
m_isKick = false;
m_ballAngle = (static_cast <float> (rand()) /
static_cast <float> (RAND_MAX) -0.5f) * 6.0f;
}
…
Rand()/RAND_MAX
resulta em um número entre 0 e 1.Subtraia 0.5 do resultado, de modo que o número fique entre -0.5 e 0.5 e multiplique-o por 6, para que o ângulo final fique entre -3 e 3. Para que sejam criadas sequências diferentes a cada jogo, você deve inicializar o gerador, chamando srand
no método CreateDeviceDependentResources
:
void Game::CreateDeviceDependentResources()
{
srand(static_cast <unsigned int> (time(0)));
…
Para chamar a função time, você deve incluir <ctime>
. Você irá usar m_ballAngle
no método Update
para configurar o novo ângulo para a bola:
m_translationZ = m_ballAngle * totalTime;
A maior parte do código está ok, mas você deve saber se o goleiro pegou a bola ou foi gol. Use um método simples para saber isso: quando a bola chega na linha do gol, você verifica se o retângulo da bola intercepta o retângulo do goleiro. Se quiser, pode usar métodos mais complexos para determinar um gol, mas para nossas necessidades, isto é suficiente. Todos os cálculos são feitos no método Update
:
void Game::Update(DX::StepTimer const& timer)
{
if (m_isKick)
{
m_startTime = static_cast<float>(timer.GetTotalSeconds());
m_isAnimating = true;
m_isKick = false;
m_isGoal = m_isCaught = false;
m_ballAngle = (static_cast <float> (rand()) /
static_cast <float> (RAND_MAX) -0.5f) * 6.0f;
}
if (m_isAnimating)
{
auto totalTime = static_cast<float>(timer.GetTotalSeconds()) - m_startTime;
m_rotation = totalTime * 0.5f;
if (!m_isCaught)
{
// ball traveling
m_translationX = 63.0f + 11.5f * totalTime;
m_translationY = 11.5f * totalTime - 5.0f * totalTime*totalTime;
m_translationZ = m_ballAngle * totalTime;
}
else
{
// if ball is caught, position it in the center of the goalkeeper
m_translationX = 83.35f;
m_translationY = 1.8f;
m_translationZ = m_goalkeeperPosition;
}
if (!m_isGoal && !m_isCaught && m_translationX >= 85.5f)
{
// ball passed the goal line - goal or caught
auto ballMin = m_translationZ - 0.5f + 7.0f;
auto ballMax = m_translationZ + 0.5f + 7.0f;
auto goalkeeperMin = m_goalkeeperPosition - 1.0f + 7.0f;
auto goalkeeperMax = m_goalkeeperPosition + 1.0f + 7.0f;
m_isGoal = (goalkeeperMax < ballMin || goalkeeperMin > ballMax);
m_isCaught = !m_isGoal;
}
if (totalTime > 2.3f)
ResetGame();
}
}
Declare dois campos privados em Game.h
: m_isGoal
e m_IsCaught
. Estes campos dizem se o foi marcado um gol ou se o goleiro pegou a bola. Se ambos forem falsos, a bola está viajando. Quando a bola chega na linha do gol, o programa calcula os limites da bola e do goleiro e determina se os limites da bola se sobrepõem aos limites do goleiro. Se você olhar no código, verá que eu somei 7.0 para cada limite. Eu fiz isso porque os limites podem ser negativos ou positivos, e isso iria complicar o cálculo. Adicionando 7.0 você se assegura que todos os números são positivos, o que simplifica o cálculo. Se a bola for pega, ela é posicionada no centro do goleiro. m_isGoal
e m_IsCaught
são reinicializados quando acontece um chute. Agora é hora de adicionar um placar ao jogo.
Adicionando um Placar
Em um jogo DirectX, você pode renderizar o placar com Direct2D, mas quando você está desenvolvendo um jogo Windows 8, você tem outra maneira de fazer isso: usando ZAML. Você pode sobrepor elementos XAML no seu jogo e criar uma ponte entre os elementos XAML e sua lógica de jogo. Esta é uma maneira mais fácil de mostrar informações e interagir com o usuário, pois você não terá que lidar com posições de elementos, renderização ou loops de atualização.
O Starter Kit vem com um placar XAML (aquele usado para marcar os toques nos elementos). Você deve apenas modificá-lo para marcar o placar do jogo. O primeiro passo é mudar DirectXPage.xaml
para alterar o placar:
<SwapChainPanel x:Name="swapChainPanel" Tapped="OnTapped"><Border VerticalAlignment="Top" HorizontalAlignment="Center" Padding="10" Background="Black"
Opacity="0.7"><StackPanel Orientation="Horizontal"><TextBlock x:Name="ScoreUser" Text="0" Style="{StaticResource HudCounter}"/><TextBlock Text="x" Style="{StaticResource HudCounter}"/><TextBlock x:Name="ScoreMachine" Text="0" Style="{StaticResource HudCounter}"/></StackPanel></Border></SwapChainPanel>
Enquanto está ali, você pode remover a barra do aplicativo, pois ela não será usada neste jogo. Você removeu todos os contadores de toques no placar, assim, também deve remover o código que menciona eles no manipulador OnTapped
em DirectXPage.xaml.cpp
:
void DirectXPage::OnTapped(Object^ sender, TappedRoutedEventArgs^ e)
{
}
Você também pode remover OnPreviousColorPressed
, OnNextColorPressed
, e ChangeObjectColor
das páginas cpp
e h porque eles foram usados na barra do aplicativo que você removeu.
Para atualizar o placar do jogo, deve haver alguma forma de comunicar entre a classe Game
e a página XAML. O placar do jogo é atualizado na classe Game
, enquanto o placar é mostrado na página XAML. Uma maneira de fazer isso é criar um evento na classe Game
, mas esta maneira tem um problema. Se você adiciona um evento para a classe Game
, você obtém um erro de compilação: “a WinRT event declaration must occur in a WinRT class.” Isto acontece porque Game
não é uma classe WinRT (ref)
. Para ser uma classe WinRT
, você deve defini-la como public ref e selá-la:
public ref class Game sealed
Você poderia mudar a classe para fazer isso, mas é melhor ir emoutra direção: criar uma nova classe – neste caso, uma classe WinRT
—e usá-la para comunicar entre a classe Game
e a página XAML. Crie uma nova classe e chame-a de ViewModel
:
#pragma once
ref class ViewModel sealed
{
public:
ViewModel();
};
Em ViewModel.h
, adicione o evento e as propriedades necessárias para atualizar o placar:
#pragma once
namespace StarterKit
{
ref class ViewModel sealed
{
private:
int m_scoreUser;
int m_scoreMachine;
public:
ViewModel();
event Windows::Foundation::TypedEventHandler<Object^, Platform::String^>^ PropertyChanged;
property int ScoreUser
{
int get()
{
return m_scoreUser;
}
void set(int value)
{
if (m_scoreUser != value)
{
m_scoreUser = value;
PropertyChanged(this, L"ScoreUser");
}
}
};
property int ScoreMachine
{
int get()
{
return m_scoreMachine;
}
void set(int value)
{
if (m_scoreMachine != value)
{
m_scoreMachine = value;
PropertyChanged(this, L"ScoreMachine");
}
}
};
};
}
Declare um campo privado do tipo ViewModel
em Game.h
(você deve incluir ViewModel.h
em Game.h
). Você deve definir também um getter público para este campo:
class Game
{
public:
// snip
StarterKit::ViewModel^ GetViewModel();
private:
StarterKit::ViewModel^ m_viewModel;
Este campo é inicializado no construtor de Game.cpp
:
Game::Game(const std::shared_ptr<DX::DeviceResources>& deviceResources) :
m_loadingComplete(false),
m_deviceResources(deviceResources)
{
CreateDeviceDependentResources();
CreateWindowSizeDependentResources();
m_viewModel = ref new ViewModel();
}
O corpo do getter é:
StarterKit::ViewModel^ Game::GetViewModel()
{
return m_viewModel;
}
Quando o chute atual termina, as variáveis são atualizadas em ResetGame em ResetGame
in Game.cpp
:
void Game::ResetGame()
{
if (m_isCaught)
m_viewModel->ScoreUser++;
if (m_isGoal)
m_viewModel->ScoreMachine++;
m_isAnimating = false;
m_goalkeeperPosition = 0;
}
Quando uma destas duas propriedades muda, o evento PropertyChanged
é levantado, e pode ser manipulado na página XAML. Ainda há um caminho indireto aqui: a página XAML não tem acesso à classe Game
(que não é uma classe ref
) diretamente mas, ao invés, chama a classe StarterKitMain
. Você deve criar um getter para o ViewModel
in StarterKitMain.h
:
class StarterKitMain : public DX::IDeviceNotify
{
public:
// snip
StarterKit::ViewModel^ GetViewModel() { return m_sceneRenderer->GetViewModel(); }
Com esta infraestrutura no lugar, você pode manipular o evento PropertyChanged
de ViewModel
no construtor de DirectXPage.xaml.cpp
:
DirectXPage::DirectXPage():
m_windowVisible(true),
m_hitCountCube(0),
m_hitCountCylinder(0),
m_hitCountCone(0),
m_hitCountSphere(0),
m_hitCountTeapot(0),
m_colorIndex(0)
{
// snip
m_main = std::unique_ptr<StarterKitMain>(new StarterKitMain(m_deviceResources));
m_main->GetViewModel()->PropertyChanged += ref new
TypedEventHandler<Object^, String^>(this, &DirectXPage::OnPropertyChanged);
m_main->StartRenderLoop();
}
O manipulador atualiza o placar (você também deve declará-lo em DirectXPage.xaml.cpp.h):
void StarterKit::DirectXPage::OnPropertyChanged(Platform::Object ^sender, Platform::String ^propertyName)
{
if (propertyName == "ScoreUser")
{
auto scoreUser = m_main->GetViewModel()->ScoreUser;
Dispatcher->RunAsync(CoreDispatcherPriority::Normal, ref new DispatchedHandler([this, scoreUser]()
{
ScoreUser->Text = scoreUser.ToString();
}));
}
if (propertyName == "ScoreMachine")
{
auto scoreMachine= m_main->GetViewModel()->ScoreMachine;
Dispatcher->RunAsync(CoreDispatcherPriority::Normal, ref new DispatchedHandler([this, scoreMachine]()
{
ScoreMachine->Text = scoreMachine.ToString();
}));
}
}
Agora o placar é atualizado a cada vez que acontece um gol ou que o goleiro pega a bola (Figura 17).
![]()
Figura 17. Jogo com atualização do placar
Usando Toque e Sensores no Jogo
O jogo está funcionando bem, mas você ainda pode adicionar elegância a ele. Os novos dispositivos Ultrabook™ tem entrada de toque e sensores que você pode usar para aprimorar o jogo. Ao invés de usar o teclado para chutar a bola e mover o goleiro, o usuário pode chutar a bola tocando na tela e mover o goleiro inclinando a tela para a esquerda ou para a direita.
Para chutar a bola com um toque na tela, use o evento OnTapped
em DirectXPage.cpp
:
void DirectXPage::OnTapped(Object^ sender, TappedRoutedEventArgs^ e)
{
m_main->OnKeyDown(VirtualKey::Space);
}
O código usa o método OnKeyDown
, passando a barra de espaço como parâmetro – da mesma maneira que o usuário tivesse pressionado a barra de espaço. Se quiser, você pode aprimorar o código e obter a posição do toque e somente chutar a bola se o toque for sobre ela. Eu deixo isso para você como lição de casa. Como ponto de partida, o Starter Kit tem código para detector se o usuário tocou um objeto na cena.
O próximo passo é mover o goleiro quando o usuário inclina a tela. Para isso, você deve usar o inclinômetro, que detecta todo movimento na tela. Este sensor retorna três leituras: pitch, roll, e yaw, correspondendo às rotações nos eixos x, y e z respectivamente. Para este jogo, você usará apenas a leitura roll.
Para usar este sensor, você deve obter uma instância para ele, o que você pode fazer usando o método GetDefault
. Então, você configura o intervalo de informação, com código parecido a este de void Game::CreateDeviceDependentResources
in Game.cpp
:
void Game::CreateDeviceDependentResources()
{
m_inclinometer = Windows::Devices::Sensors::Inclinometer::GetDefault();
if (m_inclinometer != nullptr)
{
// Establish the report interval for all scenarios
uint32 minReportInterval = m_inclinometer->MinimumReportInterval;
uint32 reportInterval = minReportInterval > 16 ? minReportInterval : 16;
m_inclinometer->ReportInterval = reportInterval;
}
...
m_inclinometer
é um campo privado declarado em Game.h
. No método Update
reposicione o goleiro:
void Game::Update(DX::StepTimer const& timer)
{
// snip
SetGoalkeeperPosition();
if (totalTime > 2.3f)
ResetGame();
}
}
SetGoalkeeperPosition
reposiciona o goleiro, dependendo da leitura do inclinômetro:
void StarterKit::Game::SetGoalkeeperPosition()
{
if (m_isAnimating && m_inclinometer != nullptr)
{
Windows::Devices::Sensors::InclinometerReading^ reading =
m_inclinometer->GetCurrentReading();
auto goalkeeperVelocity = reading->RollDegrees / 100.0f;
if (goalkeeperVelocity > 0.3f)
goalkeeperVelocity = 0.3f;
if (goalkeeperVelocity < -0.3f)
goalkeeperVelocity = -0.3f;
m_goalkeeperPosition = fabs(m_goalkeeperPosition) >= 6.0f ?
m_goalkeeperPosition : m_goalkeeperPosition + goalkeeperVelocity;
}
}
Com esta mudança, você pode movimentar o goleiro inclinando a tela. Você agora já tem um jogo acabado.
Medida de Performance
Com o jogo executando bem em sua máquina de desenvolvimento, você deve testá-lo num dispositivo móvel menos potente. Uma coisa é desenvolver numa máquina potente com um processador gráfico topo-de-linha e 60 FPS. É completamente diferente rodar em um dispositivo com um processador Intel® Atom™ com uma placa gráfica embutida.
Seu jogo deve executar bem em ambas as máquinas. Para medir a performance, você pode usar as ferramentas incluídas no Visual Studio ou então o Intel® Graphics Performance Analyzers (Intel® GPA), uma suíte de analisadores gráficos que podem detectar gargalos e aumentar a performance de seu jogo. O Intel GPA permite uma análise gráfica de como seu jogo está sendo executado e pode ajudá-lo a fazer com que ele execute mais rápido e melhor.
Conclusão
Finalmente, você chegou ao final da sua jornada. Você iniciou com uma chaleira dançante e terminou com um jogo DirectX, com teclado e entradas de sensores. Com as linguagens ficando cada vez mais semelhantes C++/CX não foi tão difícil de usar para um desenvolvedor C#.
A maior dificuldade é dominar os modelos 3D, faze-los mover e posicioná-los em uma maneira familiar. Para isso você teve que usar alguma física, geometria, trigonometria e matemática.
Em resumo, desenvolver um jogo não é uma tarefa impossível. Com alguma paciência e as ferramentas certas, você pode criar grandes jogos, com performance excelente.
Agradecimentos Especiais
Gostaria de agradecer a Roberto Sonnino pelas suas dicas para o artigo e pela sua revisão técnica.
Créditos de imagens
Para mais informações
Sobre o Autor
Bruno Sonnino é um Microsoft Most Valuable Professional (MVP) no Brasil. Ele é desenvolvedor, consultor e autor, tendo escrito cinco livros de Delphi, publicados em Português pela Pearson Education Brazil, e muitos artigos em revistas e websites brasileiros e americanos.
Intel®Developer Zone offers tools and how-to information for cross-platform app development, platform and technology information, code samples, and peer expertise to help developers innovate and succeed. Join our communities for the Internet of Things, Android*, Intel® RealSense™ Technology and Windows* to download tools, access dev kits, share ideas with like-minded developers, and participate in hackathons, contests, roadshows, and local events.
Intel, the Intel logo, Intel Atom, and Ultrabook are trademarks of Intel Corporation in the U.S. and/or other countries.
*Other names and brands may be claimed as the property of others.
Copyright © 2014. Intel Corporation. All rights reserved.