Tuesday, September 10, 2013

Building mobile game engine with Android NDK: 3 - abstracting types and logging



all parts:
1 - installing the tools and running samples
2 - calling C++ library from Java
3 - abstracting types and logging
4 - timing service


 Last time we ended with displaying green screen. We set all the needed things on java side and we created Main.cpp on C++ side. Today we will do things that will prepare our engine for going multiplatform. So, there will not be to much fun but it will pay soon in next parts of this series.


Project structure

 After the last article we ended with jni folder in our project with structure like this:


 When we finish today the structure will look like this:


 So, first go into jni folder and create all new subfolders: src, Engine, Game, System and under System also the Android folder. In future there will be some common system files in System folder. Usually some abstraction of some feature like touch input and in folder with concrete system name there will be implementation of this feature.


Adjusting Main.cpp

 Go to Main.cpp and in the top remove these lines:

#include <jni.h>
#include <errno.h>
#include <stdio.h>
#include <android/sensor.h>
#include <android/asset_manager.h>
#include <android/asset_manager_jni.h>

#include <GLES2/gl2.h>
#include <GLES2/gl2ext.h>

 and replace it with these lines:

//BEGIN_INCLUDE(all)
#include "src/System/system.h"

#undef LOG_TAG
#define LOG_TAG "Main"

using namespace SBC::System;

 The purpose of the LOG_TAG will be clear soon. You can also see that we will structure our engine with namespaces to keep it clean.


Parameters

  As the engine will grow and support more platforms and features, we will need some way how to parametrize it. So in the same folder as Main.cpp resides, create file params.h with the following content:

#ifndef PARAMS_H
#define PARAMS_H

//---------------- TARGET -----------------------------
// define target platforms
#define PLATFORM_ANDROID 1
#define PLATFORM_BADA  2
#define PLATFORM_WIN  3
#define PLATFORM_TIZEN  4
// choose target platform
#define PLATFORM_ID   PLATFORM_ANDROID

//---------------- OPEN GL ----------------------------
// define parameters for other sources
#define SBC_USE_OPENGL
// use values 1 or 2
#define SBC_USE_OPENGL_VERSION 2

//---------------- RESOLUTION -------------------------
// resolution
#define FRAMEBUFFER_WIDTH 1280
#define FRAMEBUFFER_HEIGHT 800

#endif /* PARAMS_H */

 In this file additional parameters will be added in future. From here I am switching various debugging features on and off.
 What we set now is that we say we are using Android (PLATFORM_ID = PLATFORM_ANDROID). We plan to use OpenGL in its version 2.0 and the target resolution for our game is 1280 x 800. The target resolution is not size of your device screen but buffer the OpenGL draws in. It is than scaled to your device screen.

 Now go to System folder and create file _parameters.h in it. The only purpose is to suck content of params.h into engine. As the params.h can be different for different platforms and we do not want to change or set up anything in System folder in future we are doing it in this way. The file looks like this:

#ifndef PARAMETERS_H_
#define PARAMETERS_H_

#include "params.h"

#endif /* PARAMETERS_H_ */


Abstract system

 In System folder create file system.h. If you later in your engine or game will need some system features like logging you will call it every time in the same way regardless of the actual system. Put these lines into the file:

#ifndef SYSTEM_H_
#define SYSTEM_H_

#include "_parameters.h"

/*
 *  define some common constants, typedefs, etc.
 */

#include "Types.h"
#include "Log.h"

// Android platform
#if (PLATFORM_ID == PLATFORM_ANDROID)
 // system
 #include "Android/AndroidSystem.h"

// Bada platform
#elif (PLATFORM_ID == PLATFORM_BADA)
 // system
 #include "bada/badaSystem.h"

// Win platform
#elif (PLATFORM_ID == PLATFORM_WIN)
 // system
 #include "Win/WinSystem.h"

// Tizen platform
#elif (PLATFORM_ID == PLATFORM_TIZEN)
 // system
 #include "Tizen/TizenSystem.h"
#endif

#endif /* SYSTEM_H_ */

 As you can see, while we are working with Android today I left also other systems there. In Eclipse it will be grayed out to mark that preprocessor is skipping it.

 Next create Log.h and Types.h that have similar structure.

 Log.h:
#ifndef LOG_H_
#define LOG_H_

#include "_parameters.h"

/*
 * define platform specific things
 */

// Android platform
#if (PLATFORM_ID == PLATFORM_ANDROID)
 // basic system
 #include "Android/AndroidLog.h"

// Bada platform
#elif (PLATFORM_ID == PLATFORM_BADA)
 #include "bada/badaLog.h"

#elif (PLATFORM_ID == PLATFORM_WIN)
 // basic system
 #include "Win/WinLog.h"

