Adding user input

Our applications so far were boring in terms that no user interaction was possible. We displayed something but we couldn't even move the camera to look at scene at different angle. In this tutorial things will change a bit. Once again, we will create a simple 3D scene but this time we will be able to move around using a keyboard and mouse. We will also introduce the concept of listeners as it will come handy while we’re trying to understand the topics presented here.

There are many schemes of communication between objects. One of them is through events. Say we've got two objects A and B communicating with each other. B wants to know if A is in specific state, let's say if mouse pointer is over it. We could test each time if such a situation occurred and perform some code if it did. That would be a waste though, as in most cases the condition would be false. The better solution is not to test at all but to operate normally till such situation. And here events come to play. Object A is aware of its state. If it changes to "mouse pointer is over me", A sends a message to B letting it know that something happened. B could then respond with the desired action. There are many more advantages in using events. If you are familiar with such languages as C# or Visual Basic you probably use them frequently. Also many C++ GUI libraries are event-driven. If you are not I suggest further reading on this topic as this knowledge will pay off sooner or later.

What events have to do with nGENE though? Well some of its modules use events and events handlers to some extent. This includes GUI, graphical resources and what we are mostly interested in now - user input. When the user presses mouse button or moves mouse the input system notifies user input event handlers that such an event occurred.

In this tutorial we will build up our application with the code from tutorial 1.2, so be sure to read it before continuing. We will let the user change the viewing angles using the mouse and move around the scene using the keyboard. Also it will become possible to close the application by pressing Escape key.

First, here's again the App class definition:

#pragma once
 
#include "FrameworkWin32.h"
#include "ScenePartitionerQuadTree.h"
 
#include "MyInputListener.h"
 
using namespace nGENE::Application;
using nGENE::ScenePartitionerQuadTree;
 
class App: public FrameworkWin32
{
private:
    ScenePartitionerQuadTree* m_pPartitioner;
 
    MyInputListener m_InputListener;
 
public:
    App() {}
    ~App() {}
 
    void createApplication();
};

Not much changes here… only some additional includes, and usings and one new member: m_InputListener. It represents the class we're yet going to implement, so without further ado let's move to the App.cpp:

#include "App.h"
#include "nGENE.h"
 
void App::createApplication()
{
    FrameworkWin32::createApplication();
 
    m_pMouse->setSensitivity(0.02f);
 
    m_pPartitioner = new ScenePartitionerQuadTree();
    SceneManager* sm = Engine::getSingleton().getSceneManager(0);
    sm->setScenePartitioner(m_pPartitioner);
 
    Renderer& renderer = Renderer::getSingleton();
    renderer.setClearColour(0);
    renderer.setCullingMode(CULL_CW);
    uint anisotropy = 0;
    renderer.getDeviceRender().testCapabilities(CAPS_MAX_ANISOTROPY, &anisotropy);
    for(uint i = 0; i < 8; ++i)
        renderer.setAnisotropyLevel(i, anisotropy);
 
    CameraFirstPerson* cam;
    cam = (CameraFirstPerson*)sm->createCamera(L"CameraFirstPerson", L"Camera");
    cam->setVelocity(10.0f);
    cam->setPosition(Vector3(0.0f, 5.0f, -10.0f));
    sm->getRootNode()->addChild(L"Camera", cam);
    Engine::getSingleton().setActiveCamera(cam);
 
    NodeMesh <MeshLoadPolicyXFile>* pSculp = sm->createMesh <MeshLoadPolicyXFile>(L"statue.x");
    pSculp->setPosition(0.0f, 0.0f, 7.0f);
    pSculp->setScale(0.25f, 0.25f, 0.25f);
    Surface* pSurface = pSculp->getSurface(L"surface_2");
    pSurface->flipWindingOrder();
    Material* matstone = MaterialManager::getSingleton().getLibrary(L"default")->getMaterial(L"dotCel");
    pSurface->setMaterial(matstone);
    sm->getRootNode()->addChild(L"Sculpture", *pSculp);
 
    Plane pl(Vector3::UNIT_Y, Point(0.0f, 0.0f, 0.0f));
    PrefabPlane* plane = sm->createPlane(pl, Vector2(80.0f, 80.0f));
    plane->setPosition(0.0f, 1.4, 0.0f);
    pSurface = plane->getSurface(L"Base");
    Material* matGround = MaterialManager::getSingleton().getLibrary(L"default")->getMaterial(L"normalMap");
    pSurface->setMaterial(matGround);
    sm->getRootNode()->addChild(L"Plane", plane);
 
    NodeLight* pLight = sm->createLight(LT_DIRECTIONAL);
    pLight->setPosition(-4.0f, 5.0f, 0.0f);
    pLight->setDirection(Vector3(0.7f, -0.5f, 1.0f));
    pLight->setDrawDebug(false);
    Colour clrWorld(204, 204, 0);
    pLight->setColour(clrWorld);
    pLight->setRadius(180.0f);
    sm->getRootNode()->addChild(L"WorldLight", *pLight);
    renderer.addLight(pLight);
 
    m_InputListener.setApp(this);
    InputSystem::getSingleton().addInputListener(&m_InputListener);
}
 
