Monday, 13 February 2012

Getting Started with the Programming Assignment Pt 3 Linking Things Together

In the previous post I created a simple SpaceShip class, to contain the state data for the Ship, I also outlined different  methods. In this post I'm going to discuss different ways of associating the classes with each other. In this case we have the SpaceShip class which contains the state data, and the Models class which will contain the actual Mesh to be draw.

I'm going to then highlight three different ways of associating the classes so the SpaceShip class can access the model and draw.

In all these version we will have an instance of the SpaceShip class created in the GLWindow class called m_spaceShip, and we need to pass the global ngl::Camera to the SpaceShip class as well as the instance of the Model class.

Pass By Reference version
In this version we are going to pass into the draw method both the model and the camera, we declare the draw prototype as follows
/// @brief draw the ship
/// @param[in] _model a reference to the model loader
/// @param[in] _cam a reference to the global camera
void draw(
          const Models &_model,
          const ngl::Camera &_cam
         );
You will now note that this method is no longer marked as const, this is because in the loadMatricesToShader method we need to access the m_transform object and grab the current matrix. This will force the transform object to calculate the transforms and mutate itself, and hence is now a non const method.
/// @brief load our matrices to OpenGL shader
/// @param[in] _cam a reference to the camera use to grab the 
/// VP part of the Matrix for drawing
void loadMatricesToShader(
                           const ngl::Camera &_cam
                         );
So the draw method will basically load the Model View Projection matrix to the shader by calling the method above we then need to access the model and call the draw method, this is shown in the following code
void SpaceShip::draw(const Models &_model, const ngl::Camera &_cam)
{
  loadMatricesToShader(_cam);
  _model.draw("spaceship");

}

void SpaceShip::loadMatricesToShader(const ngl::Camera &_cam)
{
  ngl::ShaderLib *shader=ngl::ShaderLib::instance();
  (*shader)["TextureShader"]->use();
  const ngl::Matrix MVP=m_transform.getMatrix()*_cam.getVPMatrix();
  shader->setShaderParamFromMatrix("MVP",MVP);
}
In the loadMatricesToShader method we have already loaded the shaders to the ShaderLib class and we then just call it when required. We can now use the following code in the GLWindow class to draw our SpaceShip
m_spaceShip.draw(m_model,*m_cam);
Which gives us the following
As you can see the orientation is wrong at present but I will fix that later.

Class Global Pointer Version
The next version is going to have two global pointers one for the Camera and one for the Models, these will be set in the spaceship class on construction and then used in the draw methods.

Firstly we need to add them to the SpaceShip class as follows
/// @brief the global camera
const ngl::Camera *m_camera;
/// @brief the global model store
const Models *m_models;
We now need to set these value, the best way to do this is by using the constructor, as we can guarantee that this will always be called.
/// @brief the ctor
/// @param[in] _cam the global camera
/// @param[in] _m the models 
SpaceShip(
           const ngl::Camera *_cam,
           const Models *_m
         );
If we only implement the parameterized constructor above we will force the object to be constructed using these values. In the constructor we just need to copy these values
SpaceShip::SpaceShip(
                      const ngl::Camera *_cam,
                      const Models *_m
                     )
{
  m_camera=_cam;
  m_models=_m;
  m_pos.set(0,0,0);
  m_rotation.set(0,0,0);
  m_numLives=3;
  m_shieldStrength=100;
}
Now the draw and loadMatrices to shader method signatures can change to have no parameters as shown
void SpaceShip::draw()
{
  loadMatricesToShader();
  m_models->draw("spaceship");

}

void SpaceShip::loadMatricesToShader()
{
  ngl::ShaderLib *shader=ngl::ShaderLib::instance();
  (*shader)["TextureShader"]->use();
  const ngl::Matrix MVP=m_transform.getMatrix()*m_camera->getVPMatrix();
  shader->setShaderParamFromMatrix("MVP",MVP);
}
The code to generate the Ship is now different, firstly the SpaceShip class in GLWindow.h must now be a pointer and we need to build the object once we have created our camera and models as shown below
m_cam= new ngl::Camera(From,To,Up,ngl::PERSPECTIVE);
// set the shape using FOV 45 Aspect Ratio based on Width and Height
// The final two are near and far clipping planes of 0.5 and 10
m_cam->setShape(45,(float)720.0/576.0,0.05,350,ngl::PERSPECTIVE);
 

