Chapter 6. 3D Shooters Episode I: Wolfenstein 3D for Android

The next two chapters are my personal favorites and the most exciting of this book. We start by looking at the real thing: Wolfenstein 3D (also referred to as Wolf 3D), the godfather of all 3D shooters. The main goal of this chapter is to show you how easy is to bring Wolf 3D from the PC to the Android device, but also in this chapter, you will learn how to do the following:

  • Maximize code reuse by compiling high-performance native code in a dynamic shared library.

  • Write JNI code to connect Java and C subroutines.

  • Cascade graphics back and forth the native and Java layers.

  • Handle sound requests sent by the native layer with the Android media player.

  • Build the Android project and test in the emulator.

Let's get started.

Gathering Your Tools

Some Java developers simply dismiss other languages, especially procedural languages like C. I believe that you should embrace the elegant object oriented features of Java and the raw power of C and there is nothing you cannot do in the gaming world.

Before we start, you will need to acquire the things explained in this section to make the most of this chapter.

Downloading the Chapter Source Code

My 1.210goal in here is to create a balance between the object-oriented features of the Java language and the power of C. Thus I have provided not only the Java source but the C code, neatly organized in this chapter source code. You should import the project to workspace as you move along this chapter. Some of the listings have been stripped for simplicity. To import the chapter source to your Eclipse workspace, follow these steps:

  1. From the main menu click File

    Downloading the Chapter Source Code
  2. In the Import dialog, select Existing Projects into Workspace. Click Next.

  3. Navigate to the Chapter source ch06.Wolf3D.SW; optionally check "Copy project into workspace". Click Finish.

The automated build will kick in immediately; make sure there are no errors in the project. Next, try to familiarize yourself with the folder layout, which is divided in the following subfolders:

  • src: This folder contains the Java layer of the game.

  • assests: This folder contains the game files for the shareware version of Wolf 3D compressed in the zip file: wolfsw.zip.

  • libs/armeabi: This folder contains the Wolf 3D native library libwolf_jni.so.

  • native: This folder contains the source of the Wolf 3D C engine. The version I choose is Wolf 3D for the Game Park gaming device (a popular Korean handheld also known as GP32).

  • res: This folder contains Android resources: graphics, sounds, layouts, and strings.

Introducing Wolf 3D

When Wolf 3D came to the game scene of the 1990s, it revolutionized PC gaming forever. It was the first game of its kind: a game with immersive 3D graphics where the player navigates the Nazi underground picking up weapons, opening secret doors, and shooting up rabid dogs and evil Nazis (see Figure 6-1).

Wolf 3D running on the emulator

Figure 6-1. Wolf 3D running on the emulator

Even though the game's environment appears to be 3D, it really is not. In reality, the game uses a technique called ray casting pioneered by John Carmack from id Software (one of the original creators of the game) to simulate a 3D environment. Ray casting is also called 2.5D and provides the geometry to render perspectives and other pseudo-3D elements.

Tip

A nice ray casting tutorial by F. Permad discusses the basics such as drawing textures, floors, ceilings, walls, shading, and more. Plus, it has some neat Java examples. It can be found at http://www.permadi.com/tutorial/raycast/index.html.

According to an article on Wikipedia at http://en.wikipedia.org/wiki/Wolfenstein_3D, Wolf 3D was released by id Software in 1992 to a huge success. It popularized the first-person shooter game for the PC. The source code was later released under a shareware strategy that helped the game to be ported to almost any platform imaginable, including these:

  • Windows and Pocket PC

  • Linux

  • Nintendo Entertainment System (NES) and Super Nintendo (SNES)

  • Atari Jaguar

  • Mac OS

  • Game Boy Advance

  • PlayStation and PSP

  • Xbox 360

  • iPhone and iPod Touch

When I started fiddling with the idea of bringing Wolf 3D to Android, I knew the key of the project would be to find a highly portable version for Linux, because first, Android is built on Linux, and second, high portability will minimize potential compilation and optimization errors that are very common when using different versions of the GNU C compiler. The best two candidates I could find on the web follow:

  • Wolfenstein 3D for GP32: This is a port to the Gamepark32, a Korean handheld device similar to the PSP. It is written in C. Wolf 3D for GP32 is available online at http://sourceforge.net/projects/gp32wolf3d/.

  • Wolf3D S60: This is port of Wolfenstein 3D and Spear of Destiny for Nokia S60 cell phones. Wolf3D S60 is available online at http://sourceforge.net/projects/wolf3d-s60/.

Digging through the source of both projects, I realized that both were almost identical and based on a defunct port for Linux found on the Web. Of these two, the Gamepark32 version seemed the cleanest and easiest to understand, so I decided to use it. If you wish to understand why nobody has taken the time to port this code to Java, take a look at Listing 6-1.

Example 6-1. Total Number of Lines of Source and Headers for Wolf 3D for Gamepark32

gp2xwolf3d>wc -l *.c
   1026 fmopl.c
   1127 id_ca.c
    507 id_us.c
    471 id_vh.c
    416 jni_wolf.c
    355 misc.c
    145 objs.c
    198 sd_comm.c
    166 sd_null.c
    758 sd_oss.c
     16 test.c
    399 vi_comm.c
    273 vi_null.c
    450 vi_sdl.c
    999 wl_act1.c
   2622 wl_act2.c
    801 wl_act3.c
   1483 wl_agent.c
    248 wl_debug.c
   1411 wl_draw.c
   1603 wl_game.c
   1424 wl_inter.c
   2453 wl_main.c
   3626 wl_menu.c
   1373 wl_play.c
   1483 wl_state.c
    731 wl_text.c
  26564 total

gp2xwolf3d>wc -l *.h
    121 audiosod.h
    137 audiowl6.h
    108 fmopl.h
    144 foreign.h
    171 gfxv_sdm.h
    242 gfxv_sod.h
    185 gfxv_wl1.h
    189 gfxv_wl6.h
    203 gfxv_wl6_92.h
     24 gp2xcont.h
     80 id_ca.h
     71 id_heads.h
     39 id_us.h
     60 id_vh.h
     69 misc.h
     47 sd_comm.h
     20 version.h
    176 vi_comm.h
795 wl_act3.h
   1401 wl_def.h
    212 wl_menu.h
   4494 total

Listing 6-1 shows the total number of lines of code (.c files plus .h header files) of Wolf 3D for the Gamepark32. It's close to 30,000 lines of code. It doesn't seem that much for today's standards, but consider this: when I was in school, one of my computer ethics professors said that, statistically, a software developer can deliver ten lines of production-quality code per day. If you do the math, 3,000 days is the time that a single person would take to deliver a high-quality product. Compound this with the fact that that the work must be done pro bono. Wolf 3D falls under the GNU Public License (GPL), which would allow you to sell your Java version; however, the GPL also requires for you to give up the source, which will prevent you from reaping the product of your hard work.

In my quest to bring this game to Android, I even played with the idea of rewriting this code in Java. I found people who attempted a Java and JavaScript port of the game, but I was quickly discouraged after looking at the complexity and size of the source. These are the reasons why a pure Java port of this game is not feasible:

  • Size: The code base is too big and complex (even for such an old game).

  • Licensing: All work must be done for free, which is not good for business.

  • Time: Writing the game will take too long even for a small team of developers.

The only solution with potential time and cost savings is to create a dynamic shared library of the game and cascade messages back and forth between Java and C using JNI. This is what you will learn in this chapter. Let's get started by looking at the game architecture from a high level perspective.

Understanding the Game Architecture

Figure 6-2 5.250shows the workflow of the application. When you start the game the main activity (WolfLauncher) will start. After some preliminary sanity checks, WolfLauncher will start a thread which loads the main subroutine from the dynamic shared object (DSO)—the native library). All interactions with the native library go thru the native interface class (Natives). The DSO loads all game data and runs the main game loop. In each iteration of this loop, two basic pieces of information are sent back to Java thru JNI:

  • An array of integers representing the video buffer for each frame of the game: This array is encoded in Android's format, as a 32 bit integers with ARGB values.

  • An integer representing the index of a sound to be played by the Java audio API: This value gets sent every time there is a sound event, such as firing the gun or opening a door.

Tip

Because there is no native sound implementation, sound events are cascaded back to Java, which will load and play them on the fly. This Java sound implementation slows things down a bit. Unfortunately, because Android doesn't use the standard Linux audio device and mixer, native sound is not possible at the time of this writing. Furthermore, Google doesn't support native development, which makes things tough.

The Wolf3D game architecture

Figure 6-2. The Wolf3D game architecture

Other types of messages sent by the DSO layer include these:

  • Graphics initialization: This message is sent when the video buffer is first created. Message contents include the width and height of the buffer.

  • Miscellaneous text messages: These string messages are sent by the DSO to Java to display information to the user.

The game display is represented by an XML layout, which contains two visual components:

  • An image view: This element displays the video buffer sent back by the DSO after an iteration of the game loop.

  • A controller layout: This layout displays an SNES-style controller (see Figure 6-2), which listens for touch events such as navigation, gun firing, and others. These events are cascaded as scan codes back to the DSO, which process them and updates the game flow accordingly.

Game audio is handled by two Java classes that encapsulate multiple media players. These audio classes receive events from the DSO (thru JNI) when a sound event occurs and play it accordingly. Let's look at the Java classes in more detail.

Understanding the Java Classes for Wolf 3D

We have looked at the high-level game architecture of Wolf 3D for Android. Now, let's look at the classes that constitute the Java side. Before we get started, make sure you have imported the chapter source into your workspace. This will greatly help your understanding of this section. Figure 6-3 shows the layout of the Java files for the game. The most important ones follow:

  • WolfLauncher: This is the game's main activity class, and it is created when the user starts the game. Its job is to display the UI, handle phone events, such as keyboard touch events, and send them to the Natives class. It also spawns the main thread that starts the native game loop.

  • SNESController: This class encapsulates the SNES-style controller used to play the game without a keyboard.

  • ControllerListener: This is an interface implemented by WolfLauncher and provides the means for listening for controller events.

  • AudioClip: This class encapsulates the Android media player and has methods to play, stop, or loop a sound.

  • AudioMananger: This class acts as an intermediary between the main activity and the audio clips. It has logic to load, start, and cache sounds for better performance.

  • SoundNames: This class maps native sounds (represented by a number) to raw sounds resources within the project.

  • Natives: This is a two-way interface with the DSO. It contains all the native functions that will be implemented in the DSO, plus callbacks that will be started when the DSO sends messages back to Java.