int main()
{
    App app;
 
    app.createApplication();
    app.run();
 
    app.shutdown();
 
    return 0;
}

Again - not much changed here. The line:

m_pMouse->setSensitivity(0.02f);

sets mouse sensitivity. If you play any games you should be familiar with this setting. We're setting sensitivity to rather small value. m_pMouse is a default mouse (Direct Input compliant) object created by nGENE Framework. It gives you access to various fields and methods. Be sure to check them out.

We also set camera speed to 10 units per second:

cam->setVelocity(10.0f);

The third important change is made here:

m_InputListener.setApp(this);
InputSystem::getSingleton().addInputListener(&m_InputListener);

By using these two lines of code you register event listener to listen to keyboard and mouse events. Or to put it simpler, you let your application respond to the input.

Till now not much new. Let's move to MyInputListener class definition file:

#pragma once
 
#include "InputListener.h"
#include "Vector3.h"
#include "Quaternion.h"
 
using nGENE::InputListener;
using nGENE::KeyboardEvent;
using nGENE::MouseEvent;
using nGENE::Vector3;
using nGENE::Real;
using nGENE::Quaternion;
 
class App;
 
class MyInputListener: public InputListener
{
private:
    App* m_pApp;
 
    Real m_Angle1;
    Real m_Angle2;
 
    long m_prev;
 
public:
    MyInputListener();
    ~MyInputListener();
 
    void handleEvent(const MouseEvent& _evt);
    void handleEvent(const KeyboardEvent& _evt);
 
    void setApp(App* _app) {m_pApp = _app;}
};

Members are not really important here. They represent:

  • pointer to application object,
  • angles of rotation around y and x axes,
  • some helper integer to find out how much time passed since last frame

respectively. Methods are of far greater importance though.

Still remember this lengthy beginning about events and listeners? All the handleEvent(EventType) methods are the ones responding to EventType events. In this case, mouse and keyboard events. So if for instance, a game user presses CTRL key, the "CTRL-key pressed" event is sent to all InputListeners (bear in mind MyInputListener only derives from it) and they respond via handleEvent(const KeyboardEvent& _evt) method. I hope that's easy.

Now we're moving to MyInputListener.cpp:

#include "MyInputListener.h"
#include "nGENE.h"
 
#include "App.h"
 
using namespace nGENE;
 
MyInputListener::MyInputListener():
    m_pApp(NULL),
    m_Angle1(0.0f),
    m_Angle2(0.0f)
{
}
 
MyInputListener::~MyInputListener()
{
}
 
void MyInputListener::handleEvent(const MouseEvent &_evt)
{
    float fDiff = float(Engine::getSingleton().getTimer().getMilliseconds() - m_prev) * 0.001f;
 
    Camera* pCamera = Engine::getSingleton().getActiveCamera();
    if(pCamera)
    {
        if(_evt.type == MET_MOVE_X || _evt.type == MET_MOVE_Y)
        {
            if(_evt.type == MET_MOVE_X)
                m_Angle1 += _evt.fvalue;
            else if(_evt.type == MET_MOVE_Y)
                m_Angle2 += _evt.fvalue;
 
            Quaternion quatRot1(Vector3(0.0f, 1.0f, 0.0f), m_Angle1);
            Quaternion quatRot2(Vector3(1.0f, 0.0f, 0.0f), m_Angle2);
 
            pCamera->setRotation(quatRot1 * quatRot2);
        }
    }
 
    m_prev = Engine::getSingleton().getTimer().getMilliseconds();
}
 