m_model.addModel("bigrock","models/RockBig.obj","textures/rock_texture.bmp");
m_model.addModel("spike","models/RockSpike.obj","textures/rock_texture.bmp");
m_model.addModel("spaceship","models/SpaceShip.obj","textures/spaceship.bmp");


m_spaceShip = new SpaceShip(m_cam,&m_model);
Both these methods work in more or less the same way, not passing the parameters to the draw method does reduce some of the stack overhead with accessing the data however simple timing tests give both methods the same speed of approximately 60FPS
Game State Object
The final method I'm going to describe is going to use a global game object. This object will be a singleton class, it will have public attributes for the camera and the models, which must be set when the class is first used.
The SpaceShip class will then access this when required.
#ifndef __GLOBALGAMEOBJECT_H__
#define __GLOBALGAMEOBJECT_H__

#include "Models.h"
#include <ngl/Camera.h>

/// @file GlobalGameObject.h
/// @brief we use this object to pass around global game data this is a singleton
/// @author Jonathan Macey
/// @version 1.0
/// @date 13/2/12
/// @class Models
/// @brief we only put const global references in this class to pass around data used
/// by the game


class GlobalGameObject
{
  public :
    /// @brief this is a singleton class this get the current instance
    static GlobalGameObject * instance();
    /// @brief our model made public for ease of access
    const Models *m_models;
    /// @brief the global camera made public for ease of access
    const ngl::Camera *m_camera;
  private :
    /// @brief hide away the ctor as this is a singleton
    GlobalGameObject(){;}
    /// @brief hide the copy ctor
    GlobalGameObject(const GlobalGameObject &_g){Q_UNUSED(_g);}
    /// @brief hide the dtor
    ~GlobalGameObject();
    /// @brief hide the assignment operator
    GlobalGameObject operator =(const GlobalGameObject &_r){Q_UNUSED(_r);}
    /// @brief our instance pointer
    static GlobalGameObject *m_instance;
};
#endif
The main C++ code looks like this and is a standard singleton, in this case we don't have to worry about managing the lifetime of the class as it will only contain const pointers and the other classes will manage their own lifetimes
#include "GlobalGameObject.h"

GlobalGameObject* GlobalGameObject::m_instance = 0;// initialize pointer

GlobalGameObject* GlobalGameObject::instance()
{
  if (m_instance == 0)  // is it the first call?
  {
    m_instance = new GlobalGameObject; // create sole instance
  }
  return m_instance; // address of sole instance
}
To use this class we must create it in GLWindow and attach the pointers as shown
GlobalGameObject *game=GlobalGameObject::instance();
game->m_camera=m_cam;
game->m_models=&m_model;
Now in the SpaceShip class we use the following to access the models and the camera
void SpaceShip::draw()
{
  loadMatricesToShader();
  GlobalGameObject *game=GlobalGameObject::instance();
  game->m_models->draw("spaceship");
}

void SpaceShip::loadMatricesToShader()
{
  GlobalGameObject *game=GlobalGameObject::instance();
  ngl::ShaderLib *shader=ngl::ShaderLib::instance();
  (*shader)["TextureShader"]->use();
  const ngl::Matrix MVP=m_transform.getMatrix()*game->m_camera->getVPMatrix();
  shader->setShaderParamFromMatrix("MVP",MVP);
}
As you can see this method breaks some of the OO rules about encapsulation but will require less re-working of the code if we wish to change game elements or add / remove elements to be shared with the rest of the game objects. Another speed test puts this at about the same speed as the others. So there is no clear winner at present for any of the methods. All of the code for the three examples can be found here, for now I think I'm going to use the GlobalGame object method as it will make the development cycle quicker as I only need to add things to the singleton class.

Next how to move the ship

No comments:

Post a Comment