Layout of Java Resources

Figure 6-3. Layout of Java Resources

Other utility classes include the following:

  • DialogTool: This class has utility methods to display messages back to the user, launch a browser, and create dialogs.

  • LibraryLoader: This class is used to load the DSO from the main activity.

  • ScanCodes: This class maps android key codes to PC scan codes and ASCII codes.

  • WolfTools: This class has miscellaneous game information such as the name of the DSO, game, and subroutines to install game files.

Creating the Main WolfLauncher Class

The main activity class fires when the user starts the game (see Listing 6-2). This activity is defined by two resources: a layout XML file and the class itself (WolfLauncher.java)

Main Activity Layout

The activity layout defines the main user interface. Wolf 3D has a simple UI: an image view that displays the video buffer sent by the DSO and the controller layout explained in the "Movement Controller" section.

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    >
    <!— GAME IMAGE —>
    <ImageView android:id="@+id/wolf_iv"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:focusableInTouchMode="true"
        android:focusable="true"
        android:layout_centerHorizontal="true"
        android:layout_centerVertical="false"/>
    <!— Controller XML removed —>
</RelativeLayout>

Main Activity Class (WolfLauncher)

Here is where all the meat resides. When this class starts, the method onCreate executes performs several actions. First, it sets the window manager to full screen with:

getWindow().setFlags(
    WindowManager.LayoutParams.FLAG_FULLSCREEN,
    WindowManager.LayoutParams.FLAG_FULLSCREEN);

Next, it sets the content view XML for the application using R.layout.wolf. This represents the application layout XML containing the game image view and the controller components. It also keeps a reference to the image view representing the video buffer (R.id.wolf_iv):

setContentView(R.layout.wolf);
mView = (ImageView) findViewById(R.id.wolf_iv);

It then initializes the movement controller with initController.

Finally, it starts the game using the base folder (the folder that contains the game files), the game ID (Wolf 3D), and a Boolean value indicating if portrait or landscape mode should be used: startGame(mGameDir, WolfTools.GAME_ID, true).

Listing 6-2 shows the first part of WolfLauncher.java with the methods onCreate and intController.

Example 6-2. The First Section of the Wolf 3D Main Activity WolLauncher.java

package game.wolfsw;

public class WolfLauncher extends Activity
implements Natives.EventListener, ControllerListener
{
    private static final String TAG = "Wolf3D";

    public static final Handler mHandler = new Handler();

    private static boolean mGameStarted = false;

    private static Bitmap mBitmap;
    private static ImageView mView;

    // Audio Manager
    private AudioManager mAudioMgr;

    // Sound? (yes by default)
    private boolean mSound = true;

    private String mGameDir = WolfTools.WOLF_FOLDER;

    // Navigation
    public static enum eNavMethod {
        KBD, PANEL
    };

    public static eNavMethod mNavMethod = eNavMethod.KBD;

    private SNESController controller;

    /**
     * Called when the activity is first created.
     */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        // full screen
        getWindow().setFlags(
                WindowManager.LayoutParams.FLAG_FULLSCREEN,
                WindowManager.LayoutParams.FLAG_FULLSCREEN);

        // No title
        requestWindowFeature(Window.FEATURE_NO_TITLE);

        setContentView(R.layout.wolf);

        mView = (ImageView) findViewById(R.id.wolf_iv);

        initController();

        if (mGameStarted) {
            return;
        }
mGameDir += getString(R.string.pkg_name) + File.separator
                + "files" + File.separator;

        if (!mGameStarted)
            DialogTool.Toast(this, "Menu for options");

        // Image size
        setImageSize(320, 200);

        // Start Game
        startGame(mGameDir, WolfTools.GAME_ID, true);
    }

    private void initController() {
        // No controller in landscape
        if (!isPortrait()) {
            return;
        }

        // init controller
        if (controller == null) {
            controller = new SNESController(this);
            controller.setListener(this);
        }

        findViewById(R.id.snes).setVisibility(View.VISIBLE);
        mNavMethod = eNavMethod.PANEL;
    }

    public boolean isPortrait() {
        return getWindowManager().getDefaultDisplay().getOrientation() == 0;
    }

    /**
     * This will set the size of the image view
     *
     * @param w
     * @param h
     */
    private void setImageSize(int w, int h) {
        LayoutParams lp = mView.getLayoutParams();
        lp.width = w;
        lp.height = h;
    }

Other tasks handled by Listing 6-2 include the following:

  • It initializes the movement controller, setting WolfLauncher as the listener for controller events. It makes the controller visible, and sets the navigation method to PANEL. (see the section titled Movement Controller for more details on this class):

    SNESController controller = new SNESController(this);
    controller.setListener(this);
    
    findViewById(R.id.snes).setVisibility(View.VISIBLE);
    mNavMethod = eNavMethod.PANEL;
  • It defines the method isPortrait() to query the orientation of the device:

    public boolean isPortrait() {
      return getWindowManager().getDefaultDisplay().getOrientation() == 0;
    }
  • It defines a method to set the size of the video buffer (using the ImageView reference mView) and its layout parameters:

    private void setImageSize(int w, int h) {
      LayoutParams lp = mView.getLayoutParams();
      lp.width = w;
      lp.height = h;
    }

Creating the Wolf 3D Main Menu

The main menu in Wolf3D is implemented by overriding two methods: onCreateOptionsMenu and onOptionsItemSelected (see Listing 6-3). onCreateOptionsMenu is used to add menu options that will be displayed when the user presses the menu key on the device. Wolf3D has three options:

  • Toggle Screen: This option toggles the screen size between 320 × 320 pixels and 480 × 320 (full screen). It works in landscape mode only.

  • Navigation: This option displays the navigation method dialog. This dialog lets the user select between keyboard navigation and a game pad controller (useful for keyboardless phones).

  • Exit: This option terminates the game.

The next method (onOptionsItemSelected) fires when the user selects an option from the menu. This method receives a MenuItem; using the item's menu ID (obtained by calling item.getItemId()), the method can proceed appropriately. Thus when the menu ID is 0, the screen size is toggled. When it is 1, the navigation method dialog is display. When it is 2, the game is terminated. Figure 6-3 shows the main menu of Wolf 3D plus the navigation method dialog in action.

Example 6-3. The Main Menu for Wolf 3D (from WolfLauncher.java).

/**
 * Menu
 */
@Override
public boolean onCreateOptionsMenu(Menu menu) {
    super.onCreateOptionsMenu(menu);
    menu.add(0, 0, 0, "Toggle Screen").setIcon(R.drawable.view);
    menu.add(0, 1, 1, "Navigation").setIcon(R.drawable.nav);
menu.add(0, 2, 2, "Exit").setIcon(R.drawable.exit);

    return true;
}

/**
 * Menu actions
 */
@Override
public boolean onOptionsItemSelected(MenuItem item) {
    super.onOptionsItemSelected(item);

    switch (item.getItemId()) {
    case 0:
         // Screen size
         LayoutParams lp = mView.getLayoutParams();

         // 0 == prt, 1 = land
         int orien = getWindowManager().getDefaultDisplay()
                 .getOrientation();

         if (orien == 1) {
             if (lp.width < 0 || lp.height < 0) {
                 lp.width = 480;
                 lp.height = 320;
                 return true;
             }

             lp.width = lp.width == 320 ? 480 : 320;
             lp.height = lp.height == 200 ? 320 : 200;
         } else {
             DialogTool.Toast(this, "Gotta be in landscape.");
         }
         return true;

     case 1:
         // navigation method
         DialogTool.showNavMethodDialog(this);
         return true;

     case 2:
         // Exit
         WolfTools.hardExit(0);
         return true;

     }
     return false;
}
Wolf 3D main menu and navigation options dialog

Figure 6-4. Wolf 3D main menu and navigation options dialog

Handling Key and Touch Events

Listing 6-4 shows the way the game handles key and touch events within the main activity.

  • When the user presses a key, onKeyDown first checks if the menu key has been pressed. It ignores the event if so. Otherwise, it translates the Android key code to a PC scan code with ScanCodes.keySymToScancode(keyCode) and sends it to the DSO using the Natives class key press.

  • When the key is released, onKeyUp will be called performing similar steps as before. Menu keys are ignored, and the key code gets translated to a scan code and sent to the DSO as a key release event.

  • When the screen is touched, onTouchEvent will be called. This method will send a CONTROL scan code to the DSO, which indicates the gun should be fired.

Example 6-4. Event Handlers Within the Main Activity

/**
 * Key down
 */
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
    // Ignore MENU
    if (keyCode == KeyEvent.KEYCODE_MENU)
        return false;

    try {
        int sym = ScanCodes.keySymToScancode(keyCode);

        Natives.keyPress(sym);

    } catch (UnsatisfiedLinkError e) {
        System.err.println(e.toString());
}
    return false;
}

/**
 * Key Released
 */
@Override
public boolean onKeyUp(int keyCode, KeyEvent event) {
    // Ignore MENU
    if (keyCode == KeyEvent.KEYCODE_MENU)
        return false;

    try {
        int sym = ScanCodes.keySymToScancode(keyCode);

        Natives.keyRelease(sym);

    } catch (UnsatisfiedLinkError e) {
        System.err.println(e.toString());
    }
    return false;
}

/**
 * Touch event
 */
@Override
public boolean onTouchEvent(MotionEvent event) {
    try {
        if (event.getAction() == MotionEvent.ACTION_DOWN) {
             // Fire on tap R-CTL
             Natives.keyPress(ScanCodes.sc_Control);
        } else if (event.getAction() == MotionEvent.ACTION_UP) {
            Natives.keyRelease(ScanCodes.sc_Control);
        } else if (event.getAction() == MotionEvent.ACTION_MOVE) {
            // Motion event
        }
        return true;
    } catch (UnsatisfiedLinkError e) {
        // Should not happen!
        Log.e(TAG, e.toString());
        return false;
    }
}

Creating the Game Loop

