Android Studio, Native Activity, Game Activity, C++, OpenGL ES
Default Android languages are Java and Kotlin. In order to use C++ we'll need so called NativeActivity.
Start Android Studio.
Pick New Project.
Scroll down, select Game Activity (C++) which is a direct descendant of NativeActivity and shares a similar architecture. It's a Jetpack library designed to assist Android games in processing app cycle commands, input events, and text input in the application's C/C++ code.
GameActivity is a relatively new option, available from Android 11. If you don't have 11 (or higher) yet, emulator theoretically should help, although this is not always the case (particularly with ES 3.2), which is why I personally prefer to work with real devices. Or you can just ignore the Android parts until better times when you have a more modern device.
Next.
Project name: Let's call it pa. "p" - for platform, "a" - for android.
Package name: replace "example" by your own name, in my case - com.writingagame.pa.
Save location: replace it by C:\CPP\a999hello\pa
Language: pick Java.
Minimum SDK: Select API 30: Android 11.0 (R). For our Game Activity - quite enough.
Next.
Toolchain: Replace Default by C++14 (for compatibility with upcoming Windows project).
Finish.
Wait a bit while it loads. When ready, we can try to run it (green triangular arrow in the top menu) on a connected real device (with USB debugging enabled) or on an emulator.
For this we will need to enable (if not enabled yet) “USB debugging” in Settings -> Developer options on the device.
If “Developer Options” are not in the list, to enable them go to Settings -> About phone -> Software information and tap on Build number seven times. After that Developer options supposed to be available, so you can go back to Settings -> Developer options and enable USB debugging.
Turn on, unlock your phone and plug it in to your PC with USB cable. Device should ask “Allow USB debugging?” – allow. After that your device should appear as an available option in Android Studio's top menu.
Run the project (green triangular arrow in the top menu). Expected result:
However, this example seems to me somewhat overloaded and misleading. Also, it uses OpenGL ES 3.0 when we target 3.2.
As You can see, there is a bunch of files in cpp folder. Of course, eventually we'll need all these helper classes, but not now, not in this form and not in this place.
So, let's clean it up a bit. Close running app (red square in the top menu).
Open app/cpp/main.cpp and replace code by
#include <jni.h>
#include <game-activity/GameActivity.cpp>
#include <game-text-input/gametextinput.cpp>
#include <EGL/egl.h>
#include <GLES3/gl32.h>
#include <game-activity/native_app_glue/android_native_app_glue.c>
struct android_app* pAndroidApp = NULL;
EGLDisplay androidDisplay = EGL_NO_DISPLAY;
EGLSurface androidSurface = EGL_NO_SURFACE;
EGLContext androidContext = EGL_NO_CONTEXT;
bool bExitApp = false;
int screenSize[2] = {0,0};
void android_init_display() {
// Choose your render attributes
constexpr EGLint attribs[] = {
EGL_RENDERABLE_TYPE, EGL_OPENGL_ES3_BIT,
EGL_SURFACE_TYPE, EGL_WINDOW_BIT,
EGL_BLUE_SIZE, 8,
EGL_GREEN_SIZE, 8,
EGL_RED_SIZE, 8,
EGL_DEPTH_SIZE, 24,
EGL_NONE
};
// The default display is probably what you want on Android
auto display = eglGetDisplay(EGL_DEFAULT_DISPLAY);
eglInitialize(display, nullptr, nullptr);
// figure out how many configs there are
EGLint numConfigs;
eglChooseConfig(display, attribs, nullptr, 0, &numConfigs);
// get the list of configurations
std::unique_ptr<EGLConfig[]> supportedConfigs(new EGLConfig[numConfigs]);
eglChooseConfig(display, attribs, supportedConfigs.get(), numConfigs, &numConfigs);
// Find a config we like.
// Could likely just grab the first if we don't care about anything else in the config.
// Otherwise hook in your own heuristic
auto config = *std::find_if(
supportedConfigs.get(),
supportedConfigs.get() + numConfigs,
[&display](const EGLConfig &config) {
EGLint red, green, blue, depth;
if (eglGetConfigAttrib(display, config, EGL_RED_SIZE, &red)
&& eglGetConfigAttrib(display, config, EGL_GREEN_SIZE, &green)
&& eglGetConfigAttrib(display, config, EGL_BLUE_SIZE, &blue)
&& eglGetConfigAttrib(display, config, EGL_DEPTH_SIZE, &depth)) {
//aout << "Found config with " << red << ", " << green << ", " << blue << ", "
// << depth << std::endl;
return red == 8 && green == 8 && blue == 8 && depth == 24;
}
return false;
});
// create the proper window surface
EGLint format;
eglGetConfigAttrib(display, config, EGL_NATIVE_VISUAL_ID, &format);
EGLSurface surface = eglCreateWindowSurface(display, config, pAndroidApp->window, nullptr);
// Create a GLES 3 context
EGLint contextAttribs[] = {
EGL_CONTEXT_MAJOR_VERSION, 3,
EGL_CONTEXT_MINOR_VERSION, 2,
EGL_NONE};
EGLContext context = eglCreateContext(display, config, nullptr, contextAttribs);
// get some window metrics
auto madeCurrent = eglMakeCurrent(display, surface, surface, context);
if(!madeCurrent) {
;
}
androidDisplay = display;
androidSurface = surface;
androidContext = context;
}
void android_term_display() {
if (androidDisplay != EGL_NO_DISPLAY) {
eglMakeCurrent(androidDisplay, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
if (androidContext != EGL_NO_CONTEXT) {
eglDestroyContext(androidDisplay, androidContext);
androidContext = EGL_NO_CONTEXT;
}
if (androidSurface != EGL_NO_SURFACE) {
eglDestroySurface(androidDisplay, androidSurface);
androidSurface = EGL_NO_SURFACE;
}
eglTerminate(androidDisplay);
androidDisplay = EGL_NO_DISPLAY;
}
}
void updateRenderArea() {
EGLint width,height;
eglQuerySurface(androidDisplay, androidSurface, EGL_WIDTH, &width);
eglQuerySurface(androidDisplay, androidSurface, EGL_HEIGHT, &height);
if (width != screenSize[0] || height != screenSize[1]) {
screenSize[0] = width;
screenSize[1] = height;
glViewport(0, 0, width, height);
}
}
void handle_cmd(android_app *pApp, int32_t cmd) {
switch (cmd) {
case APP_CMD_INIT_WINDOW:
android_init_display();
updateRenderArea();
break;
case APP_CMD_TERM_WINDOW:
android_term_display();
break;
default:
break;
}
}
void handleInput() {
// Process all pending events before running game logic.
int events;
android_poll_source *pSource;
if (ALooper_pollAll(0, nullptr, &events, (void **) &pSource) >= 0)
if (pSource)
pSource->process(pAndroidApp, pSource);
//if no display - wait for it
while (androidDisplay == EGL_NO_DISPLAY)
if (ALooper_pollAll(0, nullptr, &events, (void **) &pSource) >= 0)
if (pSource)
pSource->process(pAndroidApp, pSource);
// handle all queued inputs
for (auto i = 0; i < pAndroidApp->motionEventsCount; i++) {
// cache the current event
auto &motionEvent = pAndroidApp->motionEvents[i];
// cache the current action
auto action = motionEvent.action;
// Find the pointer index, mask and bitshift to turn it into a readable value
auto pointerIndex = (action & AMOTION_EVENT_ACTION_POINTER_INDEX_MASK)
>> AMOTION_EVENT_ACTION_POINTER_INDEX_SHIFT;
//aout << "Pointer " << pointerIndex << ":";
// get the x and y position of this event
auto &pointer = motionEvent.pointers[pointerIndex];
auto x = GameActivityPointerAxes_getX(&pointer);
auto y = GameActivityPointerAxes_getY(&pointer);
//aout << "(" << x << ", " << y << ") ";
// Only consider touchscreen events, like touches
auto actionMasked = action & AINPUT_SOURCE_TOUCHSCREEN;
// determine the kind of event it is
switch (actionMasked) {
case AMOTION_EVENT_ACTION_DOWN:
case AMOTION_EVENT_ACTION_POINTER_DOWN:
//aout << "Pointer Down";
break;
case AMOTION_EVENT_ACTION_UP:
case AMOTION_EVENT_ACTION_POINTER_UP:
//aout << "Pointer Up";
break;
default:
;//aout << "Pointer Move";
}
}
android_app_clear_motion_events(pAndroidApp);
// handle key inputs
for (auto i = 0; i < pAndroidApp->keyUpEventsCount; i++) {
// cache the current event
auto &keyEvent = pAndroidApp->keyUpEvents[i];
if (keyEvent.keyCode == AKEYCODE_BACK) {
// actions on back key
bExitApp = true;
}
}
android_app_clear_key_up_events(pAndroidApp);
}
/*!
* This the main entry point for a native activity
*/
float g=0;
void android_main(struct android_app *pApp) {
pAndroidApp = pApp;
// register an event handler for Android events
pApp->onAppCmd = handle_cmd;
while (!bExitApp){
// Process game input
handleInput();
// Render a frame
g+=(2.0/256.0);
if(g>1)
g=0;
glClearColor(0.0, g, 0.0, 1.0);
glClear(GL_COLOR_BUFFER_BIT);
// Present the rendered image. This is an implicit glFlush.
eglSwapBuffers(androidDisplay, androidSurface);
}
android_term_display();
std::terminate();
}
Run (green arrow). Result - green flashing screen:
Stop (red square in the top menu).
Now is safe to remove by-passed classes/files from project's cpp folder (left side pane, right click on the file -> Delete):
- AndroidOut.cpp
- AndroidOut.h
- Model.h
- Renderer.cpp
- Renderer.h
- Shader.cpp
- Shader.h
- TextureAsset.cpp
- TextureAsset.h
- Utility.cpp
- Utility.h
Leave only
- includes
- CMakeLists.txt
- main.cpp
Deleting files from Android Studio's project will delete them from hard drive as well, so don't need to delete them in Windows File Explorer.
Now - remove these files from CMakeLists.
Open cpp/CMakeLists.txt and replace by:
# For more information about using CMake with Android Studio, read the
# documentation: https://d.android.com/studio/projects/add-native-code.html
# Sets the minimum version of CMake required to build the native library.
cmake_minimum_required(VERSION 3.22.1)
# Declares and names the project.
project("pa")
# Creates and names a library, sets it as either STATIC
# or SHARED, and provides the relative paths to its source code.
# You can define multiple libraries, and CMake builds them for you.
# Gradle automatically packages shared libraries with your APK.
add_library( # Sets the name of the library.
pa
# Sets the library as a shared library.
SHARED
# Provides a relative path to your source file(s).
main.cpp)
# Searches for a specified prebuilt library and stores the path as a
# variable. Because CMake includes system libraries in the search path by
# default, you only need to specify the name of the public NDK library
# you want to add. CMake verifies that the library exists before
# completing its build.
find_library( # Sets the name of the path variable.
log-lib
# Specifies the name of the NDK library that
# you want CMake to locate.
log)
# Searches for a package provided by the game activity dependency
find_package(game-activity REQUIRED CONFIG)
# Specifies libraries CMake should link to your target library. You
# can link multiple libraries, such as libraries you define in this
# build script, prebuilt third-party libraries, or system libraries.
target_link_libraries( # Specifies the target library.
pa
android
# The game activity
game-activity::game-activity
# EGL, required for configuring the display context
EGL
# GL ES 3, used for the sample renderer
GLESv3
# for AImageDecoder, to load images from resources
jnigraphics
# Links the target library to the log library
# included in the NDK.
${log-lib})
Also, ok to delete android_robot.png from assets.
Run (green arrow) again, make sure it still works.
Final touch: As was defined by the project settings, the app name on the device is "pa". Let's change it to something more meaningful.
Stop app (red square).
Open app/manifests/AndroidManifest.xml
and change android:label property to let's say "Hello Android":
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="Hello Android"
android:supportsRtl="true"
android:theme="@style/Theme.Pa"
tools:targetApi="31">
<activity
android:name=".MainActivity"
android:screenOrientation="portrait"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<meta-data
android:name="android.app.lib_name"
android:value="pa" />
</activity>
</application>
</manifest>
- I also added property android:screenOrientation="portrait", since GameActivity handles screen-rotate not very gracefully (it restarts main.cpp completely), besides I planned to do so anyway (for the sake of future game layout).
Optional (just in case you ever need):
Another thing I changed here is an applicationId (to com.writingagame.pa_hello).. Changing it in build.gradle or in AndroidManifest or by refactoring a package folder name didn't really work.
Correct procedure is: In the Project explorer (left pane) right-click on app -> Open Module Settings -> Modules -> Default Config. Change Application ID. Then - OK
Run (green arrow) again, perfect.
Just in case: When You open Android Studio next time, it will pre-load this pa project automatically.
To disable pre-load go to top menu -> File -> Settings -> Appearance & Behavior -> System Settings,
uncheck Reopen Projects on Startup.
Apply, Ok.
For now, let's put Android aside and move on to the PC.
Stop app (red square) and close Android Studio.