void MyInputListener::handleEvent(const KeyboardEvent& _evt)
{
    if(_evt.isKeyPressed(KC_ESCAPE))
    {
        m_pApp->closeApplication();
        return;
    }
 
    CameraFirstPerson* pCamera = (CameraFirstPerson*)Engine::getSingleton().getActiveCamera();
    if(pCamera)
    {
        Vector3 movement;
        if(_evt.isKeyDown(KC_UP))
            movement.z = 1.0f;
        if(_evt.isKeyDown(KC_DOWN))
            movement.z = -1.0f;
        if(_evt.isKeyDown(KC_LEFT))
            movement.x = -1.0f;
        if(_evt.isKeyDown(KC_RIGHT))
            movement.x = 1.0f;
 
        if(movement.x != 0.0f)
            pCamera->strafe(movement.x);
        if(movement.z != 0.0f)
            pCamera->move(movement.z);
    }
}

In this example we're using mouse events to rotate camera and keyboard ones to move it in the scene and to quit application.

if(pCamera)
{
    if(_evt.type == MET_MOVE_X || _evt.type == MET_MOVE_Y)
    {
        if(_evt.type == MET_MOVE_X)
            m_Angle1 += _evt.fvalue;
        else if(_evt.type == MET_MOVE_Y)
            m_Angle2 += _evt.fvalue;
 
        Quaternion quatRot1(Vector3(0.0f, 1.0f, 0.0f), m_Angle1);
        Quaternion quatRot2(Vector3(1.0f, 0.0f, 0.0f), m_Angle2);
 
        pCamera->setRotation(quatRot1 * quatRot2);
    }
}

Not much magic in here. We test if the mouse has been moved recently (in x or y axis). To do this we check if the event's type is either MET_MOVE_X or MET_MOVE_Y. There are more event types which could be of interest:

  • MET_MOVE_Z - responsible for mouse wheel
  • MET_BUTTON_LEFT
  • MET_BUTTON_RIGHT
  • MET_BUTTON_MIDDLE
  • MET_BUTTON_ADDITIONAL_1
  • MET_BUTTON_ADDITIONAL_2
  • MET_BUTTON_ADDITIONAL_3
  • MET_BUTTON_ADDITIONAL_4
  • MET_BUTTON_ADDITIONAL_5

Button-like types tell us if a specific button has been pressed, for example. If a movement has been made, we increase a specific angle by value of movement length. We then set the camera's rotation. Note that we're using quaternions in this example. The theory behind them is just too complex to cover it here so be sure to read about them.

The keyboard events handling is also quite simple:

if(_evt.isKeyPressed(KC_ESCAPE))
{
    m_pApp->closeApplication();
    return;
}
 
CameraFirstPerson* pCamera = (CameraFirstPerson*)Engine::getSingleton().getActiveCamera();
if(pCamera)
{
    Vector3 movement;
    if(_evt.isKeyDown(KC_UP))
        movement.z = 1.0f;
    if(_evt.isKeyDown(KC_DOWN))
        movement.z = -1.0f;
    if(_evt.isKeyDown(KC_LEFT))
        movement.x = -1.0f;
    if(_evt.isKeyDown(KC_RIGHT))
        movement.x = 1.0f;
 
    if(movement.x != 0.0f)
        pCamera->strafe(movement.x);
    if(movement.z != 0.0f)
        pCamera->move(movement.z);
}

At the beginning of the method we close the application if the Escape key has been pressed. To properly release all the resources, be sure to call closeApplication() of the Framework before exiting.
Then we move the camera if at least one of Up, Down, Left or Right arrow keys is down. Note that this time we're not using isPressed() method as it will require the user to tap it all the time to make any visible movement. To move camera (important to remember that we're using first person perspective one) we use strafe() and move() methods. strafe() methods let us strafe, and move() to move in the direction we're currently facing.

Be aware that input handling method presented in this tutorial is not the only one. You can also override Framework::processInput() method and use m_pKeyboard and m_pMouse pointers to retrieve information about input. However, this method is left as an exercise to the programmer. To be honest - it's just less elegant.

This time there is no image as it is literally the same as before. Note one thing. You can go below floor and through sculpture. That's not good. We will address these problems when talking about physics and collision detection in the following tutorials.

Unless otherwise stated, the content of this page is licensed under Creative Commons Attribution-ShareAlike 3.0 License