In this section, we look at how the game starts within the main activity (see Listing 6-5). The startGame function performs the following steps:

  • It checks for a valid base folder. This is the folder that contains the game files. By default it points to /data/data/PACKAGE_NAME/files.

  • It installs the game files (if required). Game files for the shareware PC version of Wolf 3D follow:

    • Audio: audiohed.wl1 and audiot.wl1

    • Graphics: config.wl1, gamemaps.wl1, and maphead.wl1

    • VGA: vgadict.wl1, vgagraph.wl1, vgahead.wl1, and vswap.wl1

  • It defines the game arguments: the game label, ID, and base folder. Game IDs are wolf (for the retail version) and wolfsw (for shareware).

  • It loads the native library using the system call System.load(LIBRAY_NAME).

  • It sets up a listener to the natives interface class. This allows the game to receive messages from the native layer.

  • It creates an instance of the AudioManager. This class is in charge of playing background music or sound events received from the native layer.

  • It starts a new thread with the main game loop. The thread is required because the native game loop will block execution.

At this point, the game will start, and native events will begin cascading into the Java layer.

Example 6-5. The Game Loop from the Main Activity

/**
 * Start the main game loop
 * @param baseDir
 * @param game
 * @param portrait
 */
private void startGame(String baseDir, String game, boolean portrait) {
    File dir = new File(baseDir);

    if (!dir.exists()) {
        if (!dir.mkdir()) {
            MessageBox("Invalid game base folder: " + baseDir);
            return;
        }
    }

    // setup game files
    try {
        WolfTools.installGame(this, dir);
    } catch (Exception e) {
        MessageBox("Fatal Error", "Unable to set game files:"
                + e.toString());
return;
    }

    Log.d(TAG, "Start game base dir: " + baseDir + " game=" + game
            + " port:" + portrait);

    // Args
    final String argv[] = { "wolf3d", game, "basedir", baseDir };

    // Load lib
    if (!loadLibrary()) {
        // this should not happen
        return;
    }

    Natives.setListener(this);

    // Audio?
    if (mSound)
        mAudioMgr = AudioManager.getInstance(this);

    new Thread(new Runnable() {
        public void run() {
            mGameStarted = true;
            Natives.WolfMain(argv);
        }
    }).start();
}

Making Native Callbacks

After the startGame function completes, the following events will cascade from the native layer (see Listing 6-6):

  • OnSysError: This event will fire whenever an unrecoverable system error occurs. The receiver should display the incoming message to the user and quit the application.

  • OnImageUpdate: This event will fire every time there is a native video update. Depending on the frame rate, it may occur around 10 to 20 times per second. The incoming arguments include:

    • Android pixels: This is an array of 32-bit integers representing a packed ARGB pixel.

    • Frame X and Y coordinates: These are the left and top coordinates of the video frame.

    • Frame width and height: These are the width and height of the video frame respectively.

  • OnInitGraphics: This event fires once on graphics initialization and will always fire before the first call to OnImageUpdate. It receives the width and height of the video buffer that will be used to create the game bitmap in the Java layer.

  • OnMessage: This event fires whenever there is a text message from the native layer. It is mostly used to check what is going on in the native side.

  • OnStartSound and OnStartMusic: See the next section for details.

Example 6-6. Native Callbacks from Main Activity

/******************************************************
 * Native Events
 ******************************************************/
@Override
public void OnSysError(final String text) {
    mHandler.post(new Runnable() {
        public void run() {
            MessageBox("System Message", text);
        }
    });

    // Wait for the user to read the box
    try {
        Thread.sleep(8000);
    } catch (InterruptedException e) {

    }
    // Ouch !
    WolfTools.hardExit(−1);
}

@Override
public void OnImageUpdate(int[] pixels, int x, int y, int w, int h) {
    mBitmap.setPixels(pixels, 0, w, x, y, w, h);

    mHandler.post(new Runnable() {
        public void run() {
            try {
                mView.setImageBitmap(mBitmap);

            } catch (Throwable e) {
                e.printStackTrace();
            }
        }
    });
}

@Override
public void OnInitGraphics(int w, int h) {
    Log.d(TAG, "OnInitGraphics creating Bitmap of " + w + " by "
+ h);
    mBitmap = Bitmap.createBitmap(w, h, Config.ARGB_8888);
}

@Override
public void OnMessage(String text) {
    System.out.println("** Wolf Message: " + text);
}

/**
 * Load JNI library. Native lib lives in ProjectFolder/libs/armeabi/libwolf_jni.so
 */
private boolean loadLibrary() {
    Log.d(TAG, "Loading JNI librray from " + WolfTools.WOLF_LIB);
    LibraryLoader.load(WolfTools.WOLF_LIB);

    // Listen for Doom events
    Natives.setListener(this);
    return true;
}

void MessageBox(String text) {
    WolfTools.MessageBox(this, getString(R.string.app_name), text);
}

void MessageBox(String title, String text) {
    WolfTools.MessageBox(this, title, text);
}

Creating Sound and Music Handlers

Good sound and background music are important for any computer game. They provide the realistic feeling for a great gaming experience. Listing 6-7 shows a section of the main activity that implements the sound callbacks, which are fired by the native interface class:

  • OnStartSound: This callback fires whenever the native layer requests a sound to be played such as firing a gun. The argument is a sound number that maps to a file name within the project.

  • OnStartMusic: This callback fires when background music is requested by the native interface class. Background music files are large and usually play once, as opposed to sounds that are small and play many times.

The singleton AudioManager handles all sound and music playing using the methods: startSound and startMusic. For more details, see the section "Sound Handler Classes."

Example 6-7. Audio Handlers from the Main Activity

@Override
public void OnStartSound(int idx) {
if (mSound && mAudioMgr == null)
        Log.e(TAG, "Bug: Audio Mgr is NULL but sound is enabled!");

    try {
        if (mSound && mAudioMgr != null)
            mAudioMgr.startSound(idx);

    } catch (Exception e) {
        Log.e(TAG, "OnStartSound: " + e.toString());
    }
}

public void OnStartMusic(int idx) {
    if (mSound && mAudioMgr == null)
        Log.e(TAG, "Bug: Audio Mgr is NULL but sound is enabled!");

    try {
        if (mSound && mAudioMgr != null)
            mAudioMgr.startMusic(this, idx);

    } catch (Exception e) {
        Log.e(TAG, "OnStartSound: " + e.toString());
    }
}

Creating Movement Controller Handlers

Listing 6-8 shows the section of the main activity that handles the movement controller events. The movement controller is an SNES-style display (see Figure 6-4), which provides navigation capabilities for phones without a keyboard. The controller itself consists of a series of drawable resources and controller classes (see "Creating Movement Controller Classes"), which fire the following events whenever the user presses any of its buttons:

  • ControllerDown(int btnCode): This event fires when the user presses any button in the controller. btnCode represents the Android key code defined in the KeyEvent class.

  • ControllerUp(int btnCode): This event fires when the user releases a button in the controller. It will always fire after ControllerDown.

As Listing 6-8 shows, the controller buttons are mapped in the following way:

  • Y: Strafe (or step) left

  • X: Strafe right

  • B: Fire

  • A: Enable running

Also note that the Android key code must be mapped to a PC scan code and sent to the DSO using the native interface class: Natives.sendNativeKeyEvent (event, scan code). For more details on how the controller works see the "Creating the Movement Controller" section.

Example 6-8. Controller Events from the Main Activity

@Override
    public void ControllerDown(int btnCode) {
        switch (btnCode) {
        case KeyEvent.KEYCODE_Y:
            // strafe left
            Natives.sendNativeKeyEvent(Natives.EV_KEYDOWN,
                    ScanCodes.sc_Alt);
            Natives.sendNativeKeyEvent(Natives.EV_KEYDOWN,
                    ScanCodes.sc_LeftArrow);
            break;

        case KeyEvent.KEYCODE_X:
            // strafe right
            Natives.sendNativeKeyEvent(Natives.EV_KEYDOWN,
                    ScanCodes.sc_Alt);
            Natives.sendNativeKeyEvent(Natives.EV_KEYDOWN,
                    ScanCodes.sc_RightArrow);
            break;

        case KeyEvent.KEYCODE_B:
            // Fire
            Natives.sendNativeKeyEvent(Natives.EV_KEYDOWN,
                    ScanCodes.sc_Control);
            break;

        case KeyEvent.KEYCODE_A:
            // Rshift (Run)
            Natives.sendNativeKeyEvent(Natives.EV_KEYDOWN,
                    ScanCodes.sc_RShift);
            break;

        default:
            Natives.sendNativeKeyEvent(Natives.EV_KEYDOWN, ScanCodes
                    .keySymToScancode(btnCode));
            break;
        }
    }

    @Override
    public void ControllerUp(int btnCode) {
        switch (btnCode) {
        case KeyEvent.KEYCODE_Y:
            // strafe left
            Natives.sendNativeKeyEvent(Natives.EV_KEYUP,
                    ScanCodes.sc_Alt);
Natives.sendNativeKeyEvent(Natives.EV_KEYUP,
                    ScanCodes.sc_LeftArrow);
            break;

        case KeyEvent.KEYCODE_X:
            // strafe right
            Natives.sendNativeKeyEvent(Natives.EV_KEYUP,
                    ScanCodes.sc_Alt);
            Natives.sendNativeKeyEvent(Natives.EV_KEYUP,
                    ScanCodes.sc_RightArrow);
            break;

        case KeyEvent.KEYCODE_B:
            // Fire
            Natives.sendNativeKeyEvent(Natives.EV_KEYUP,
                    ScanCodes.sc_Control);
            break;

        case KeyEvent.KEYCODE_A:
            // shift (Run)
            Natives.sendNativeKeyEvent(Natives.EV_KEYUP,
                    ScanCodes.sc_RShift);
            break;

        default:
            Natives.sendNativeKeyEvent(Natives.EV_KEYUP, ScanCodes
                    .keySymToScancode(btnCode));
            break;
        }
    }
}

Creating the Movement Controller

The movement controller is designed to provide navigation and other actions for phones with no keyboard. It is modeled after the popular SNES controller (see Figure 6-5), and provides the following buttons:

  • Navigation buttons: For up, down, left, and right rotation

  • Selection buttons: For the select and start actions

  • Extra buttons for miscellaneous actions: In Wolf 3D, they are mapped as follows: Y to strafe left, X to strafe right, B to fire, and A to enable running.

The game controller

Figure 6-5. The game controller

Controller Layout

The controller UI is defined by an absolute layout XML (see Listing 6-9) within the main layout file (wolf.xml). All buttons are placed on screen using absolute X and Y coordinates, where the upper left corner of the screen represents (0,0). For this reason, this controller will only work in portrait mode (it is too big for landscape at 480×320). Figure 6-4 shows the background image for the controller. On top of this image the following image buttons are placed:

  • Arrows for up, down, left, and right buttons

  • Select and start

  • X, Y, A, and B buttons

