MOVIMENTAÇÃO
Nessa parte será mostrado um exemplo de implementação da câmera como em jogos FPS(First Person Shooter) e estilo MMORPG (Massively Multiplayer Online Role-Playing Game). Podem existir várias modificações apartir do que será exposto no texto, isso irá depender do jogo a ser implementado.
First Person Shooter
A movimentação estilo FPS é bem simples de ser implementada. Ela pode ser encontrada em jogos como Quake, Unreal, Medal of Honor, etc. Um outro exemplo dessa implementação pode ser encontrada nesta página no wiki da Ogre.
forward = left = right = back = up = down = turnL = turnR = false; mouseEsqDown = mouseDirDown = false;
Vamos utilizar as variáveis acima, criadas na parte 6 dos tutoriais para iniciantes. Elas serão responsáveis para determinar qual a(s) tecla(s) de movimentação do personagem foram pressionadas. E as do mouse(mouseEsqDown e mouseDirDown) qual qual botão do mouse esta pressionado no momento.
SceneNode *camNode; SceneNode *nodeCamCentro;
No arquivo AplicationListener.h adicione os seguintes SceneNodes que serão utilizados.
//adiciona o SceneNode que servirá para rotacionar a camera
nodeCamCentro = scene_mgr->getRootSceneNode()->createChildSceneNode("camCentro");
//adicona o SceneNode da camera
camNode = nodeCamCentro->createChildSceneNode("camNode");
camNode->attachObject(camera);
camNode->setPosition(nodeCamCentro->getPosition() + Vector3(-3,5,0));
O trecho acima deve ser adicionado no método construtor da classe AplicationListener, ele irá monstar a estrutura para a movimentação da câmera.
O nodeCamCentro é um SceneNode que ficará exatamente na mesma posição onde o nosso personagem estará, no nosso exemplo o robô. Esse SceneNode será responsável pela rotação da câmera, usando o princípio de SceneNode pai e SceneNode Filho.
O SceneNode camNode será o da câmera, e deve ser um filho de nodeCamCentro, observe o modo como ele foi criado no código acima. Porque isso? Como toda ação executada pelo pai terá efeito sobre os seus filhos, quando nós rotacionarmos o nodeCamCentro(SceneNode Pai) automaticamente nós rotacionaremos o camNode(SceneNode Filho) e consequentemente a câmera que está anexada a camNode.
A posição inicial de camNode é definida em relação a posição inicial de nodeCamCentro que é obtida logo quando criamos a camNode e definimos como um filho. Para que a câmera fique atrás do robô adicionamos a sua posição inicial o vetor (-3,5,0) para que ele fique 3 unidades após o robô já que inicialmente sua "frente" está voltada para a o eixo positivo de X. E cinco unidades acima para que o SceneNode não tenha problemas de rotação no eixo Y.
//define como alvo para a camera o nodeCamCentro camNode->setAutoTracking(true, nodeCamCentro); camNode->setFixedYawAxis(true);
Utilizando a função setAutoTracking() definimos o SceneNode que a câmera ficará sempre olhando, no nosso exemplo será nodeCamCentro. O método setFixedYawAxis() é necessário para ajustar os valores de rotação.
//chama a animação do robô parado
animationState = scene_mgr->getEntity("robo")->getAnimationState("Idle");
animationState->setLoop(true);
animationState->setEnabled(true);
O trecho de código coloca o robô na sua animação de parado, você pode coloca-lo no construtor da classe.
if(e.state.X.rel != 0 || e.state.Y.rel != 0 ){
nodeCamCentro->yaw(Degree(-e.state.X.rel * rotateCam), Node::TS_WORLD);
nodeCamCentro->pitch(Degree(-e.state.Y.rel * rotateCam), Node::TS_LOCAL);
}
Adicione o código acima no método mouseMoved(), ele será responsável pela rotação da câmera quando o mouse se movimentar. Caso exista algum movimento do mouse na direção X ou Y a câmera será rotacionada adequadamente. Observe que o SceneNode rotacionado é nodeCamCentro para consequentemente rotacionar o SceneNode da câmera.
return naviMgr->injectMouseMove(camera->getViewport()->getActualWidth()/2,
camera->getViewport()->getActualHeight()/2);
Ainda no método mouseMoved() devemos modificar o seu retorno afim de que o ponteiro do mouse fique sempre no centro da tela. Para isso usamos a função injectMouseMove() onde os dois parâmetros são as coordenadas da nova posição do ponteiro. Como ele deve ficar no centro obtemos a altura e largura do viewport através dos métodos getActualWidth() e getActualHeight() respectivamente e dividimos por dois.
//atualiza a animação do char animationState->addTime(evt.timeSinceLastFrame);
Indo para o método frameStarted() adicione o código acima para que a animação do robô seja atualizada. Para mais informações sobre animação, acesse a parte 1 dos tutoriais intemediários.
transVector = Vector3::ZERO; if(forward) transVector.z -= moveCam; if(back) transVector.z += moveCam; if(left) transVector.x -= moveCam; if(right) transVector.x += moveCam; if(up) transVector.y += moveCam; if(down) transVector.y -= moveCam;
O vetor transVector que será utilizado para transladar, movimentar o robô de acordo com as teclas pressionadas, como foi explicado na parte 6 dos tutoriais para iniciantes. Atente ao fato de que ele sempre é zerado antes da atualização de seus valores(transVector = Vector3::ZERO).
scene_mgr->getSceneNode("roboNode")->translate(
scene_mgr->getSceneNode("roboNode")->getOrientation() * transVector *
evt.timeSinceLastFrame);
Continuando no método frameStarted() para movimentar o robô, usamos a função translate() onde o parâmentro será um vetor resultado da multiplicação dos vetores da orientação do robô, de transVector e do tempo passado desde o último frame. Você pode ajustar a velocidade de movimentação substituindo evt.timeSinceLastFrame por um outro valor.
deslocamento = (scene_mgr->getSceneNode(
"roboNode")->getWorldPosition() - nodeCamCentro->getPosition());
nodeCamCentro->translate(deslocamento + Vector3(0,60,0));
O vetor deslocamento irá guardar a distância do robô e o SceneNode nodeCamCentro, pois esse SceneNode deve sempre estar na mesma posição do robô. Com o valor da distância, deslocamos o nodeCamCentro até a posição do robô, ajustando a sua posição 60 unidades para cima, afim de que a câmera fique mais adequada. Execute o seu projeto e verifique se está tudo funcionando.
Massively Multiplayer Online Role-Playing Game
A movimentação da câmera estilo MMORPG possui alguns detalhes a mais que a estilo RPG. Um outro exemplo de implementação desse tipo você entra neste tutorial no wiki da Ogre. Apague todo o código da parte de movimentação semelhante a FPS.
scene_mgr->setWorldGeometry("terrain.cfg");
Inicialmente vamos colocar um terreno na cena, já que a maioria dos cenários em MMORPGs são grandes campos abertos. Coloque a linha acima no método CarregaMalhas() do arquivo Inicializa.cpp.
SceneNode *camNode; SceneNode *nodeCamCentro; SceneNode *targetPosition; Real walkSpeed; Real move; Real distancia; //distancia que irá andar Vector3 destino; //para onde o char vai andar Vector3 direcao; //direção do seu movimento Vector3 deslocamento; //distancia da camera ate a sua localização atras do char
Declare as seguintes variáveis no arquivo AplicationListener.h.
//adiciona o SceneNode que servirá para rotacionar a camera
nodeCamCentro = scene_mgr->getRootSceneNode()->createChildSceneNode("camCentro");
//adicona o SceneNode da camera
camNode = nodeCamCentro->createChildSceneNode("camNode");
camNode->attachObject(camera);
camNode->setPosition(nodeCamCentro->getPosition() + Vector3(-300,200,0));
O código acima, que deve ser adicionado ao construtor da classe AplicationListener, é muito semelhante ao da movimentação FPS, mas o ajuste da posição da câmera em relação ao personagem é diferente. Agora os valores (-300,200,0) deixam a câmera mais afastada do personagem.
//targetPosition que guarda a posição de onde o "alvo" que a camera esta sempre
//olhando deve ficar
targetPosition = scene_mgr->getSceneNode("roboNode")->createChildSceneNode("roboAlvo");
targetPosition->setPosition(targetPosition->getPosition() + Vector3(0,60,0));
//define como alvo para a camera o node a_target_node
camNode->setAutoTracking(true, targetPosition);
camNode->setFixedYawAxis(true);
O targetPosition será o SceneNode que a câmera sempre ficará olhando agora. Ele é um filho do SceneNode do robô. Sua posição é ajustada para ficar 60 unidades de altura, para que a câmera ao se aproximar do robô não fique olhando para o chão.
Então o SceneNode que a câmera ficará sempre olhando é definido usando a função setAutoTracking() e depois com o método setFixedYawAxis() são feitos ajustes para as coordenadas.
//chama a animação do robô parado
animationState = scene_mgr->getEntity("robo")->getAnimationState("Idle");
animationState->setLoop(true);
animationState->setEnabled(true);
Mantenha o trecho acima, da animação do robô, no construtor da classe.
if(e.state.Z.rel != 0){
naviMgr->injectMouseWheel(e.state.Z.rel);
//zoom da camera
direcaoCam.z = direcaoCam.z + (e.state.Z.rel * 0.5);
camNode->translate(camNode->getOrientation() * direcaoCam * 0.5);
direcaoCam.z = direcaoCam.z - (e.state.Z.rel * 0.5);
}
No método mouseMoved() vamos adicionamos o trecho acima, que aproximará ou afastará a câmera do personagem(robô), caso o jogador, movemente a "rodinha" do meio do mouse. Caso o jogador movimente a "rodinha", alguns valores da Navi serão atualizados, com a função injectMouseWheel() e então dependendo da movimentação na direção Z, que é feita através da "rodinha", o SceneNode da câmera é movimentado adequadamente.
Observe que não é feita nenhuma verificação da distância da câmera até o robô, dessa forma ocorrerão ações indesejadas(bugs) quando a câmera se aproximar demais e passar da posição do robô. Essa verificação depende da maneira do comportamento da câmera no jogo, dessa forma depende do jogo que esta sendo implementado.
if (!(NaviManager::Get().isAnyNaviFocused()))
{
if (mouseDirDown)
{
nodeCamCentro->yaw(Degree(-e.state.X.rel * rotateCam), Node::TS_WORLD);
nodeCamCentro->pitch(Degree(-e.state.Y.rel * rotateCam), Node::TS_LOCAL);
}
}
Ainda no método mouseMoved() o trecho acima cuida da rotação da câmera. Primeiramente verifica-se se o ponteiro do mouse esta sobre algum elemento da interface, com a função .isAnyNaviFocused(). Se estiver iremos movimentar o elemento da interface com o botão direito do mouse pressionado e não a câmera.
Caso o ponteiro não esteja sobre algum elemento da interface e o botão direito do mouse esteja pressionado, rotacionamos o nodeCamCentro, e consequentemente os seus filhos, ou seja, o SceneNode camNode e então a câmera, usando as funções yaw() para o eixo X e pitch() para o Y.
return naviMgr->injectMouseMove(e.state.X.abs, e.state.Y.abs);
Valor de retorno da função mouseMoved() deve ser alterado para a linha acima, para que o ponteiro do mouse não fique travado no centro da tela.
Ray mouseRay = camera->getCameraToViewportRay(e.state.X.abs/float(e.state.width),
e.state.Y.abs/float(e.state.height));
raySceneQuery->setRay(mouseRay);
raySceneQuery->setSortByDistance(true);
Agora no método onLeftPressed() adicionamos o trecho acima. Criamos um ray de forma especial utilizando a função "getCameraToViewportRay()". Ela cria um ray a partir da localização do ponteiro do mouse no Viewport, com direção perpendicular a do Viewport. Os dois parâmetros dessa função são exatamente a posição X e Y da origem do ray. E com "setRay()" passamos esse ray para o "RaySceneQuery()".
RaySceneQueryResult &result = raySceneQuery->execute(); RaySceneQueryResult::iterator itr;
Com a variável "result" guardamos as informações da execução do ray, ou seja, após o ray percorrer a cena, através do método "execute()", a váriavel "result" terá uma lista com os elementos da cena em que o ray tocou. Um iterator é criado para que a lista seja percorrida.
//testa o resultado da iteração do charRay com a cena
for (itr = result.begin(); itr != result.end(); itr++)
{
Para percorrer toda a lista usamos um for, que vai incrementado a posição na lista até encontrar o final dela(itr != result.end()).
if (itr->worldFragment)
{
//coloca a animação do robo andando
animationState = scene_mgr->getEntity("robo")->getAnimationState("Walk");
animationState->setLoop(true);
animationState->setEnabled(true);
Caso cliquemos em uma parte do terreno(itr->worldFragment) então vamos colocar o robô para andar até a posição onde clicamos. Primeiramente chamamos a animação do robô andando.
//destino guarda a posição do terreno para onde o char vai andar
destino = itr->worldFragment->singleIntersection;
Obtemos a posição exata do local clicado com singleIntersection que retorna um Vetor com esses valores, e atribuimos ao vetor destino que será usado mais a frente.
//coloca o knot na posição para onde o char vai
scene_mgr->getSceneNode("knotNode")->setPosition(itr->worldFragment->singleIntersection);
scene_mgr->getSceneNode("knotNode")->setVisible(true);
Marcamos o local de onde clicamos no mapa com a malha knot.
direcao = destino - scene_mgr->getSceneNode("roboNode")->getPosition();
distancia = direcao.normalise();
A direção do movimento é obtido a partir da subtração dos vetores que guardam a posição de onde o robô vai e a atual localização dele. A distância por ele a ser pecorrida é obtida apartir da normalização do vetor direção, usando a função normalise().
//essa parte do codigo serve para girar a "frente" do char para a direção que
//ele irá andar
src = scene_mgr->getSceneNode("roboNode")->getOrientation() * Vector3::UNIT_X;
if ((src.dotProduct(direcao)) < 0.0001f)
{
scene_mgr->getSceneNode("roboNode")->yaw(Degree(180));
}
else
{
quat = src.getRotationTo(direcao);
scene_mgr->getSceneNode("roboNode")->rotate(quat, Node::TS_WORLD);
}
} //fim do if
O trecho acima irá rotacionar o robô e colocar a sua "frente" na direção correta do movimento. Uma observação deve ser feita sobre a primeira linha desse trecho. O robô foi modelado com a sua "frente" voltada para o eixo positivo de X, dessa forma na primeira linha deve ser colocado Vector3::UNIT_X. Se a sua frente tenha sido modelada para o eixo Z positivo, como é o caso do ninja, uma outra malha que vem na Ogre, deve-se colocar Vector3::UNIT_Z.
move = walkSpeed * evt.timeSinceLastFrame; distancia -= move;
No método frameStarted() atribuimos a variável move, a distância percorrida pelo robô desde o último frame, para isso multiplicamos a velocidade dele pelo tempo decorrido desde o último frame renderizado. Então atualizamos a distância a ser percorrida.
//atualiza a animação do char animationState->addTime(evt.timeSinceLastFrame * 1);
Atualizamos a animação do char com o código acima.
if (distancia <= 0.0f)
{
//coloca o vetor direcao igual a ZERO (indica robô parado)
direcao = Vector3::ZERO;
//chama a animação do robô parado
animationState = scene_mgr->getEntity("robo")->getAnimationState("Idle");
animationState->setLoop(true);
animationState->setEnabled(true);
scene_mgr->getSceneNode("knotNode")->setVisible(false);
}
else
{
//movimenta o robô
scene_mgr->getSceneNode("roboNode")->translate(direcao * move);
}
Quando a distância percorrida pelo robô for menor ou igual a zero, então ele deve ser parado. Dessa forma, definimos o vetor direcao como zero, e chamamos a sua animação parada(Idle) e colocamos a malha knot como invisível.
Mas caso ele ainda tenha alguma distância a ser percorrida, movimentamos o robô usando a função translate() onde os parâmetros são o vetor direcao e a quantidade andada por ele desde o último frame.
deslocamento = (scene_mgr->getSceneNode("roboNode")->getWorldPosition() -
nodeCamCentro->getPosition()) * evt.timeSinceLastFrame;
nodeCamCentro->translate(deslocamento);
O código acima faz o deslocamento do nodeCamCentro que faz a rotação da câmera. Com o trecho(* evt.timeSinceLastFrame), quando o personagem parar o movimento a câmera irá parar a sua movimentação de forma mais suave. Então você pode retirar esse final caso não queira essa suavização. Uma forma de movimentar o personagem usando a biblioteca NxOgre pode ser encontrado neste tutorial. Compile e execute o código.
Italo Mendes
italo.ribeiro@gmail.com
