Time for action – displaying raw graphics

Let's make DroidBlaster more interactive with some graphics and game components.

  1. Edit 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
  2. Create a new file, 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 size
    • registerElement() is a GraphicsElement factory method that tells the manager what element to draw
    • start() and update()initialize the manager and render the screen for each frame respectively

    A few member variables are needed:

    • mApplication stores the application context needed to access the display window
    • mRenderWidth and mRenderHeight for the display size
    • mElements 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
  3. Implement 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++];
    }
    ...
  4. Implement the 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;
    }
    ...
  5. Write the 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*));
    ...
    • Once cleared, draw all elements registered with GraphicsManager. Each element is represented as a red square onscreen.
    • First, compute the coordinates (upper-left and bottom-right corners) of the elements to draw.
    • Then, clip their coordinates to avoid drawing outside the window memory area. This operation is rather important as going beyond window limits might result in a segmentation fault:
      ...
          // 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;
      ...
  6. After that, draw each pixel of the element on screen. The 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;
    }
  7. Create a new component 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
  8. Implement 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();
    }
  9. Append the newly created manager and component to 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
  10. Finally, update the 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);
    }
    ...
  11. Initialize 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;
    }
    ...
  12. Finally, update the manager in onStep():
    ...
    status DroidBlaster::onStep() {
        return mGraphicsManager.update();
    }

What just happened?

Compile and run DroidBlaster. The result should be a simple red square representing our spaceship in the first quarter of the screen, as follows:

What just happened?

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);
  • The following methods return the width, height (in pixels), and the format of the window surface. The returned value is negative incase an error occurs. Note that these methods are tricky to use because their behavior is a bit inconsistent. Prior to Android 4, it is preferable to lock the surface once to get reliable information (which is already provided by 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.

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

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