Drawable resources are defined using the XML android:background="@drawable/IMAGE_NAME", where IMAGE_NAME must exist in the res folder within the project.

Example 6-9. Movement Controller Layout

<AbsoluteLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/snes"
    android:orientation="vertical"
    android:layout_width="320px"
    android:layout_height="240px"
    android:background="@drawable/snes"
    android:layout_alignParentBottom="true"
    android:layout_alignParentLeft="true"
    android:focusable="false"
    android:focusableInTouchMode="false"
    android:visibility="gone"
    >

    <!-- Arrows -->
    <ImageButton android:id="@+id/btn_up"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
android:background="#00000000"
        android:layout_margin="0px"
        android:src="@drawable/snes_u0"
        android:layout_x="48px" android:layout_y="60px"/>

    <ImageButton android:id="@+id/btn_down"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="#00000000"
        android:layout_margin="0px"
        android:src="@drawable/snes_d0"
        android:layout_x="45px" android:layout_y="128px"/>

    <ImageButton android:id="@+id/btn_left"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="#00000000"
        android:layout_margin="0px"
        android:src="@drawable/snes_l0"
        android:layout_x="6px" android:layout_y="99px"/>

    <ImageButton android:id="@+id/btn_right"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="#00000000"
        android:layout_margin="0px"
        android:src="@drawable/snes_r0"
        android:layout_y="99px" android:layout_x="80px"/>

    <!-- Select/Start -->
    <ImageButton android:id="@+id/btn_select"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="#00000000"
        android:layout_margin="0px"
        android:src="@drawable/snes_select0"
        android:layout_y="205px" android:layout_x="110px"/>

    <ImageButton android:id="@+id/btn_start"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="#00000000"
        android:layout_margin="0px"
        android:src="@drawable/snes_start0"
        android:layout_y="205px" android:layout_x="160px"/>

    <!-- X/Y -->
    <ImageButton android:id="@+id/btn_X"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="#00000000"
android:layout_margin="0px"
        android:src="@drawable/snes_x0"
        android:layout_y="63px" android:layout_x="223px"/>

    <ImageButton android:id="@+id/btn_Y"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="#00000000"
        android:layout_margin="0px"
        android:src="@drawable/snes_y0"
        android:layout_x="174px" android:layout_y="99px"/>

    <!-- A/B -->
    <ImageButton android:id="@+id/btn_A"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="#00000000"
        android:layout_margin="0px"
        android:src="@drawable/snes_a0"
        android:layout_y="99px" android:layout_x="267px"/>

    <ImageButton android:id="@+id/btn_B"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="#00000000"
        android:layout_margin="0px"
        android:src="@drawable/snes_b0"
        android:layout_x="223px" android:layout_y="141px"/>

</AbsoluteLayout>

Controller Class

