Using Native Code in Your Game

There are many scenarios in which you may need to use native code in your game. Many times, rather simple native libraries can help you work around some issues and accomplish tasks that might not be possible otherwise. If you want to use an existing native API, you can write one-to-one bindings. That is, you can create a native method for each of the API functions so that they can be called from the Java side. The disadvantage is if the API for which you are writing the bindings is platform specific, you will have some work and redesigning to do when and if you wish to port your game to other platforms. For example, writing one-to-one bindings for DirectX may not be a good idea. Of course, if you are developing a game only for the Windows platform, you will be just fine. APIs such as JOGL and JOAL are good examples of one-to-one bindings. Because the APIs are themselves available across platforms, writing direct bindings is a good idea. Many times, a better way to extend the functionality of your Java game is to design a platform-independent layer and use any platform-dependent API underneath that layer. It is important to note that using native libraries has some implications that we will discuss in a later section.

When writing native libraries, even your native code should be separated into platform-dependent and platform-independent files. You can have header files that contain machine-dependent declarations and other headers that conform to your API. You can also abstract APIs to allow for communication between native and Java code at a higher level. For example, Java3D does not have bindings for OpenGL or Direct3D calls and defines its own API that internally uses OpenGL and Direct3D. You can even write a generic interface to call arbitrary functions in arbitrary native libraries. Such an interface would need to take a library name, function name, and parameter list. You can even return the memory address of an object in the system memory to the Java side as an integer and pass it to other native functions. This approach is obviously not very safe. The code samples that follow show examples of direct bindings.

High-Resolution Timer

This example demonstrates a simple binding for retrieving the CPU counter’s frequency as well as its current tick count. By using the frequency, we can convert the value of the counter from ticks to time. The resolution of this timer depends on the speed of the CPU itself. The performance counter is the value of a register that is incremented by the CPU every cycle. The more cycles the CPU can have in a second, the higher the resolution of the timer, which even on slower CPUs is much higher than milliseconds. Unfortunately, the resolution of System.currentTimeMillis() is platform dependent and can have resolution as low as 50 milliseconds on an older Windows platform. Such poor resolution means that measuring small time slices is impossible. For example, if you are doing animation or time-based physics, low time resolution does not allow for smooth and accurate calculations. The QueryPerformanceCounter class shown in the next code segment allows for retrieval of the counter and the frequency of the counter.

 On the CD   The following code can be found on the book’s companion CD-ROM (Chapter 11/High Resolution—Timer folder):

