Let's make
DroidBlaster
more interactive with some graphics and game components.
jni/Types.hpp
and create a new structure Location
to hold entity positions. Also, define a macro to generate a random value in the requested range as follows:#ifndef _PACKT_TYPES_HPP_ #define _PACKT_TYPES_HPP_ ... struct Location { Location(): x(0.0f), y(0.0f) {}; float x; float y; }; #define RAND(pMax) (float(pMax) * float(rand()) / float(RAND_MAX)) #endif
jni/GraphicsManager.hpp
. Define a structure GraphicsElement
, which contains the location and dimensions of the graphical element to display:#ifndef _PACKT_GRAPHICSMANAGER_HPP_ #define _PACKT_GRAPHICSMANAGER_HPP_ #include "Types.hpp" #include <android_native_app_glue.h> struct GraphicsElement { GraphicsElement(int32_t pWidth, int32_t pHeight): location(), width(pWidth), height(pHeight) { } Location location; int32_t width; int32_t height; }; ...
Then, in the same file, define a GraphicsManager
class as follows:
getRenderWidth()
and getRenderHeight()
to return the display sizeregisterElement()
is a GraphicsElement
factory method that tells the manager what element to drawstart()
and update()initialize
the manager and render the screen for each frame respectivelyA few member variables are needed:
mApplication
stores the application context needed to access the display windowmRenderWidth
and mRenderHeight
for the display sizemElements
and mElementCount
for a table of all the elements to draw... class GraphicsManager { public: GraphicsManager(android_app* pApplication); ~GraphicsManager(); int32_t getRenderWidth() { return mRenderWidth; } int32_t getRenderHeight() { return mRenderHeight; } GraphicsElement* registerElement(int32_t pHeight, int32_t pWidth); status start(); status update(); private: android_app* mApplication; int32_t mRenderWidth; int32_t mRenderHeight; GraphicsElement* mElements[1024]; int32_t mElementCount; }; #endif
jni/GraphicsManager.cpp
, starting with the constructor, destructor, and registration methods. They manage the list of GraphicsElement
to update:#include "GraphicsManager.hpp" #include "Log.hpp" GraphicsManager::GraphicsManager(android_app* pApplication) : mApplication(pApplication), mRenderWidth(0), mRenderHeight(0), mElements(), mElementCount(0) { Log::info("Creating GraphicsManager."); } GraphicsManager::~GraphicsManager() { Log::info("Destroying GraphicsManager."); for (int32_t i = 0; i < mElementCount; ++i) { delete mElements[i]; } } GraphicsElement* GraphicsManager::registerElement(int32_t pHeight, int32_t pWidth) { mElements[mElementCount] = new GraphicsElement(pHeight, pWidth); return mElements[mElementCount++]; } ...
start()
method to initialize the manager.First, use the ANativeWindow_setBuffersGeometry()
API method to force the window depth format to 32 bits. The two zeros passed in parameters are the required window width and height. They are ignored unless initialized with a positive value. In such a case, the requested window area defined by width and height is scaled to match the screen size.
Then, retrieve all the necessary window dimensions in an ANativeWindow_Buffer
structure. To fill this structure, the window must be first locked with ANativeWindow_lock()
, and then unlocked with AnativeWindow_unlockAndPost()
once done.
... status GraphicsManager::start() { Log::info("Starting GraphicsManager."); // Forces 32 bits format. ANativeWindow_Buffer windowBuffer; if (ANativeWindow_setBuffersGeometry(mApplication->window, 0, 0, WINDOW_FORMAT_RGBX_8888) < 0) { Log::error("Error while setting buffer geometry."); return STATUS_KO; } // Needs to lock the window buffer to get its properties. if (ANativeWindow_lock(mApplication->window, &windowBuffer, NULL) >= 0) { mRenderWidth = windowBuffer.width; mRenderHeight = windowBuffer.height; ANativeWindow_unlockAndPost(mApplication->window); } else { Log::error("Error while locking window."); return STATUS_KO; } return STATUS_OK; } ...
update()
method, which renders raw graphics each time an application is stepped.The window surface must be locked before any draw operation takes place with AnativeWindow_lock()
. Again, the AnativeWindow_Buffer
structure is filled with window information for width and height, but more importantly, the stride
and bits
pointer.
The stride
gives the distance in "pixels" between two successive pixel lines in the window.
The bits
pointer gives direct access to the window surface, in much the same way as the Bitmap API, as seen in the previous chapter.
With these two pieces of information, any pixel-based operations can be performed natively.
For example, clear the window memory area with 0
to get a black background. A brute-force approach using
memset()
can be applied for that purpose.
... status GraphicsManager::update() { // Locks the window buffer and draws on it. ANativeWindow_Buffer windowBuffer; if (ANativeWindow_lock(mApplication->window, &windowBuffer, NULL) < 0) { Log::error("Error while starting GraphicsManager"); return STATUS_KO; } // Clears the window. memset(windowBuffer.bits, 0, windowBuffer.stride * windowBuffer.height * sizeof(uint32_t*)); ...
GraphicsManager
. Each element is represented as a red square onscreen.... // Renders graphic elements. int32_t maxX = windowBuffer.width - 1; int32_t maxY = windowBuffer.height - 1; for (int32_t i = 0; i < mElementCount; ++i) { GraphicsElement* element = mElements[i]; // Computes coordinates. int32_t leftX = element->location.x - element->width / 2; int32_t rightX = element->location.x + element->width / 2; int32_t leftY = windowBuffer.height - element->location.y - element->height / 2; int32_t rightY = windowBuffer.height - element->location.y + element->height / 2; // Clips coordinates. if (rightX < 0 || leftX > maxX || rightY < 0 || leftY > maxY) continue; if (leftX < 0) leftX = 0; else if (rightX > maxX) rightX = maxX; if (leftY < 0) leftY = 0; else if (rightY > maxY) rightY = maxY; ...
line
variable points to the beginning of the first line of pixels on which the element is drawn. This pointer is computed using the stride (distance between two lines of pixels) and the top Y
coordinate of the element.Then, we can loop over window pixels to draw a red square representing the element. Start from the left X
coordinate to the right X
coordinate of the element, switching from one pixel line to another (that is, on the Y
axis) when the end of each is reached.
... // Draws a rectangle. uint32_t* line = (uint32_t*) (windowBuffer.bits) + (windowBuffer.stride * leftY); for (int iY = leftY; iY <= rightY; iY++) { for (int iX = leftX; iX <= rightX; iX++) { line[iX] = 0X000000FF; // Red color } line = line + windowBuffer.stride; } } ...
Finish drawing operations with ANativeWindow_unlockAndPost()
and pend call to pendANativeWindow_lock()
. These must always be called in pairs:
... // Finshed drawing. ANativeWindow_unlockAndPost(mApplication->window); return STATUS_OK; }
jni/Ship.hpp
that represents our spaceship.We will handle initialization only for now, using initialize()
.
Ship
is created with the factory method registerShip()
.
The GraphicsManager
and the ship GraphicsElement
are needed to initialize the ship properly.
#ifndef _PACKT_SHIP_HPP_ #define _PACKT_SHIP_HPP_ #include "GraphicsManager.hpp" class Ship { public: Ship(android_app* pApplication, GraphicsManager& pGraphicsManager); void registerShip(GraphicsElement* pGraphics); void initialize(); private: GraphicsManager& mGraphicsManager; GraphicsElement* mGraphics; }; #endif
jni/Ship.cpp
. The important part is initialize()
, which positions the ship on the lower quarter of the screen, as shown in the following code:#include "Log.hpp" #include "Ship.hpp" #include "Types.hpp" static const float INITAL_X = 0.5f; static const float INITAL_Y = 0.25f; Ship::Ship(android_app* pApplication, GraphicsManager& pGraphicsManager) : mGraphicsManager(pGraphicsManager), mGraphics(NULL) { } void Ship::registerShip(GraphicsElement* pGraphics) { mGraphics = pGraphics; } void Ship::initialize() { mGraphics->location.x = INITAL_X * mGraphicsManager.getRenderWidth(); mGraphics->location.y = INITAL_Y * mGraphicsManager.getRenderHeight(); }
jni/DroidBlaster.hpp
:... #include "ActivityHandler.hpp" #include "EventLoop.hpp" #include "GraphicsManager.hpp" #include "Ship.hpp" #include "Types.hpp" class DroidBlaster : public ActivityHandler { ... private: ... GraphicsManager mGraphicsManager; EventLoop mEventLoop; Ship mShip; }; #endif
jni/DroidBlaster.cpp
constructor:... static const int32_t SHIP_SIZE = 64; DroidBlaster::DroidBlaster(android_app* pApplication): mGraphicsManager(pApplication), mEventLoop(pApplication, *this), mShip(pApplication, mGraphicsManager) { Log::info("Creating DroidBlaster"); GraphicsElement* shipGraphics = mGraphicsManager.registerElement( SHIP_SIZE, SHIP_SIZE); mShip.registerShip(shipGraphics); } ...
GraphicsManager
and the Ship
component in onActivate()
:... status DroidBlaster::onActivate() { Log::info("Activating DroidBlaster"); if (mGraphicsManager.start() != STATUS_OK) return STATUS_KO; mShip.initialize(); return STATUS_OK; } ...
onStep()
:...
status DroidBlaster::onStep() {
return mGraphicsManager.update();
}
Compile and run DroidBlaster
. The result should be a simple red square representing our spaceship in the first quarter of the screen, as follows:
Graphical feedback is provided through the ANativeWindow
API, which gives native access to the display window. It allows manipulating its surface like a bitmap. Similarly, accessing the window surface requires locking and unlocking both before and after processing.
The
AnativeWindow
API is defined in android/native_window.h
and android/native_window_jni.h
. It provides the following:
ANativeWindow_setBuffersGeometry()
initializes the Pixel format (or Depth format) and size of the window buffer. The possible Pixel formats are:
WINDOW_FORMAT_RGBA_8888
for 32-bit colors per pixel, 8 bits for each of the Red, Green, Blue, and Alpha (for transparency) channels.WINDOW_FORMAT_RGBX_8888
is the same as the previous one, except that the Alpha channel is ignored.WINDOW_FORMAT_RGB_565
for 16-bit colors per pixel (5 bits for Red and Blue, and 6 for the Green channel).If the supplied dimension is 0
, the window size is used. If it is non-zero, then the window buffer is scaled to match window dimensions when displayed onscreen:
int32_t ANativeWindow_setBuffersGeometry(ANativeWindow* window, int32_t width, int32_t height, int32_t format);
ANativeWindow_lock()
must be called before performing any drawing operations:int32_t ANativeWindow_lock(ANativeWindow* window, ANativeWindow_Buffer* outBuffer, ARect* inOutDirtyBounds);
ANativeWindow_unlockAndPost()
releases the window after drawing operations are done and sends it to the display. It must be called in a pair with ANativeWindow_lock()
:int32_t ANativeWindow_unlockAndPost(ANativeWindow* window);
ANativeWindow_acquire()
gets a reference, in the Java way, on the specified window to prevent potential deletion. This might be necessary if you do not have fine control on the surface life cycle:void ANativeWindow_acquire(ANativeWindow* window);
ANativeWindow_fromSurface()
associates the window with the given Java android.view.Surface
. This method automatically acquires a reference to the given surface. It must be released with ANativeWindow_release()
to avoid memory leaks:ANativeWindow* ANativeWindow_fromSurface(JNIEnv* env, jobject surface);
ANativeWindow_release()
removes an acquired reference to allow freeing window resources:void ANativeWindow_release(ANativeWindow* window);
ANativeWindow_lock()
):int32_t ANativeWindow_getWidth(ANativeWindow* window); int32_t ANativeWindow_getHeight(ANativeWindow* window); int32_t ANativeWindow_getFormat(ANativeWindow* window);
We now know how to draw. However, how do we animate what is drawn? A key is needed in order to do this: time.
13.58.121.131