#elif (PLATFORM_ID == PLATFORM_TIZEN)
 // basic system
 #include "Tizen/TizenLog.h"

#endif

#endif /* LOG_H_ */

Types.h:
#ifndef TYPES_H_
#define TYPES_H_

#include "_parameters.h"

// Android platform
#if (PLATFORM_ID == PLATFORM_ANDROID)
 // types
 #include "Android/AndroidTypes.h"

// Bada platform
#elif (PLATFORM_ID == PLATFORM_BADA)
 // types
 #include "bada/badaTypes.h"

// Win platform
#elif (PLATFORM_ID == PLATFORM_WIN)
 // types
 #include "Win/WinTypes.h"

// Tizen platform
#elif (PLATFORM_ID == PLATFORM_TIZEN)
 // types
 #include "Tizen/TizenTypes.h"

#endif

#endif /* TYPES_H_ */

 You see that its purpose is again just to load concrete implementation from selected system folder. Now we can go into Android folder and implement it.


Android implementation

 We have selected Android in parameters and our engine now awaits concrete implementation for system, logging and types. Let's make it happy.

 First create file AndroidTypes.h in Android folder and fill it like this:

#ifndef ANDROIDTYPES_H_
#define ANDROIDTYPES_H_

#include "../_parameters.h"

#if (PLATFORM_ID == PLATFORM_ANDROID)

// typedefs for safe datatypes
typedef unsigned char   u8;
typedef signed char   s8;
typedef char     c8;
typedef unsigned short  u16;
typedef signed short  s16;
typedef unsigned int  u32;
typedef signed int  s32;
typedef float   f32;
typedef double   f64;

#endif // PLATFORM_ANDROID

#endif /* ANDROIDTYPES_H_ */

 You see that we abstracted C++ types with our own. In the rest of the engine we will use s32 instead of int or u16 instead of unsigned short. In this way we can construct 32 signed integer needed by engine from int on most of today platforms but in case int was only 16bits on some platform we can construct it from long which will probably be 32bit. But our engine will use s32 and will not be interested what is behind.

 Now to logging. It is nice that Android has __android_log_print function in log.h file. But again it is not general enough for multiplatform engine. So, we will abstract it in file AndroidLog.h like this:

#ifndef ANDROIDLOG_H_
#define ANDROIDLOG_H_

#include "../_parameters.h"

#if (PLATFORM_ID == PLATFORM_ANDROID)
#include <android/log.h>

// logging functions with fixed tag
#define  LOGD(...)  __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)
#define  LOGI(...)  __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
#define  LOGW(...)  __android_log_print(ANDROID_LOG_WARN, LOG_TAG, __VA_ARGS__)
#define  LOGE(...)  __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)

#endif

#endif /* ANDROIDLOG_H_ */

 We defined four macros and in the rest of the engine we will use it just like: LOGD("Hello");
 Here comes into play the LOG_TAG we defined in the top of the Main.cpp. We will define it in every source file so logging will always say us where we used it.

 Finally create file AndroidSystem.h with this content:

#ifndef ANDROIDSYSTEM_H_
#define ANDROIDSYSTEM_H_

#include "../_parameters.h"

#if (PLATFORM_ID == PLATFORM_ANDROID)

#include <jni.h>
#include <errno.h>
#include <stdio.h>
#include <android/sensor.h>
#include <android/asset_manager.h>
#include <android/asset_manager_jni.h>

// include openGL headers
#ifdef SBC_USE_OPENGL
 #include <EGL/egl.h>

 #if (SBC_USE_OPENGL_VERSION == 1)
  #include <GLES/gl.h>
 #elif (SBC_USE_OPENGL_VERSION == 2)
  #include <GLES2/gl2.h>
  #include <GLES2/gl2ext.h>
 #endif
#endif

namespace SBC
{
namespace System
{
typedef struct android_app Application;
} // System
} // SBC

 You can see that we are benefiting from our parameter file params.h where we set OpenGL version 2. Without any further settings it is processed inside system files and the system is parametrized from one place.


Let's try it

 Today there will not be any wow effect when running the engine. But we can at least prove that what we did is working. Go to Main.cpp and add these lines to our native methods:

//------------------------------------------------------------------------
static void engine_start(JNIEnv* aEnv, jobject aObj, jobject aAssetManager)
{
 LOGD("Starting engine ...");
}

//------------------------------------------------------------------------
static void engine_stop(JNIEnv* aEnv, jobject aObj, jboolean aTerminating)
{
 LOGD("Stopping engine ...");
}

 With this you should see log messages in the the LogCat.


Conclusion

 Today we put base for spreading our engine to more platforms in future. Next time we will focus on measuring time.

Download TutGame-03.zip