public class QueryPerformanceCounter {     private QueryPerformanceCounter(){} // cannot be instantiated     public static boolean loadNativeLibrary(){         try{             System.loadLibrary("QueryPerformanceCounter");         }catch(UnsatisfiedLinkError e){             e.printStackTrace();             return false;            }         return true;     }     public static native long nativeQueryPerformanceFrequency();     public static native long nativeQueryPerformanceCounter(); }

The QueryPerformanceCounter loads the library through a static method and returns whether the load was successful. For example, if the .dll is not found, the standard timers can be used instead. The class provides bindings for the QueryPerformanceFrequency and QueryPerformanceCounter functions, which are part of WinAPI. The corresponding native functions defined in image from book QueryPerformanceCounter.dll are simple, as can be seen here. The HighResolutionTimer class uses the frequency and the current tick count to compute the time. The QueryPerformanceCounter method is similar to the sun.misc.Perf.getPerf().highResCounter() method. A call to getElapsedTimeInSeconds returns the time since the reset method was last called.

#include "windows.h" #include "QueryPerformanceCounter.h" LARGE_INTEGER   largeInt; JNIEXPORT jlong JNICALL Java_QueryPerformanceCounter_nativeQueryPerformanceFrequency     (JNIEnv *, jclass){     QueryPerformanceFrequency(&largeInt);     return largeInt.QuadPart; } JNIEXPORT jlong JNICALL  Java_QueryPerformanceCounter_nativeQueryPerformanceCounter     (JNIEnv *, jclass){     QueryPerformanceCounter(&largeInt);     return largeInt.QuadPart; }

Listing 11.1  High-resolution timer class.

image from book
public class HighResolutionTimer {     ...         public HighResolutionTimer(){     boolean success =              QueryPerformanceCounter.loadNativeLibrary();                  if (success){             frequency = QueryPerformanceCounter.                 nativeQueryPerformanceFrequency();         }                 if (success && frequency != 0){             highResSupported = true;             System.out.println(" frequency: " + frequency);             frequencyInverse = 1.0/frequency;             reset();             return;         }              System.out.println("====: WARNING: PerformanceCounter not             supported. System time will be used instead");         reset();     }          public void reset(){         if (highResSupported){             counterStart = QueryPerformanceCounter.                 nativeQueryPerformanceCounter();          }else{             timeStart = System.currentTimeMillis();          }     }          public double getElapsedTimeInSeconds(){                 if (highResSupported){             counterEnd = QueryPerformanceCounter.                 nativeQueryPerformanceCounter();                    elapsedTime = (counterEnd - counterStart)*                  frequencyInverse;                    }else{ ... }                    return elapsedTime;     } }
image from book

Joystick

This example demonstrates simple bindings for joystick calls available in the Windows multimedia library. One way to do this would be to simply write native functions that retrieve data, such as X-Axis, one at a time. Here we take a different approach to demonstrate direct buffers. A direct buffer is used to wrap an instance of the JOYINFOEX structure that is passed to the GetJoyPos function. The function writes the joystick data to an instance of JOYINFOEX, and its data is read directly from the Java side. Because the call is part of wmm.lib, the joystick can be calibrated from the control panel, and there is no need to have Direct Input installed. The definition of the JOYINFOEX structure and the bindings for the win32 joystick API are provided in Listing 11.2. Note that the direct buffer that wraps an instance of this structure can be indexed with precomputed indices. The buffer is read as an array of integers because every DWORD corresponds to a 32-bit value.

typedef struct joyinfoex_tag {      DWORD dwSize;               // size of the struct     DWORD dwFlags;              // flags that specify which                                    data should be read,.....     DWORD dwXpos;               // axis 1     DWORD dwYpos;               // axis 2     DWORD dwZpos;               // axis 3     DWORD dwRpos;               // axis 4        DWORD dwUpos;               // axis 5     DWORD dwVpos;               // axis 6     DWORD dwButtons;            // masked button states     DWORD dwButtonNumber;       // number of buttons held down     DWORD dwPOV;                // point of view (8 digital)     DWORD dwReserved1;               DWORD dwReserved2;  } JOYINFOEX; 

 On the CD   The following code can be found on the book’s companion CD-ROM in Code/Chapter 11/JoystickWin32Ex:

Listing 11.2  Bindings for win32 joystick API.

image from book
public class JoystickWin32Ex {     ...      public static final int          FIELD_INDEX_dwSize          = 0,         FIELD_INDEX_dwFlags         = 4,         FIELD_INDEX_dwXpos          = 8,         FIELD_INDEX_dwYpos          = 12,         ...         JOY_BUTTON1         = 0x0001,         JOY_AXISX           = FIELD_INDEX_dwXpos,         JOY_RETURNX         = 0x00000001,         ...     private static ByteBuffer   joyInfoStructs[];          private static void initialize(){         joyInfoStructs = new ByteBuffer[2];              for(int i=0; i<2; i++){             joyInfoStructs[i] =                  (ByteBuffer)nativeWrapJoyInfoStruct(i);                      joyInfoStructs[i].order(ByteOrder.nativeOrder());                      joyInfoStructs[i].putInt(FIELD_INDEX_dwSize,                  joyInfoStructs[i].capacity());             joyInfoStructs[i].putInt(FIELD_INDEX_dwFlags,                 JOY_RETURNALL);             }     }        // joyAxis parameter can be JOY_AXISX, ...     public static          float getAxis(int joyId, int joyAxis){         return normalizeAxisValue(             joyInfoStructs[joyId].getInt(joyAxis));     }     // joyAxis parameter can be JOY_BUTTON1, ...     public static          boolean getButton(int joyId, int joyButton){         return ((joyInfoStructs[joyId].getInt(             FIELD_INDEX_dwButtons) & joyButton) != 0)?true:false;     }     public static void dumpJoyInfoExStruct(int joyId){         System.out.println("---- JOYINFOEX Struct ----");         System.out.println("dwSize:         " +              joyInfoStructs[joyId].             getInt(FIELD_INDEX_dwSize));         System.out.println("dwFlags:        " +              Integer.toBinaryString(joyInfoStructs[joyId].             getInt(FIELD_INDEX_dwFlags)));         System.out.println("dwXpos:         " +              normalizeAxisValue(joyInfoStructs[joyId].             getInt(FIELD_INDEX_dwXpos)));             ...     }     ...     // returns a ByteBuffers mapped to a JOYINFOEX instance     private static native          Object nativeWrapJoyInfoStruct(int joyId);     private static native int          nativePoll(boolean pollBoth); }
image from book

Listing 11.3  The native code for the bindings.

image from book
JOYINFOEX joyInfoExs[2]; JNIEXPORT jobject JNICALL Java_JoystickWin32Ex_nativeWrapJoyInfoStruct(JNIEnv *env, jclass,     jint joyId){         ...     memset(&joyInfoExs[joyId], 0, sizeof(JOYINFOEX));     jobject jDirectByteBuffer =          env->NewDirectByteBuffer(&joyInfoExs[joyId],         sizeof(JOYINFOEX));      return jDirectByteBuffer; } JNIEXPORT jint JNICALL Java_JoystickWin32Ex_nativePoll(JNIEnv    *env, jclass, jboolean pollBoth){      ...     return joyGetPosEx(JOYSTICKID1, &joyInfoExs[0]); }
image from book

Embedding the VM and Using Java as a Scripting Language

 On the CD   The code for this example can be found on the book’s companion CD-ROM in Code/Chapter 11/Scripting Example.

The source for a demo that shows how to use Java as a scripting language has been provided on the CD-ROM. ScriptBase acts as the base class for script files. A sample script file extends the ScriptBase class and creates several units as follows:

class Script1 extends ScriptBase{     void run(){                   print("Script1.run()");             Unit peons[] = new Unit[10];              for(int i=0; i<peons.length; i++){             peons[i] = createUnit(UNIT_TYPE_PEON);         }                  for(int i=0; i<peons.length; i++){              destroyUnit(peons[i]);         }     } }

The demo shows how to launch the VM, register native functions for creating and destroying units with the ScriptBase class, load a script file, and execute it.

Direct Memory and IO Access from Java (UnsafeNativeUtil)

This example demonstrates how to have the same level of control over memory and file IO as you have in C/C++. The main purpose of this example is to show that if you must have as much control as you do in C/C++, you can. Please note that in general, it is not a good practice for a Java program to perform the tasks outlined here, and you should avoid them. In fact, justifying the need to perform some of these operations from Java can be quite a challenge. Having said that, we highly recommend that you look through this example.

The UnsafeNativeUtil class provides bindings for ANSI C functions such as malloc, free, memcpy, memset, fopen, fclose, fread, fwrite, feof, fseek, and ftell. The utility class uses longs to pass around memory addresses, which is typically represented as unsigned longs in the native side. Instead of using a long to represent a file pointer, the library defines the FilePointer class:

class FilePointer{     private long pointer;      private FilePointer(){}; }

Because the class has a single private constructor, objects of type FilePointer cannot be created on the Java side. In addition, the pointer member cannot be accessed, which makes the type an opaque structure to a Java program. Instances of the class can be made only from native code. In fact, the native fopen method is the only function that can create a file pointer object and return it to a Java program. UnsafeNativeUtil is a simple class that contains several native methods, which are listed here.

 On the CD   The code for this example can be found on the book’s companion CD-ROM in Code/Chapter 11/ UnsafeNativeUtil:

//---- memory bindings ---- long malloc(int size); void free(long pointer); byte read(long pointer);   void write(long pointer, byte value);  void memcpy(long dest, long src, int count); void memset(long dest, int value, int count); //---- ByteBuffer ---- ByteBuffer    wrapbuf(long pointer, long size); long          getbuf(ByteBuffer byteBuffer); //---- file bindings ---- FilePointer fopen(String filename, String mode);   int fclose(FilePointer stream); int fread(long buffer, int size, int count, FilePointer stream); int fwrite(long buffer, int size, int count, FilePointer stream); int feof(FilePointer stream);  int fseek(FilePointer stream, int offset, int origin);  int ftell(FilePointer stream);

For details about all but read, write, wrapbuf, and getbuf, refer to the corresponding C function documentation. The read method reads the value of a byte at the provided memory address, and the write method writes the provided byte to the specified memory address. They can be used to access any byte of the system memory. The wrapbuf method uses the JNI NewDirectByteBuffer to create a ByteBuffer that wraps the memory region passed to the method. The getbuf method uses the JNI GetDirectBufferAddress to return the address of a direct ByteBuffer’s buffer.

The following Java segments are from a class provided on the CD-ROM that demonstrates how to use the UnsafeNativeUtil to duplicate a 24-bit bitmap file and modify its content directly as well as through direct ByteBuffers. This segment shows how to duplicate a file. As you might expect, this segment of Java code looks a lot like a segment of C code.

FilePointer fileInput = UnsafeNativeUtil.fopen("In.bmp", "rb"); FilePointer fileOutput = UnsafeNativeUtil.fopen("Out.bmp", "wb"); // compute file size int bufferSize = 0;          UnsafeNativeUtil.fseek(fileInput, 0, UnsafeNativeUtil.SEEK_END); bufferSize = UnsafeNativeUtil.ftell(fileInput); UnsafeNativeUtil.fseek(fileInput, 0, UnsafeNativeUtil.SEEK_SET); // allocate memory and set its content to 0 long buffer = UnsafeNativeUtil.malloc(bufferSize); UnsafeNativeUtil.memset(buffer, (byte)0, bufferSize); // read from input file into buffer, then write buffer to out file UnsafeNativeUtil.fread(buffer, 1, bufferSize, fileInput); UnsafeNativeUtil.fwrite(buffer, 1, bufferSize, fileOutput); UnsafeNativeUtil.fclose(fileInput); UnsafeNativeUtil.fclose(fileOutput);        

The following segment uses the wrapbuf method to create a ByteBuffer that wraps the memory region starting at buffer and ending at buffer + bufferSize. It then traverses the middle third of the buffer and modifies its bytes. Bytes that represent 255 (a component of the white color) are converted to 192 (gray component), and any black bytes (value of 0) are converted to white. Note that the segment uses both the put and get methods of the ByteBuffer as well as direct memory access through read/write.

ByteBuffer bBuffer = UnsafeNativeUtil.wrapbuf(buffer, bufferSize) for(int i=bBuffer.capacity()/3; i<bBuffer.capacity()*2/3; i++){     if (bBuffer.get(i) == (byte)255){         bBuffer.put(i, (byte)192);     }else if (UnsafeNativeUtil.read(buffer+i) == (byte)0){         UnsafeNativeUtil.write(buffer+i, (byte)255);     } }

The last section of this example allocates a direct ByteBuffer by calling allocateDirect, and then uses memcpy and memset to copy and set a section of the image back to white, and write out another file. It finally calls free to release the buffer. Note that failing to call free on the memory allocated through malloc will result in memory leaks. The memory allocated through malloc is not managed by the garbage collector. In fact, the garbage collector does not even know about the memory allocated here.

ByteBuffer byteBuffer2 = ByteBuffer.allocateDirect(bufferSize); long byteBuffer2Buffer = UnsafeNativeUtil.getbuf(byteBuffer2); UnsafeNativeUtil.memcpy(byteBuffer2Buffer, buffer,      byteBuffer2.capacity()); UnsafeNativeUtil.memset(byteBuffer2Buffer + bufferSize/3,      (byte)255, bufferSize/3); // write the buffer to a file  FilePointer fileOutput3 =      UnsafeNativeUtil.fopen("Output3.bmp", "wb"); UnsafeNativeUtil.fwrite(byteBuffer2Buffer, 1,      byteBuffer2.capacity(), fileOutput3); UnsafeNativeUtil.fclose(fileOutput3); // free memory that was allocated using malloc UnsafeNativeUtil.free(buffer);



Practical Java Game Programming
Practical Java Game Programming (Charles River Media Game Development)
ISBN: 1584503262
EAN: 2147483647
Year: 2003
Pages: 171

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net