Time for action – animating graphics with a timer

Let's animate the game.

  1. Create jni/TimeManager.hpp with the time.h manager and define the following methods:
    • reset() to initialize the manager.
    • update() to measure game step duration.
    • elapsed() and elapsedTotal() to get game step duration and game duration. They are going to allow the adaptation of the application behavior to the device speed.
    • now() is a utility method to recompute the current time.

    Define the following member variables:

    • mFirstTime and mLastTime to save a time checkpoint in order to compute elapsed() and elapsedTotal()
    • mElapsed and mElapsedTotal to save computed time measures
      #ifndef _PACKT_TIMEMANAGER_HPP_
      #define _PACKT_TIMEMANAGER_HPP_
      
      #include "Types.hpp"
      
      #include <ctime>
      
      class TimeManager {
      public:
          TimeManager();
      
          void reset();
          void update();
      
          double now();
          float elapsed() { return mElapsed; };
          float elapsedTotal() { return mElapsedTotal; };
      
      private:
          double mFirstTime;
          double mLastTime;
          float mElapsed;
          float mElapsedTotal;
      };
      #endif
  2. Implement jni/TimeManager.cpp. When reset, TimeManager saves the current time computed by the now() method.
    #include "Log.hpp"
    #include "TimeManager.hpp"
    
    #include <cstdlib>
    #include <time.h>
    
    TimeManager::TimeManager():
        mFirstTime(0.0f),
        mLastTime(0.0f),
        mElapsed(0.0f),
        mElapsedTotal(0.0f) {
        srand(time(NULL));
    }
    
    void TimeManager::reset() {
        Log::info("Resetting TimeManager.");
        mElapsed = 0.0f;
        mFirstTime = now();
        mLastTime = mFirstTime;
    }
    ...
  3. Implement update() which checks:
    • elapsed time since last frame in mElapsed
    • elapsed time since the very first frame in mElapsedTotal

      Note

      Note that it is important to work with double types when handling the current time to avoid losing accuracy. Then, the resulting delay can be converted back to float for the elapsed time, since the time difference between the two frames is quite low.

      ...
      void TimeManager::update() {
      	double currentTime = now();
      	mElapsed = (currentTime - mLastTime);
      	mElapsedTotal = (currentTime - mFirstTime);
      	mLastTime = currentTime;
      }
      ...
  4. Compute the current time in the now() method. Use the Posix primitive clock_gettime() to retrieve the current time. A monotonic clock is essential to ensure that the time always goes forward and is not subject to system changes (for example, if the user travels around the world):
    ...
    double TimeManager::now() {
        timespec timeVal;
        clock_gettime(CLOCK_MONOTONIC, &timeVal);
        return timeVal.tv_sec + (timeVal.tv_nsec * 1.0e-9);
    }
  5. Create a new file, jni/PhysicsManager.hpp. Define a structure PhysicsBody to hold asteroid location, dimensions, and velocity:
    #ifndef PACKT_PHYSICSMANAGER_HPP
    #define PACKT_PHYSICSMANAGER_HPP
    
    #include "GraphicsManager.hpp"
    #include "TimeManager.hpp"
    #include "Types.hpp"
    
    struct PhysicsBody {
        PhysicsBody(Location* pLocation, int32_t pWidth, int32_t pHeight):
            location(pLocation),
            width(pWidth), height(pHeight),
            velocityX(0.0f), velocityY(0.0f) {
        }
    
        Location* location;
        int32_t width; int32_t height;
        float velocityX; float velocityY;
    };
    ...
  6. Define a basic PhysicsManager. We need a reference to TimeManager to adapt bodies of movements to time.

    Define a method update() to move asteroids during each game step. The PhysicsManager stores the asteroids to update in mPhysicsBodies and mPhysicsBodyCount:

    ...
    class PhysicsManager {
    public:
        PhysicsManager(TimeManager& pTimeManager,
                GraphicsManager& pGraphicsManager);
        ~PhysicsManager();
    
        PhysicsBody* loadBody(Location& pLocation, int32_t pWidth,
                int32_t pHeight);
        void update();
    
    private:
        TimeManager& mTimeManager;
        GraphicsManager& mGraphicsManager;
    
        PhysicsBody* mPhysicsBodies[1024]; int32_t mPhysicsBodyCount;
    };
    #endif
  7. Implement jni/PhysicsManager.cpp, starting with the constructor, destructor, and registration methods:
    #include "PhysicsManager.hpp"
    #include "Log.hpp"
    
    PhysicsManager::PhysicsManager(TimeManager& pTimeManager,
            GraphicsManager& pGraphicsManager) :
      mTimeManager(pTimeManager), mGraphicsManager(pGraphicsManager),
      mPhysicsBodies(), mPhysicsBodyCount(0) {
        Log::info("Creating PhysicsManager.");
    }
    
    PhysicsManager::~PhysicsManager() {
        Log::info("Destroying PhysicsManager.");
        for (int32_t i = 0; i < mPhysicsBodyCount; ++i) {
            delete mPhysicsBodies[i];
        }
    }
    
    PhysicsBody* PhysicsManager::loadBody(Location& pLocation,
            int32_t pSizeX, int32_t pSizeY) {
        PhysicsBody* body = new PhysicsBody(&pLocation, pSizeX, pSizeY);
        mPhysicsBodies[mPhysicsBodyCount++] = body;
        return body;
    }
    ...
  8. Move asteroids in update() according to their velocity. The computation is performed according to the amount of time between the two game steps:
    ...
    void PhysicsManager::update() {
        float timeStep = mTimeManager.elapsed();
        for (int32_t i = 0; i < mPhysicsBodyCount; ++i) {
            PhysicsBody* body = mPhysicsBodies[i];
            body->location->x += (timeStep * body->velocityX);
            body->location->y += (timeStep * body->velocityY);
        }
    }
  9. Create the jni/Asteroid.hpp component with the following methods:
    • initialize() to set up asteroids with random properties when the game starts
    • update() to detect asteroids that get out of game boundaries
    • spawn() is used by both initialize() and update() to set up one individual asteroid

    We also need the following members:

    • mBodies and mBodyCount to store the list of asteroids to be managed
    • A few integer members to store game boundaries
      #ifndef _PACKT_ASTEROID_HPP_
      #define _PACKT_ASTEROID_HPP_
      
      #include "GraphicsManager.hpp"
      #include "PhysicsManager.hpp"
      #include "TimeManager.hpp"
      #include "Types.hpp"
      
      class Asteroid {
      public:
          Asteroid(android_app* pApplication,
              TimeManager& pTimeManager, GraphicsManager& pGraphicsManager,
              PhysicsManager& pPhysicsManager);
      
          void registerAsteroid(Location& pLocation, int32_t pSizeX,
                  int32_t pSizeY);
      
          void initialize();
          void update();
      
      private:
          void spawn(PhysicsBody* pBody);
      
          TimeManager& mTimeManager;
          GraphicsManager& mGraphicsManager;
          PhysicsManager& mPhysicsManager;
      
          PhysicsBody* mBodies[1024]; int32_t mBodyCount;
          float mMinBound;
          float mUpperBound; float mLowerBound;
          float mLeftBound; float mRightBound;
      };
      #endif
  10. Write the jni/Asteroid.cpp implementation. Start with a few constants, as well as the constructor and registration method, as follows:
    #include "Asteroid.hpp"
    #include "Log.hpp"
    
    static const float BOUNDS_MARGIN = 128;
    static const float MIN_VELOCITY = 150.0f, VELOCITY_RANGE = 600.0f;
    
    Asteroid::Asteroid(android_app* pApplication,
            TimeManager& pTimeManager, GraphicsManager& pGraphicsManager,
            PhysicsManager& pPhysicsManager) :
        mTimeManager(pTimeManager),
        mGraphicsManager(pGraphicsManager),
        mPhysicsManager(pPhysicsManager),
        mBodies(), mBodyCount(0),
        mMinBound(0.0f),
        mUpperBound(0.0f), mLowerBound(0.0f),
        mLeftBound(0.0f), mRightBound(0.0f) {
    }
    
    void Asteroid::registerAsteroid(Location& pLocation,
            int32_t pSizeX, int32_t pSizeY) {
        mBodies[mBodyCount++] = mPhysicsManager.loadBody(pLocation,
                pSizeX, pSizeY);
    }
    ...
  11. Set up boundaries in initialize(). Asteroids are generated above the top of screen (in mMinBound, the maximum boundary mUpperBound is twice the height of the screen). They move from the top to the bottom of the screen. Other boundaries correspond to screen edges padded with a margin (representing twice the size of an asteroid).

    Then, initialize all asteroids using spawn():

    ...
    void Asteroid::initialize() {
        mMinBound = mGraphicsManager.getRenderHeight();
        mUpperBound = mMinBound * 2;
        mLowerBound = -BOUNDS_MARGIN;
        mLeftBound = -BOUNDS_MARGIN;
        mRightBound = (mGraphicsManager.getRenderWidth() + BOUNDS_MARGIN);
    
        for (int32_t i = 0; i < mBodyCount; ++i) {
            spawn(mBodies[i]);
        }
    }
    ...
  12. During each game step, check the asteroids that get out of bounds and reinitialize them:
    ...
    void Asteroid::update() {
        for (int32_t i = 0; i < mBodyCount; ++i) {
            PhysicsBody* body = mBodies[i];
            if ((body->location->x < mLeftBound)
             || (body->location->x > mRightBound)
             || (body->location->y < mLowerBound)
             || (body->location->y > mUpperBound)) {
                spawn(body);
            }
        }
    }
    ...
  13. Finally, initialize each asteroid in spawn(), with velocity and location being generated randomly:
    ...
    void Asteroid::spawn(PhysicsBody* pBody) {
        float velocity = -(RAND(VELOCITY_RANGE) + MIN_VELOCITY);
        float posX = RAND(mGraphicsManager.getRenderWidth());
        float posY = RAND(mGraphicsManager.getRenderHeight())
                      + mGraphicsManager.getRenderHeight();
    
        pBody->velocityX = 0.0f;
        pBody->velocityY = velocity;
        pBody->location->x = posX;
        pBody->location->y = posY;
    }
  14. Add the newly created managers and components to jni/DroidBlaster.hpp:
    #ifndef _PACKT_DROIDBLASTER_HPP_
    #define _PACKT_DROIDBLASTER_HPP_
    
    #include "ActivityHandler.hpp"
    #include "Asteroid.hpp"
    #include "EventLoop.hpp"
    #include "GraphicsManager.hpp"
    #include "PhysicsManager.hpp"
    #include "Ship.hpp"
    #include "TimeManager.hpp"
    #include "Types.hpp"
    
    class DroidBlaster : public ActivityHandler {
        ...
    private:
        TimeManager     mTimeManager;
        GraphicsManager mGraphicsManager;
        PhysicsManager  mPhysicsManager;
        EventLoop mEventLoop;
    
        Asteroid mAsteroids;
        Ship mShip;
    };
    #endif
  15. Register asteroids with GraphicsManager and PhysicsManager in the jni/DroidBlaster.cpp constructor:
    ...
    static const int32_t SHIP_SIZE = 64;
    static const int32_t ASTEROID_COUNT = 16;
    static const int32_t ASTEROID_SIZE = 64;
    
    DroidBlaster::DroidBlaster(android_app* pApplication):
        mTimeManager(),
        mGraphicsManager(pApplication),
        mPhysicsManager(mTimeManager, mGraphicsManager),
        mEventLoop(pApplication, *this),
    
        mAsteroids(pApplication, mTimeManager, mGraphicsManager,
                mPhysicsManager),
        mShip(pApplication, mGraphicsManager) {
        Log::info("Creating DroidBlaster");
    
        GraphicsElement* shipGraphics = mGraphicsManager.registerElement(
                SHIP_SIZE, SHIP_SIZE);
        mShip.registerShip(shipGraphics);
    
        for (int32_t i = 0; i < ASTEROID_COUNT; ++i) {
            GraphicsElement* asteroidGraphics =
                        mGraphicsManager.registerElement(ASTEROID_SIZE,
                                                 ASTEROID_SIZE);
            mAsteroids.registerAsteroid(
                    asteroidGraphics->location, ASTEROID_SIZE,
                    ASTEROID_SIZE);
        }
    }
    ...
  16. Initialize the newly added classes in onActivate() properly:
    ...
    status DroidBlaster::onActivate() {
        Log::info("Activating DroidBlaster");
    
        if (mGraphicsManager.start() != STATUS_OK) return STATUS_KO;
    
        mAsteroids.initialize();
        mShip.initialize();
    
        mTimeManager.reset();
        return STATUS_OK;
    }
    ...
    Finally, update managers and components for each game step:
    ...
    status DroidBlaster::onStep() {
        mTimeManager.update();
        mPhysicsManager.update();
    
        mAsteroids.update();
    
        return mGraphicsManager.update();
    }
    ...

