Trending
Opinion: How will Project 2025 impact game developers?
The Heritage Foundation's manifesto for the possible next administration could do great harm to many, including large portions of the game development community.
In this reprinted #altdevblogaday in-depth piece, Gameloft 3D programmer Gustavo Samour shows you how to create an app without writing a single line of Java code.
[In this reprinted #altdevblogaday in-depth piece, Gameloft 3D programmer Gustavo Samour continues his series on running native code on Android by showing you how to create an app without writing a single line of Java code.] In the first part of this series, I mentioned a way to call C/C++ code from Java, and vice versa, on the Android platform. That method allows you to run performance-critical native code inside a Java-based Android project. Now I'll show you a way to create an Android app without writing a single line of Java code. The Android NDK revision 5 (December 2010) added support for native activities, so developers can now implement full applications in C/C++. Native code was given access to several subsystems like input, sensors, surfaces, and audio. The drawback, however, is that native activities require API level 9. This means you can only run them on devices with Gingerbread or greater (Android 2.3+). How-to There are actually two ways we can create a native activity. The first is to start with a blank slate, and write all of the code to initialize and update the activity. The other is to use the "glue" code provided in the NDK. This code defines several important enums and callbacks. I don't want to reinvent the wheel right now, so I'll use the second approach. To use the NDK glue code, you'll need to add it to your Android.mk file. You can either use it as a static library, by importing the module from its current location (android-ndk/sources/android/native_app_glue), or you can copy-paste the source files to your project's jni subfolder. Here's my Android.mk file:
LOCAL_PATH := $(call my-dir) include $(CLEAR_VARS) LOCAL_MODULE := NativeActivitySimpleExample LOCAL_SRC_FILES := main.cpp LOCAL_LDLIBS := -llog -landroid -lEGL -lGLESv1_CM LOCAL_STATIC_LIBRARIES := android_native_app_glue include $(BUILD_SHARED_LIBRARY) $(call import-module,android/native_app_glue)
Native Activity Hello World Once we have the makefile, it's time to create a main source file.
#include
#include
#define LOGI(...) ((void)__android_log_print(ANDROID_LOG_INFO,
"NativeActivitySimpleExample", __VA_ARGS__))
void android_main(struct android_app* state)
{
while(1)
{
LOGI("Tick!");
}
}
The native activity's main entry point is "ANativeActivity_onCreate()", which is implemented in the glue code. After initialization, it calls "android_main()", which you can think of as your own entry point. If you build this code, you'll get a nice libNativeActivitySimpleExample.so file. At this point you're probably wondering, how will Android know it should run my native code, instead of looking for a normal Activity class? This is handled inside the AndroidManifest.xml file. As usual, you declare the properties of your activity with an tag. But inside it, we'll pass along some metadata to reference our C/C++ library. Here is what my AndroidManifest.xml looks like. We pass along the name of our native code library via a tag. Keep in mind that the compiled library will have a name like "libNativeActivitySimpleExample.so", but the name to write here is simply "NativeActivitySimpleExample" (lose the "lib" and ".so"). We can now package the app and run it, but if we do, it'll throw a RuntimeException, with a message like "…unable to start activity… unable to load native library…". The problem is the linker decided to strip the glue module, because none of its functions were being called directly in our code. After all, the glue code is made up of callbacks, mostly. The decision was made because the –gc-sections linker flag is enabled automatically in default-build-commands.mk. You can read a brief paragraph about this optimization here. The NDK's way of solving this problem is by adding a direct call to a dummy function declared in native_activity_app_glue.h. If you look at the NDK's native activity sample, you'll notice the following code at the top of "android_main ()":
// Make sure glue isn't stripped.
app_dummy();
This ensures android_native_app_glue.o is linked. If you add these lines, rebuild, and run, you should now see the "Tick!" message appear as log output. We now have a "hello world" app which is free of Java code! Touch Input As long as you leave the previous application alone, it will probably keep running just fine. However, we game developers like to mess with things. So let's try tapping on the screen several times to send input to our app. It may take a while but, at some point in time, the OS will bring up a "app not responding" popup and will ask you to either keep waiting or force close the app. If we look at the log, we'll see lines like these: I/InputReader(XXXX): dispatchTouch::touch event's action is 1, pending(waiting finished signal)=0 I/InputDispatcher(XXXX): Delivering touch to current input target: action: 1, channel 'XXXXXXXX Sorry! (server)' The glue module implements input handling, but we're not using it explicitly in our code. So the app will not respond to touch and the OS will wait for a period of time, eventually asking you to force close. The way to enable input handling is by enabling general event processing. Here's an updated main.cpp:
#include
#include
#define LOGI(...) ((void)__android_log_print(ANDROID_LOG_INFO,
"NativeActivitySimpleExample", __VA_ARGS__))
void android_main(struct android_app* state)
{
// Make sure glue isn't stripped.
app_dummy();
while(1)
{
int ident;
int fdesc;
int events;
struct android_poll_source* source;
while((ident = ALooper_pollAll(0, &fdesc, &events, (void**)&source)) >= 0)
{
// process this event
if (source)
source->process(state, source);
}
}
}
The "ALooper_pollAll()" function waits for events to be available. Its signature is:
int ALooper_pollAll(int timeoutMillis, int* outFd, int* outEvents, void** outData);
The return value is an identifier which, if greater than or equal to 0, means there is an event for us to process. The first argument is an optional timeout which will wait a desired amount of milliseconds for an event to appear. If the value is set to zero, the function returns immediately without blocking. If it is a negative number, it will block until an event is available. In game development, we don't want to wait indefinitely, so let's set it to zero. The second, third, and fourth arguments are the file descriptor, events, and source data. These are all set to NULL if we get a negative identifier. For more information on this function and the android_poll_source structure, check the looper.h and android_native_app_glue.h files in the Android NDK. In the case of a native activity, ALooper_pollAll() can give us two types of events: lifecycle events and input events. To execute your own logic, you'll need to set the appropriate callbacks. The state variable passed into android_main is an android_app instance and has two member function pointers for this purpose: "onAppCmd" and "onInputEvent". Let's set the input callback and do something simple in it.
static int32_t handle_input(struct android_app* app, AInputEvent* event)
{
if (AInputEvent_getType(event) == AINPUT_EVENT_TYPE_MOTION)
{
size_t pointerCount = AMotionEvent_getPointerCount(event);
for (size_t i = 0; i < pointerCount; ++i)
{
LOGI("Received motion event from pointer %zu: (%.2f, %.2f)",
i, AMotionEvent_getX(event, i), AMotionEvent_getY(event, i));
return 1;
}
}
else if (AInputEvent_getType(event) == AINPUT_EVENT_TYPE_KEY)
{
LOGI("Received key event: %d", AKeyEvent_getKeyCode(event));
return 1;
}
return 0;
}
When touching the screen, we should see the touch coordinates in the log output. If a key is pressed, the key code should appear. If we handled the event, we should return 1. Lifecycle Events To execute our own logic for a lifecycle event, such as pause/resume, we need to set the onAppCmd callback:
static void handle_cmd(struct android_app* app, int32_t cmd)
{
switch (cmd)
{
case APP_CMD_SAVE_STATE:
// the OS asked us to save the state of the app
break;
case APP_CMD_INIT_WINDOW:
// get the window ready for showing
break;
case APP_CMD_TERM_WINDOW:
// clean up the window because it is being hidden/closed
break;
case APP_CMD_LOST_FOCUS:
// if the app lost focus, avoid unnecessary processing
(like monitoring the accelerometer)
break;
case APP_CMD_GAINED_FOCUS:
// bring back a certain functionality, like monitoring the accelerometer
break;
}
}
The full list of commands can be found in android_native_app_glue.h. That's it for part 2! In the next post, I want to write about drawing to the screen and getting input from sensors. [This piece was reprinted from #AltDevBlogADay, a shared blog initiative started by @mike_acton devoted to giving game developers of all disciplines a place to motivate each other to write regularly about their personal game development passions.]
You May Also Like