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
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; } ...
update()
which checks:mElapsed
mElapsedTotal
... void TimeManager::update() { double currentTime = now(); mElapsed = (currentTime - mLastTime); mElapsedTotal = (currentTime - mFirstTime); mLastTime = currentTime; } ...
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); }
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; }; ...
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
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; } ...
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); } }
jni/Asteroid.hpp
component with the following methods:initialize()
to set up asteroids with random properties when the game startsupdate()
to detect asteroids that get out of game boundariesspawn()
is used by both initialize()
and update()
to set up one individual asteroidWe also need the following members:
mBodies
and mBodyCount
to store the list of asteroids to be managed#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
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); } ...
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]); } } ...
... 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); } } } ...
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; }
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
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); } } ...
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(); } ...
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.
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.
3.149.27.202