What just happened?

Compile and run the application. This time it should be a bit more animated! Red squares representing asteroids cross the screen at a constant rhythm. The TimeManger helps with setting the pace.

What just happened?

Timers are essential to display animations and movement at the correct speed. They can be implemented with the POSIX method clock_gettime(), which retrieves time with a high precision, theoretically to the nanosecond.

In this tutorial, we used the CLOCK_MONOTONIC flag to set up the timer. A monotonic clock gives the elapsed clock time from an arbitrary starting point in the past. It is unaffected by potential system date change, and thus cannot go back in the past like other options. The downside with CLOCK_MONOTONIC is that it is system-specific and it is not guaranteed to be supported. Hopefully Android supports it, but care should be taken when porting Android code to other platforms. Another point specific to Android to be aware of is that monotonic clocks stop when the system is suspended.

An alternative, that is less precise and affected by changes in the system time (which may or may not be desirable), is gettimeofday(), which is also provided in ctime. The usage is similar but the precision is in microseconds instead of nanoseconds. The following could be a usage example that could replace the current now() implementation in TimeManager:

double TimeManager::now() {
    timeval lTimeVal;
    gettimeofday(&lTimeVal, NULL);
    return (lTimeVal.tv_sec * 1000.0) + (lTimeVal.tv_usec / 1000.0);
}

For more information, have a look at the Man-pages at http://man7.org/linux/man-pages/man2/clock_gettime.2.html.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.149.27.202