The controller is constructed by the class SNESController (see Listing 6-10). Some of the key aspects of this class follow:

  • Constructor: The class requires an Android context that contains the layout XML for the controller and is used for initialization.

  • Initialization: In this step, touch events for the buttons are set up by querying the main activity for a button ID and setting a touch listener:

    mView.findViewById(BUTTON_ID).setOnTouchListener(
         new View.OnTouchListener() {
                public boolean onTouch(View v, MotionEvent evt) {
                }
    }
  • Key presses: Depending on the action type, DOWN or UP, a key is sent to the listener using sendEvent(EVENT_TYPE, ANDROID_KEY).

  • Event listener: Any class that wishes to receive controller events must implement the ControllerListener interface. This interface provides the callbacks to handle button presses:

    public interface ControllerListener {
        public void ControllerUp(int btnCode);
        public void ControllerDown(int btnCode);

Example 6-10. The Controller Class SNESController.java.

package game.controller;

// ...

public class SNESController {

    private Activity mView;

    private ControllerListener mListener;

    public SNESController(Context context) {
        mView = (Activity) context;
        init();
    }

    public SNESController(Context context, AttributeSet attrs) {
        mView = (Activity) context;
        init();
    }

    public SNESController(Context context, AttributeSet attrs, int style) {
        mView = (Activity) context;
        init();
    }

    public void setListener(ControllerListener l) {
        mListener = l;
    }

    private void init() {
        setupControls();
    }

    private void setupControls() {
        // up
        mView.findViewById(R.id.btn_up).setOnTouchListener(
            new View.OnTouchListener() {
                @Override
                public boolean onTouch(View v, MotionEvent evt) {
                    final ImageButton b = (ImageButton) v;
int action = evt.getAction();

                    if (action == MotionEvent.ACTION_DOWN) {
                        b.setImageResource(R.drawable.snes_u1);
                        sendEvent(MotionEvent.ACTION_DOWN,
                                KeyEvent.KEYCODE_DPAD_UP);
                    } else if (action == MotionEvent.ACTION_UP) {
                        b.setImageResource(R.drawable.snes_u0);
                        sendEvent(MotionEvent.ACTION_UP,
                                KeyEvent.KEYCODE_DPAD_UP);
                    }
                    return true;
                }
            });

        // down
        mView.findViewById(R.id.btn_down).setOnTouchListener(
            new View.OnTouchListener() {
                @Override
                public boolean onTouch(View v, MotionEvent evt) {
                    final ImageButton b = (ImageButton) v;
                    int action = evt.getAction();

                    if (action == MotionEvent.ACTION_DOWN) {
                        b.setImageResource(R.drawable.snes_d1);
                        sendEvent(MotionEvent.ACTION_DOWN,
                                KeyEvent.KEYCODE_DPAD_DOWN);
                    } else if (action == MotionEvent.ACTION_UP) {
                        b.setImageResource(R.drawable.snes_d0);
                        sendEvent(MotionEvent.ACTION_UP,
                                KeyEvent.KEYCODE_DPAD_DOWN);
                    }
                    return true;
                }
            });
        // left
        mView.findViewById(R.id.btn_left).setOnTouchListener(
            new View.OnTouchListener() {
                @Override
                public boolean onTouch(View v, MotionEvent evt) {
                    int action = evt.getAction();
                    final ImageButton b = (ImageButton) v;

                    if (action == MotionEvent.ACTION_DOWN) {
                        b.setImageResource(R.drawable.snes_l1);
                        sendEvent(MotionEvent.ACTION_DOWN,
                                KeyEvent.KEYCODE_DPAD_LEFT);
                    } else if (action == MotionEvent.ACTION_UP) {
                        b.setImageResource(R.drawable.snes_l0);
                        sendEvent(MotionEvent.ACTION_UP,
                                KeyEvent.KEYCODE_DPAD_LEFT);
}
                    return true;
                }
            });
        // Right
        mView.findViewById(R.id.btn_right).setOnTouchListener(
            new View.OnTouchListener() {
                @Override
                public boolean onTouch(View v, MotionEvent evt) {
                    int action = evt.getAction();
                    final ImageButton b = (ImageButton) v;

                    if (action == MotionEvent.ACTION_DOWN) {
                        b.setImageResource(R.drawable.snes_r1);
                        sendEvent(MotionEvent.ACTION_DOWN,
                                KeyEvent.KEYCODE_DPAD_RIGHT);
                    } else if (action == MotionEvent.ACTION_UP) {
                        b.setImageResource(R.drawable.snes_r0);
                        sendEvent(MotionEvent.ACTION_UP,
                                KeyEvent.KEYCODE_DPAD_RIGHT);
                    }
                    return true;
                }
            });

     // Events for the SELECT, START, X, Y, A, B buttons
     // have been removed for simplicity. See the class SNESController.java
     // for details

    /**
     * Send an event to the {@link ControllerListener}
     * @param state Up (MotionEvent.ACTION_UP) or
     *  down (MotionEvent.ACTION_DOWN)
     * @param btnAndroid{@link KeyEvent}
     */
    private void sendEvent(int state, int btn) {
        if (mListener != null) {
            if (state == MotionEvent.ACTION_UP)
                mListener.ControllerUp(btn);
            else
                mListener.ControllerDown(btn);
        }
    }
}

Listing 6-10 defines touch events for the buttons: UP, DOWN, LEFT, RIFGHT, START, SELECT, X, Y, A, and B. Let's take a closer look at how the controller reacts to user events. The next fragment shows how the select button in the controller is initialized:

// Controller select button
mView.findViewById(R.id.btn_select).setOnTouchListener(
new View.OnTouchListener() {
        @Override
        public boolean onTouch(View v, MotionEvent evt) {
            int action = evt.getAction();
            final ImageButton b = (ImageButton) v;

            // button down
            if (action == MotionEvent.ACTION_DOWN) {
                // change button image
                b.setImageResource(R.drawable.snes_select1);

                // Send key to native layer
                sendEvent(MotionEvent.ACTION_DOWN,
                        KeyEvent.KEYCODE_ENTER);
            }
            // Button up
            else if (action == MotionEvent.ACTION_UP) {
                // switch image
                b.setImageResource(R.drawable.snes_select0);

                sendEvent(MotionEvent.ACTION_UP,
                        KeyEvent.KEYCODE_ENTER);
            }
            return true;
        }
    });

First, the button is extracted from the game layout using its ID by calling mView.findViewById(R. id.btn_select), where btn_select is the ID of the select button described in the game layout XML (wolf.xml). Next, it listens for touch events by calling setOnTouchListener and implementing the touch listener interface. Whenever the user touches a button, the controller will send the Android key code to the listener (by calling onTouch). The listener, in turn, will process the key code and react appropriately. Note that when the user presses the controller, it will receive a MotionEvent.ACTION_DOWN event. When this happens the corresponding button image will be swapped with a pressed image using button.setImageResource(BUTTON_DOWN_IMAGE_RESID). The same goes for the ACTION_UP event. This helps with the lack of sensitivity when using touch interfaces as opposed to a keyboard. Many users complain about this fact. Figure 6-6 shows the image resources used by the controller to implement this mechanism.

Image resources used to define the controller key press and release events

Figure 6-6. Image resources used to define the controller key press and release events

Sound Classes

In a perfect world, Wolf3D will use native sound for increased performance. Unfortunately Android uses a non standard sound library SoniVox EAS (Enhanced Audio System) whereas Wolf3D uses the Linux OSS (Operating System Sound). To make things worse, Google provides no documentation or sample code in the subject. This is a caveat as other smart phones such as the iPhone support a multitude of open sound libraries. Anyway to bypass the limitation, the sound track has been extracted from the original game files and stored in the res/raw folder under the main project. Figure 6-7 shows a snapshot of these files.

Snapshot of the Wolf 3D sound tracks

Figure 6-7. Snapshot of the Wolf 3D sound tracks

The sound architecture can be resumed in the following key steps:

  1. When the native game loop requests a sound, a JNI callback will fire calling the method wolf.jni.Natives.OnStartSound(idx), where idx represents an index or sound ID.

  2. wolf.jni.Natives will delegate the sound request to the listener WolfLauncher which is the main activity of the application.

  3. The main activity will use the classes AudioManager and AudioClip to play the sound by mapping the sound ID to the corresponding raw resource (WAVE file).

  4. The mappings of native sound IDs to Android raw (WAVE) files are defined in the class wolf.audio.SoundNames.

Mapping Sound Names to Raw Sound Resources

The name mapping class SoundNames is the critical component that maps a native sound IDs to an Android raw resource IDs. Wolf 3D uses 86 sounds, which are defined using an array of integers where the array index represents the native sound ID and the array value represents the Android raw resource ID (see Listing 6-11). This class is used by the AudioManager. The native sound IDs were extracted from the native header file audiowl6.h within the native/gp2xwolf3d folder (see the chapter source code for details).

Example 6-11. Native Sound Name Mappings

public class SoundNames {

    public static final int [] Sounds = new int[86];

    /* Native Sound
    typedef enum {
        HITWALLSNDWL6,              // 0
        SELECTWPNSNDWL6,            // 1
        SELECTITEMSNDWL6,           // 2
        HEARTBEATSNDWL6,            // 3
        MOVEGUN2SNDWL6,             // 4
        *** SEE SOUND NAMES CLASS ***
        MISSILEFIRESNDWL6,          // 85
        MISSILEHITSNDWL6,           // 86
        LASTSOUNDWL6
         }
    */

    static {
        //Sounds[0] = R.raw.noway;  // hit wall
        Sounds[1] = R.raw.wpnup;    // sel wpn
        Sounds[2] = R.raw.itemup;   // hit wall
        Sounds[4] = R.raw.wpnup;    // Move gun(weapon up)

        // *** SEE SOUND NAMES CLASS ***

        Sounds[76] = R.raw.podth2;
        Sounds[77] = R.raw.podth3;
        Sounds[78] = R.raw.bgdth2;

    }

    /* Music Indexes
    typedef enum {
        CORNER_MUS,              // 0
        DUNGEON_MUSWL6,          // 1
        WARMARCH_MUS,            // 2
// *** SEE SOUND NAMES CLASS ***

         } musicnamesWL6;
    */

    public static final int [] Music = new int[27];

    static {
        Music[0] = R.raw.mus_emnycrnr;
        Music[1] = R.raw.mus_emnycrnr;
        Music[2] = R.raw.mus_marchwar;
        Music[3] = R.raw.mus_getthem;

        // *** SEE SOUND NAMES CLASS ***

        Music[26] = R.raw.pacman;
    }
}

Besides sounds, Wolf 3D implements background music, which is processed in the same exact way. The difference being that background music is big and plays once, whereas sounds are tiny and can play multiple times during the game. Wolf 3D uses 27 music files defined in the C header audiowl6.h in the native/gp2xwolf3d folder (see the chapter source code for details). With the mappings in place, the AudioManager can do its work.

Creating the Audio Manager

AudioManager is a singleton helper class that manages all audio and performs the following tasks:

  • It starts a sound given its native ID.

  • It starts background music given its native ID.

  • It cashes commonly used sounds for better performance.

AudioManager uses a HashMap for clip caching:

HashMap<String, AudioClip> mSounds = new HashMap<String, AudioClip>();

When a sound request comes along, the method void startSound(int sidx) will be invoked where sidx is the ID of the native sound. Next, the Android resource ID will be extracted using the SoundNames class:

int id = SoundNames.Sounds[sidx];

Finally, the cache table will be queried. If the table contains the sound, it will be played from cache. Otherwise, the sound will be loaded from disk and cached for the next event:

if (mSounds.containsKey(key)) {
// Play from cache
  mSounds.get(key).play();
}
else {
  // load clip from disk
  AudioClip clip = new AudioClip(mContext, id);
  clip.play();

  // cache sound
  mSounds.put(key, clip);
  mClipCount++;
}

Listing 6-12 shows how it is done. Furthermore, AudioManager also implements several methods. First, getInstance() is a singleton method used to provide only one instance of AudioManager.

preloadSounds(Context ctx) preloads commonly used sounds. An application context is required for the inner AudioClip class, which is used to wrap the Android media player. The next fragment shows how this method uses the resource IDs of the most common sounds to load them into memory. The goal of this method is to improve performance by keeping these sounds in memory at all times:

public void preloadSounds(Context ctx) {
    int[] IDS = new int[] { R.raw.doropn, R.raw.dorcls,
            R.raw.pistol, R.raw.wpnup };

    // Preload sound WAVs using their IDs
    Resources res = mContext.getResources();

    for (int i = 0; i < IDS.length; i++) {
        final int id = IDS[i];
        final String key = res.getResourceName(id);

        Log.d(TAG, "PreLoading sound " + key + " ID " + id);
        mSounds.put(key, new AudioClip(ctx, id));

    }
}

Next, startMusic(Context ctx, int midx) starts the background music given a native music ID. Note that there is only one audio clip for music. This means only one music track can be played at a time. An Android context is required for the AudioClip:

public void startMusic(Context ctx, int midx) {
    // Obtain a the raw music ID from the SoundNames mapping class
    int id = SoundNames.Music[midx];

    if (id == 0)
        return;

    try {
        // Check if the RESOURCE exists on disk
        mContext.getResources().getResourceName(id);
} catch (NotFoundException e) {
        System.err.println("Music resID for idx " + midx
                + " no found");
        return;
    }

    // Stop any playing music
    if (music != null) {
        music.stop();
        music.release();
    }

    Log.d(TAG, "Starting music " + id);

    // Play it
    music = new AudioClip(ctx, id);
    music.play();
}

Finally, the instance variable music is used to start playback. Note that music is an instance variable of type AudioClip.

Example 6-12. The Audio Manager for Wolf 3D

package wolf.audio;

/**
 * Audio manager. Plays and caches sounds for Woldf 3D
*/
public class AudioManager {
    static final String TAG = "AudioMgr";

    static private AudioManager am;

    // Game sound (WAVs) cache table
    private volatile HashMap<String, AudioClip> mSounds
        = new HashMap<String, AudioClip>();

    private int MAX_CLIPS = 20;
    private int mClipCount = 0;
    private Context mContext;

    // BG music
    private AudioClip music;

    /**
     * AudioManager singleton getInstance
     */
    static public AudioManager getInstance(Context ctx) {
        if (am == null)
            return new AudioManager(ctx);
return am;
    }

    private AudioManager(Context ctx) {
        mContext = ctx;
        preloadSounds(ctx);
    }

    /**
     * Start a sound by name & volume
     * @param idx Sound idx (see SoundNames for mappings)
     */
    public synchronized void startSound(int sidx) {
        if (sidx == 0)
            return;

        if (sidx < 0 || sidx > SoundNames.Sounds.length) {
            return;
        }

        // The sound key
        int id = SoundNames.Sounds[sidx];
        String key;

        if (id == 0)
            return;

        try {
            key = mContext.getResources().getResourceName(id);
        } catch (NotFoundException e) {
            return;
        }

        if (mSounds.containsKey(key)) {
            // Play from cache
            mSounds.get(key).play();
        } else {
            // load clip from disk
            // If the sound table is full last entry
            if (mClipCount > MAX_CLIPS) {
                // Remove a last key
                int idx = mSounds.size() − 1;

                String k = (String) mSounds.keySet().toArray()[idx];
                AudioClip clip = mSounds.remove(k);
                clip.release();
                clip = null;
                mClipCount—;
            }

            AudioClip clip = new AudioClip(mContext, id);
clip.play();

            mSounds.put(key, clip);
            mClipCount++;
        }
    }

    /**
     * PreLoad the most used sounds into a hash map
     *
     * @param ctx App context
     * @return
     */
    // public void preloadSounds(Context ctx) removed for simplicity

    /**
     * Start background music
     * @param ctx
     * @param midx Music index, See SoundNames for mappings
     */
    // public void startMusic(Context ctx, int midx) Removed for simplicity

}

Creating the Audio Clip

This is the final piece of the puzzle. AudioClip wraps the Android media player and provides methods for (see Listing 6-13):

  • Creating a clip using a resource ID or URI

  • Playing or looping a sound

  • Stopping a sound

  • Setting the volume

AudioClip deals with the nasty idiosyncrasies of the media player such as illegal state exceptions when the resources are not prepared, played, or stopped in the right sequence.

Example 6-13. The Audio Clip Class for Wolf 3D.

package wolf.audio;

import android.content.Context;
import android.media.MediaPlayer;
import android.net.Uri;

public class AudioClip {
    static final String TAG = "AudioClip";
private MediaPlayer mPlayer;
    private String name;

    private boolean mPlaying = false;
    private boolean mLoop = false;

    public AudioClip(Context ctx, int resID) {
        name = ctx.getResources().getResourceName(resID);

        mPlayer = MediaPlayer.create(ctx, resID);
        mPlayer.setOnCompletionListener(
                new MediaPlayer.OnCompletionListener() {
                    @Override
                    public void onCompletion(MediaPlayer mp) {
                        mPlaying = false;
                        if (mLoop) {
                            mp.start();
                        }
                    }

                });
    }

    public AudioClip(Context ctx, Uri uri) {
        name = uri.toString();

        mPlayer = MediaPlayer.create(ctx, uri);
        mPlayer.setOnCompletionListener(
                new MediaPlayer.OnCompletionListener() {
                    @Override
                    public void onCompletion(MediaPlayer mp) {
                        mPlaying = false;
                        if (mLoop) {
                            mp.start();
                        }
                    }

                });
    }

    public synchronized void play() {
        if (mPlaying)
            return;

        if (mPlayer != null) {
            mPlaying = true;
            mPlayer.start();
        }
    }

    public synchronized void play(int vol) {
        if (mPlaying)
return;

        if (mPlayer != null) {
            mPlaying = true;
            mPlayer.setVolume((float) Math.log10(vol)
                   , (float) Math.log(vol));
            mPlayer.start();
        }
    }

    public synchronized void stop() {
        try {
            mLoop = false;
            if (mPlaying) {
                mPlaying = false;
                mPlayer.pause();
            }

        } catch (Exception e) {
            System.err.println("AduioClip::stop " + name + " "
                    + e.toString());
        }
    }

    public synchronized void loop() {
        mLoop = true;
        mPlaying = true;
        mPlayer.start();
    }

    public void release() {
        if (mPlayer != null) {
            mPlayer.release();
            mPlayer = null;
        }
    }

    public String getName() {
        return name;
    }

    /**
     * Set volume
     * @param vol integer between 1-100
     */
    public void setVolume(int vol) {
        if (mPlayer != null) {
            mPlayer.setVolume((float) Math.log10(vol)
                   , (float) Math.log10(vol));
        }
    }
}

Native Interface Class

The native interface class (Natives.java) acts as the glue between the Java and the C code (see Listing 6-14). In Wolf 3D, this class is a two-way pipeline that handles all native access:

  • It describes native methods that will be called from Java code.

  • It implements Java callbacks, that is, java methods that will be called from C.

This class declares the following native methods implemented in C:

  • native int WolfMain(String[] argv): This method starts the main game loop. argv represents an array of arguments that will be sent to the native layer.

  • native int keyPress(int key): This method can be used to send a key pressed event to the native layer. The argument key represents a PC scan code.

  • native int keyRelease(int key): This method can be used to send a key release event where key represents PC scan code.

Tip

Note that PC scan codes are not the same as Android key codes. The utility class wolf.util.ScanCodes is used to convert these codes.

Example 6-14. Native Interface Class Natives.java.

package wolf.jni;

import android.util.Log;

public class Natives {
    public static final String TAG = "Natives";

    public static final int EV_KEYDOWN = 0;
    public static final int EV_KEYUP = 1;
    public static final int EV_MOUSE = 2;

    private static EventListener listener;

    /**
     * Native event listener interface
     */
    public static interface EventListener {
        void OnMessage(String text);
        void OnInitGraphics(int w, int h);
        void OnImageUpdate(int[] pixels, int x, int y, int w, int h);
        void OnSysError(String text);
        void OnStartSound(int idx);
        void OnStartMusic(int idx);
}

    public static void setListener(EventListener l) {
        listener = l;
    }

    // Native Main game sub (takes an array of arguments)
    public static native int WolfMain(String[] argv);

    // Native Key press. It sends a pc scan code
    public static native int keyPress(int key);

     // Native Key release. It sends a  pc scan  code
    public static native int keyRelease(int key);


    /***********************************************************
     * C - Callbacks
     ***********************************************************/
     //  This fires on messages from the C layer
    private static void OnMessage(String text) {
        if (listener != null)
            listener.OnMessage(text);
    }

     // Fires on init graphics: receives the width and height of the video
    private static void OnInitGraphics(int w, int h) {
        if (listener != null)
            listener.OnInitGraphics(w, h);
    }

    // Fires on image update: receives the video RGBA pixels plus x,y
    // coordinates and width and height of the image
    private static void OnImageUpdate(int[] pixels, int x, int y,
            int w, int h) {
        if (listener != null)
            listener.OnImageUpdate(pixels, x, y, w, h);

    }

    //  Fires when the C lib calls exit()
    private static void OnSysError(String message) {
        if (listener != null)
            listener.OnSysError(message
                    + " - Please report this error.");
    }

     // Fires when a sound is requested in the C layer.
     // Receives a sound index
    private static void OnStartSound(int idx) {
        if (listener != null)
listener.OnStartSound(idx);
    }

    // Fires when music is played in the C layer.
    private static void OnStartMusic(int idx) {
        if (listener != null)
            listener.OnStartMusic(idx);
    }

     //  Sends a key event to the native layer
     // type : one of Natives.EV_KEYDOWN or Natives.EV_KEYUP
     // sym: PC scan code
    public static void sendNativeKeyEvent(int type, int sym) {
        try {
            if (type == Natives.EV_KEYDOWN)
                Natives.keyPress(sym);
            else
                Natives.keyRelease(sym);
        } catch (UnsatisfiedLinkError e) {
            Log.e(TAG, e.toString());
        }
    }
}

In order for the game activity (WolfLauncher) to receive native callbacks, such as when a video update occurs or a sound must be played, it must implement the interface Natives. EventListener. This interface provides the critical notifications at the core of the game:

  • OnMessage(String text): This event sends a message from the native layer for information purposes.

  • OnInitGraphics(int w, int h): This event gets called when the video graphics gets initialized. It returns the width and height of the video buffer.

  • OnImageUpdate(int[] pixels, int x, int y, int w, int h): This is perhaps the most important method out there. It gets called when a video update occurs and returns an Android ARGB packed array of pixels to be displayed on the device. It also returns the X and Y coordinates of the top left pixel and the width and height of the video buffer.

  • OnSysError(String text): This event fires when a fatal error occurs. The client should display the incoming message and terminate; otherwise, the application will certainly crash.

  • OnStartSound(int idx): This event fires when a sound must be played. idx represents the native sound ID.

  • OnStartMusic(int idx): This event fires when background music must be played. idx represents the native music ID.

Now, let's look at some of the native code where all the magic occurs.

Coding the Native Layer

Even though Wolf3D is an old game written in the 1980s, it is still big for a mobile device. The C code is roughly 30,000 lines of C code that's very hard to understand. This section will take a look at some tiny changes made to the core to add JNI functionality. You should look at the source code of the chapter when going through this section.

The changes made to the original game for JNI can be summarized as follows:

  • New files:

    • jni_wolf.c: This file contains all JNI native method implementations plus C to Java callbacks

    • jni_wolf.h and wolf_jni_Natives.h. These are the C headers that support jni_wolf.c.

  • Updated files:

    • sd_null.c: This file will be updated to cascade sound and music requests back to Java.

    • vi_null.c: This file will be updated to cascade video buffer events back to Java.

Let's take a look at sections of jni_wolf.c. This is the most important file and must be understood completely. Table 6-1 shows the name mappings of the native Java methods to their corresponding C counterparts:

Table 6-1. Native Methods and Their C Names for Wolf 3D.

JavaC

C

public static native int

wolf.jni.Natives.WolfMain(String[] argv)

JNIEXPORT jint JNICALL Java_wolf_jni_Natives_WolfMain

(JNIEnv * env, jclass cls, jobjectArray jargv)

public static native int

wolf.jni.Natives.keyPress(int key)

JNIEXPORT jint JNICALL Java_wolf_jni_Natives_keyPress

(JNIEnv * env, jclass cls, jint scanCode)

public static native int

wolf.jni.Natives.keyRelease(int key)

JNIEXPORT jint JNICALL Java_wolf_jni_Natives_

keyRelease (JNIEnv * env, jclass cls, jint scanCode)

Consider the native wolf.jni.Natives.WolfMain. Internally, it gets translated to Java_wolf_jni_Natives_WolfMain. This must be done using the javah command (see the section "Compiling the DSO" for details). This function starts the main game loop.

Initializing the Game Loop

This is the most important function in this file. It starts the native game loop and will block execution. For this reason, this function should be called within a Java thread. The critical steps shown in Listing 6-15 follow:

  1. Save a reference to the Java VM is using (*env)->GetJavaVM(env, &g_VM). This reference will be later used within the C to Java callbacks.

  2. Convert the Java string array (jargv) to a C array (char **). The C array will be send to the native game loop. The key aspects of converting a Java string array to a C array follow:

    • Get the length of the java array with (*env)->GetArrayLength(env, jarray).

    • Get the value of the Nth row of the Java array as a Java string with jstring jrow = (jstring)(*env)->GetObjectArrayElement(env, jargv, i).

    • Convert the Java string above (jrow) to a C string: const char *row = (*env)->GetStringUTFChars(env, jrow, 0).

    • Allocate space for the C array for that specific row: args[i] = malloc(strlen(row) + 1).

    • Copy the new C string to the C array: strcpy (args[i], row).

    • Release the java string row: (*env)->ReleaseStringUTFChars(env, jrow, row).

    • Repeat the same process for each element in the Java array.

  3. Load the native interface class wolf.jni.Natives.java: jNativesCls = (*env)-> FindClass(env, "wolf/jni/Natives"). This class will be used by the C to Java native callbacks.

  4. Load the video update callback using its name and signature: jSendImageMethod = (*env)->GetStaticMethodID(env, jNativesCls, "OnImageUpdate", "([IIIII)V").

  5. Load the sound update callback: jStartSoundMethod = (*env)->GetStaticMethodID(env, jNativesCls, "OnStartSound", "(I)V").

  6. Invoke the native game loop sending the C array created previously: wolf_main (clen, args).

At this point, execution will block, and the native video buffer should fill up; plus native events should start coming up. These events must be glued to the Android activity using the C to Java callbacks.

Example 6-15. The Main Game Loop from wolf_jni.c

// Global Java VM
static JavaVM *g_VM;

// Java Native interface class
jclass jNativesCls;
// Video buffer java callback
jmethodID jSendImageMethod;

// Sound callback
jmethodID jStartSoundMethod;

// Java image pixels: int ARGB
jintArray jImage;
int iSize;

// Native game loop
extern int wolf_main(int argc, char **argv);

JNIEXPORT jint JNICALL Java_wolf_jni_Natives_WolfMain
  (JNIEnv * env, jclass cls, jobjectArray jargv)
{
    (*env)->GetJavaVM(env, &g_VM);

    // Extract char ** args from Java array
    jsize clen =  getArrayLen(env, jargv);

    // convert Java String[] arguments to C char **
    char * args[(int)clen];

    int i;
    jstring jrow;
    for (i = 0; i < clen; i++)
    {
        jrow = (jstring)(*env)->GetObjectArrayElement(env, jargv, i);
        const char *row  = (*env)->GetStringUTFChars(env, jrow, 0);

        args[i] = malloc(strlen(row) + 1);
        strcpy (args[i], row);

        // free java string jrow
        (*env)->ReleaseStringUTFChars(env, jrow, row);
    }

    /*
     * Load the Java native interface class wolf.jni.natives
     */
    jNativesCls = (*env)->FindClass(env, "wolf/jni/Natives");

    if (jNativesCls == 0) {
        printf("Unable to find class: wolf/jni/Natives");
            return −1;
    }

    // Load wolf.jni.Natives.OnImageUpdate(int[] video, int x, int y, int w, int h)
    jSendImageMethod = (*env)->GetStaticMethodID(env, jNativesCls
, "OnImageUpdate"
           , "([IIIII)V");

    if (jSendImageMethod == 0) {
        jni_printf("Unable to find method wolf.jni.OnImageUpdate(byte[])");
        return −1;
    }

    // Load OnStartSound(int id)
    jStartSoundMethod = (*env)->GetStaticMethodID(env, jNativesCls
           , "OnStartSound"
           , "(I)V");


    if (jStartSoundMethod == 0) {
        jni_printf("Unable to find method wolf.jni.OnStartSound sig: ([BI)V");
        return −1;
    }
    // Print args
    for (i = 0; i < clen; i++) {
        jni_printf("WolfMain args[%d]=%s", clen, args[i]);
    }

    // Invoke Quake's main sub. This will loop forever
    wolf_main (clen, args);
    return 0;
}

Cascading Messages with C to Java Callbacks

C to Java callbacks are used to cascade native events such as video and sound events back to the Java activity. The callbacks require a JNI implementation plus some tiny changes to the original C files. There are three types of callbacks in Wolf 3D:

  • Graphics initialization: It is the very first callback that should fire to notify the main activity of the size (width and height) of the video buffer. The main activity will in turn use this information to create a bitmap to be displayed on the device.

  • Video buffer update: This callback must fire after graphics initialization and send an Android packed ARGB bitmap back to the main activity for display.

  • Sound and music requests: These are used to tell the main activity that a sound or background music must be played.

Initializing Graphics

Listing 6-15 shows the way graphics are initialized in the game. There are two functions in two different files: jni_init_graphics (int width, int height) in jni_wolf.c and VL_Startup() in vi_null.c.

jni_init_graphics implements the C to Java callback that tells the main activity that graphics are ready. It sends the width and height of the video buffer, and they are used to create a Java bitmap that will be used to display the video on the device. Note that this callback fires outside of the current JNI thread. When the callback fires, you cannot touch the JNI environment directly (JNIEnv *env), or you will cause an invalid thread access error that will crash the application. Instead, you must attach to the current thread with the following call:

(*g_VM)->AttachCurrentThread (g_VM, (void **) &env, NULL)

where env will get a reference to the current thread JNI environment. Furthermore to do this, you must save a global reference to the Java virtual machine (usually from the very first native call):

(*env)->GetJavaVM(env, &g_VM);

Once you have attached to the current thread, you can load the wolf.jni.Natives.OnInitGraphics(width, height) using the method name and signature and execute it with the width and height of the video buffer:

jmethodID mid = (*env)->GetStaticMethodID(
         env, jNativesCls, "OnInitGraphics", "(II)V");
(*env)->CallStaticVoidMethod(env, jNativesCls, mid, width, height);

The second part of the listing (a section of the file vi_null.c) shows where jni_init_graphics is called from VL_Startup(). This is the native video startup callback of the game (see Listing 6-16).

Example 6-16. Graphics Initialization from wolf_jni.c and vi_null.c.

// This function is new code for Android and lives in wolf_ini.c
void jni_init_graphics(int width, int height)
{
    JNIEnv *env;

    if (!g_VM) {
       ERROR0("I_JNI: jni_init_graphics No JNI VM available.
");
       return;
    }

    (*g_VM)->AttachCurrentThread (g_VM, (void **) &env, NULL);


    iSize = width * height;

    // Create a new int[] used by jni_send_pixels
    jImage = (*env)-> NewIntArray(env, iSize);

    // call doom.util.Natives.OnInitGraphics(w, h);
    jmethodID mid = (*env)->GetStaticMethodID(env, jNativesCls
            , "OnInitGraphics"
            , "(II)V");

    if (mid) {
        (*env)->CallStaticVoidMethod(env, jNativesCls
, mid
               , width, height);
    }
}

// This function lives in vi_null.c
// byte* gfxbuf is the global graphics buffer
void VL_Startup()
{
    // Original code
    vwidth = 320;
    vheight = 200;

    if (MS_CheckParm("x2")) {
        vwidth *= 2;
        vheight *= 2;
    } else if (MS_CheckParm("x3")) {
        vwidth *= 3;
        vheight *= 3;
    }

    if (gfxbuf == NULL)
        gfxbuf = malloc(vwidth * vheight * 1);

    // New code for Android
    jni_printf("VL_Startup %dx%d. Calling init graphics.",vwidth, vheight);
    jni_init_graphics(vwidth, vheight);
}

The function VL_Startup is the video startup function called by the Wolf native engine. Look closer, and you can see that it initializes the size of the screen to a resolution of 320×200 pixels. It also checks for the argument ×2 or ×3 (which tell the Wolf engine to double or triple the screen resolution). It also allocates space for the video buffer. Finally, the last two lines display a debugging message and, most importantly, tell the Java layer that the graphics have been initialized. These two lines are critical for the game to work in Android.

Cascading Video Buffers

After the graphics are initialized, we can start sending video buffer updates to the main activity. To do so, we use the C to Java callback jni_send_pixels in jni_wolf.c and VW_UpdateScreen() in vi_null.c.

jni_send_pixels is almost identical to the callback in the previous section. The main difference being that this callback will fire many times per second and must be as quick and nimble as possible.

  • To achieve maximum performance, the method ID for wolf.jni.Natives.OnImageUpdate is loaded by the native WolfMain implementation.

  • To speed thing up even more, the array buffer used to send the pixels to Java (jImage) is created on jni_init_graphics using the width and height of the buffer.

  • With these two variables, the array region is set by calling: (*env)-> SetIntArrayRegion(env, jImage, 0, iSize, (jint *) data), where data is an array of 32 bit integers packed in Android format (ARGB).

In the second file of Listing 6-17, two important functions are called:

  • VL_SetPalette (const byte *palette): This is the function that sets the palette that will be used to pack the pixels in Android format. Wolf 3D uses a 256-color palette of sequential red, green, and blue (RGB) bytes, (256 × 3 for a total of 768 bytes).

  • VW_UpdateScreen(): With the palette in place, this game callback will fire every time there is a video update. Here, we simply make a palette lookup using the global graphics buffer (gf×buf), and the video width and height (vwidth and vheight) for each byte in gf×buf. Note that gf×buf is an array of bytes that represent color indexes in the palette:

    int size = vwidth * vheight;
    int pixels[size], i;
    
    for (i = 0; i < size; i ++) {
       byte colIdx = gfxbuf[i];
       pixels[i] = (0xFF << 24)
          | (pal[colIdx].red << 16)
          | (pal[colIdx].green << 8)
          | (pal[colIdx].blue);
    }

Finally, the array of pixels is sent to Java along with the video width and height:

jni_send_pixels(pixels,0,0, vwidth, vheight).

Example 6-17. Cascading the Video Buffer

// In jni_wolf.c
// g_VM is the global Java VM
void jni_send_pixels(int * data, int x, int y, int w, int h)
{
    JNIEnv *env;

    if (!g_VM) {
        return;
    }

    (*g_VM)->AttachCurrentThread (g_VM, (void **) &env, NULL);

    // Send img back to java.
    if (jSendImageMethod) {
        (*env)->SetIntArrayRegion(env, jImage, 0, iSize, (jint *) data);

        // Call Java method
(*env)->CallStaticVoidMethod(env, jNativesCls
            , jSendImageMethod
            , jImage
            , (jint)x, (jint)y, (jint)w, (jint)h);
    }
}

// In file vi_null.c
/****************************************************
 * Class XColor
 ***************************************************/
typedef struct Color XColor;

struct Color
{
    int red;
    int green;
    int blue;
    //int pixel;
};

// Pallete
XColor pal[256];

void VL_SetPalette(const byte *palette)
{
    int i;

    VL_WaitVBL(1);

    for (i = 0; i < 256; i++)
    {
        pal[i].red = palette[i*3+0] << 2;
        pal[i].green = palette[i*3+1] << 2;
        pal[i].blue = palette[i*3+2] << 2;
    }
}

void VW_UpdateScreen()
{
    // screen size
    int size = vwidth * vheight;

    // ARGB pixels
    int pixels[size], i;

    for (i = 0; i < size; i ++) {
        byte colIdx     = gfxbuf[i];
        pixels[i]   = (0xFF << 24)
            | (pal[colIdx].red << 16)
            | (pal[colIdx].green << 8)
| (pal[colIdx].blue);

    }
    // send thru JNI here
    jni_send_pixels(pixels,0,0, vwidth, vheight);

}

Cascading Sound and Music Requests

Sound and music requests are cascaded by two C to Java callbacks and changes to the native file sd_null.c:

  • jni_start_sound (int idx): This callback calls the Java method wolf.jni.Natives.OnStartSound(int idx), where idx is the native sound ID.

  • jni_start_music (int idx): This callback calls the Java method wolf.jni.Natives.OnStartMusic(int idx), where idx is the native music ID.

Listing 6-18 shows the implementation of these callbacks. Remember that you must attach to the current thread to prevent a JNI invalid thread access with

(*g_VM)->AttachCurrentThread (g_VM, (void **) &env, NULL)

Once the thread is attached, you can call the Java method using its ID plus the argument (the sound ID in this case):

(*env)->CallStaticVoidMethod(env, jNativesCls, jStartSoundMethod, (jint) idx);

Note that the method IDs for sound and music, jStartSoundMethod and jStartMusicMethod respectively, were loaded at startup for increased performance.

Example 6-18. Cascading Sound and Music Requests

// In jni_wolf.c
void jni_start_sound (int idx)
{
    /*
     * Attach to the curr thread otherwise we get JNI WARNING:
     * threadid=3 using env from threadid=15 which aborts the VM
     */
    JNIEnv *env;

    if (!g_VM) {
       return;
    }

    (*g_VM)->AttachCurrentThread (g_VM, (void **) &env, NULL);

    if (jStartSoundMethod == 0) {
jni_printf("BUG: Invalid JNI method method OnStartSound (I)V");
            return;
    }

    // Call Java method wolf.jni.OnStartSound(int idx)
    (*env)->CallStaticVoidMethod(env, jNativesCls
           , jStartSoundMethod
           , (jint) idx);

}

void jni_start_music (int idx)
{
    /*
     * Attach to the curr thread otherwise we get JNI WARNING:
     * threadid=3 using env from threadid=15 which aborts the VM
     */
    JNIEnv *env;

    if (!g_VM) {
       return;
    }

    if (!jNativesCls) {
       printf("JNIStartMusic: No JNI interface
");
       return;
    }

    (*g_VM)->AttachCurrentThread (g_VM, (void **) &env, NULL);

    jmethodID mid = (*env)->GetStaticMethodID(env, jNativesCls
        , "OnStartMusic"
        , "(I)V");

    if (mid) {
        (*env)->CallStaticVoidMethod(env, jNativesCls
               , mid
               , (jint) idx);
    }
}

// In sd_null.c
extern void jni_start_sound (int idx);
extern void jni_start_music (int idx);

boolean SD_PlaySoundWL6(soundnamesWL6 sound)
{
    // New code for Android
    jni_start_sound (sound);
    return true;
}
boolean SD_PlaySoundSOD(soundnamesSOD sound)
{
    // New code for Android
    jni_start_sound (sound);
    return true;
}

Listing 6-18 also shows the changes required to the native sound handlers (in sd_null.c) to call the C to Java callbacks:

  • SD_PlaySoundWL6 (soundnamesWL6 sound): This is the handler for the Wolf 3D chapter of the game. soundnamesWL6 s a C enumeration described in audiowl6.h, which defines the IDs for sounds and music.

  • SD_PlaySoundSOD (soundnamesSOD sound): This is the handler for the Spear of Destiny (SOD) chapter of the game. soundnamesSOD s a C enumeration described in audiosod.h which defines the IDs for sounds and music.

Finally, you are ready to compile the code and see how it works on the device. Here is how.

Compiling the Native Library

The native library (or Dynamic Shared Object - DSO) contains all the native code and interacts with the Java side of Wolf 3D. Before we can test the game in the emulator, this library must be compiled. Here are the steps required to do this:

  1. Write a Makefile to compile the native code.

  2. Generate the JNI headers required by the JNI interface.

  3. Compile the native code into the dynamic shared library (DSO): libwolf_jni.so.

  4. Place the DSO under the libs/armeabi folder of the main project.

  5. Start the project in the emulator.

Writing the Makefile

The Makefile for Wolf 3D uses the agcc and ald scripts for compiling and linking defined in Chapter 1. Some interesting features of this file are described in Listing 6-19:

  • Compilation: We use -wall to display all warnings and -O2 to define a level of optimization.

  • Linking: The linking step is performed by the script: ald -shared -o libwolf_jni.so $(OBJS), where -shared tells the compiler to build a shared library and -o defines the name of the output library (libwolf_jni.so).

  • JNI Headers: The make target jni is defined to create the headers required by the JNI interface using the javah command: javah -jni -classpath ../../bin -d include wolf.jni.Natives. The class path must point to the location of the compiled wolf.jni.Natives class, -d defines the directory where the output headers will be stored, and wolf.jni.Natives is the Java class that contains the native methods.

Example 6-19. The Makefile for Wolf 3D

CC = agcc

# All warning + Optimizations
CFLAGS = -Wall -O2

# Object files
OBJS = objs.o misc.o id_ca.o id_vh.o id_us.o 
   wl_act1.o wl_act2.o wl_act3.o wl_agent.o wl_game.o 
   wl_inter.o wl_menu.o wl_play.o wl_state.o wl_text.o wl_main.o 
   wl_debug.o vi_comm.o sd_comm.o sd_null.o wl_draw.o

# JNI files
JNIOBJS = jni_wolf.o vi_null.o

OBJS += $(JNIOBJS)

# Main target
all: lib

# Library
lib: $(OBJS) j
        ald  -shared -o libwolf_jni.so $(OBJS)

.c.o:
        @echo
        $(CC) -fpic -c $(CFLAGS) $(INCLUDES) $<

# Create JNI headers
jni:
        @echo "Creating JNI C headers..."
        javah -jni -classpath ../../bin -d include wolf.jni.Natives

clean:
        rm -rf *.o

Generating JNI Headers

We must generate the headers for the JNI interface before we compile the DSO. Use the Makefile to do so:

$make jni

Note that this command must be run within the native/gp2xwolf3d folder of the chapter source. With the JNI headers in place, we can compile the DSO as follows:

$ make

If you make changes to the native code, you might wish to test for missing symbols in the DSO by compiling a simple test program against the library (see Listing 6-20). This is a small program that calls the game loop. It is not meant to be run but to detect linker errors (missing symbols) in the Wolf 3D native library.

Example 6-20. Wolf 3D Test Program Used to Detect Linker Errors

#include <stdio.h>

void _start(int argc, char **argv) {
  int i;
  int myargc = 4;

  // wolf, wolfsw, sodemo, sod, sodm2, sodm3
  char * myargv[] = {"wolf3d", "wolfsw", "basedir", "/data/wolf/"};

  for (i = 0; i < myargc; i++)
    printf("argv[%d]=%s
", i, myargv[i]);

  wolf_main(myargc, myargv);
  exit(0);
}

$ agcc -c test.c
$ ald -o testwolf -L. -lwolf_jni

Testing Wolf 3D in the Emulator

Let's play Wolf 3D in the emulator. Make sure to do this sanity check first: the native library is critical and must be placed in the libs/armeabi folder of the main project (as shown in Figure 6-8). At runtime, the library will be loaded and cached to the right location in the device.

Launch properties for Wolf 3D

Figure 6-8. Launch properties for Wolf 3D

Tip

If the library is not placed properly, the Java system call System.loadLibrary() will fail with an UnsatisfiedLinkError.

To create a launch configuration for Wolf 3D:

  1. Click Run

    Launch properties for Wolf 3D
  2. Right-click the Android Application on the left menu, and click New.

  3. Enter a name: Wolf. Select the corresponding project: ch06.Wolf3D.SW. Click Run.

Take a look at the log messages from the device to make sure everything works (see Listing 6-21). This can be seen from the LogCat view of the Eclipse IDE.

Example 6-21. Log Messages from Wolf 3D

08-21 18:12:50.823: DEBUG/WolfTools(710): Installing game wolfsw.zip in 
Log Messages from Wolf 3D
/data/data/game.wolfsw/files 08-21 18:12:53.653: DEBUG/Wolf3D(710): Start game base dir: /data/data/game.wolfsw/files/
Log Messages from Wolf 3D
game=wolfsw port:true 08-21 18:12:53.663: DEBUG/Wolf3D(710): Loading JNI librray from wolf_jni 08-21 18:12:53.697: DEBUG/LibLoader(710): Trying to load library wolf_jni from LD_PATH:
Log Messages from Wolf 3D
/system/lib 08-21 18:12:53.704: DEBUG/dalvikvm(710): Trying to load lib /data/data/game.wolfsw/lib/
Log Messages from Wolf 3D
libwolf_jni.so 0x43596e78 08-21 18:12:54.034: DEBUG/dalvikvm(710): Added shared lib /data/data/game.wolfsw/lib/
Log Messages from Wolf 3D
libwolf_jni.so 0x43596e78 08-21 18:12:55.305: INFO/System.out(710): ** Wolf Message: WolfMain args[4]=wolf3d 08-21 18:12:55.305: INFO/System.out(710): ** Wolf Message: WolfMain args[4]=wolfsw 08-21 18:12:55.314: INFO/System.out(710): ** Wolf Message: WolfMain args[4]=basedir 08-21 18:12:55.354: INFO/System.out(710): ** Wolf Message: WolfMain args[4]=/data/data/
Log Messages from Wolf 3D
game.wolfsw/files/ 08-21 18:12:55.465: INFO/System.out(710): ** Wolf Message: Now Loading Wolfenstein 3D Plus
Log Messages from Wolf 3D
basedir /data/data/game.wolfsw/files/ 08-21 18:12:55.515: INFO/System.out(710): ** Wolf Message: You Chose Shareware Wolf3D 08-21 18:12:55.766: INFO/ActivityManager(580): Displayed activity game.wolfsw/
Log Messages from Wolf 3D
.WolfLauncher: 8499 ms 08-21 18:12:56.463: INFO/System.out(710): ** Wolf Message: VL_Startup 320x200. Calling
Log Messages from Wolf 3D
init graphics. 08-21 18:12:56.514: DEBUG/Wolf3D(710): OnInitGraphics creating Bitmap of 320 by 200

The first line shows the game files being installed in /data/data/game.wolfsw/files. Note that without these data files the game will crash. The next lines show the native library being successfully loaded by the Java VM:

Trying to load lib /data/data/game.wolfsw/lib/libwolf_jni.so 0x43596e78
Added shared lib /data/data/game.wolfsw/lib/libwolf_jni.so 0x43596e78

At this point, the game should start successfully, and you should be able to use the controller buttons (or the keyboard if you wish) to play Wolfenstein 3D in your Android device (see Figure 6-9). Enjoy!

Wolf 3D in action on the emulator

Figure 6-9. Wolf 3D in action on the emulator

What's Next?

Who would have thought that the PC version of Wolf 3D could ever be run on a mobile device? In this chapter, you have learned how this can be accomplished by embracing the power of two great languages: Java and C. Specifically, we covered the basic game architecture and how the following Java and C components—the main activity, audio, JNI interface, and native code—fit together. Next, we looked at the Java layer that controls the interaction with the device including:

  • The game launcher activity: It contains the UI layout and the device event listeners for keyboard or touch events.

  • Game loop: It contains the user-defined thread that loads the native library and starts the native game loop.

  • Sound and music handlers: They receive notifications from the native layer when sound or background music should be started.

  • Movement controller handlers: They control movement events using a UI-based game pad useful for phones that have no keyboard.

  • Sound classes: They work in concert with the sound and music handlers to provide high quality sound to the game.

  • Native interface class: It contains the native method implementations that allow the Java code to talk to the C code, plus C-to-Java callbacks for communication in the opposite direction.

Next, we looked at the native code where we made simple changes to the original C code to insert C to Java callbacks. These callbacks are used as part of the JNI layer to cascade graphics information, video buffers, and sound/music requests back to Java. Finally, we looked at compilation and testing. Here, we covered the Makefile required for building the DSO as well as how to set up the game project file system for testing on the emulator.

In the next chapter, we continue with 3D shooters by looking at the next great PC shooter, Doom. That chapter will show you how easy it is to bring this complex game for the PC to Android in record time with minimal changes.

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

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