CHAPTER 12

image

Building A Game Prototyping Tool for Android Mobile Devices

Gustavo Samour

You never truly know where inspiration will come from, or when it will happen. A game developer can be caught off guard at home, on the subway, or while on vacation. Forgetting an idea because one was not able to write it down can be a frustrating feeling. And, while finding a pen and paper is easy, oftentimes these tools are not robust enough to fully capture the essence of an idea. Sometimes something more sophisticated, like a game prototyping tool, is necessary to test and better express a concept. If a picture is worth a thousand words, then an interactive demo is worth a million.

We are used to seeing these tools on personal computers such as workstations and laptops. If inspiration strikes while at home or in the office, developers can boot up their machines and get to work. But if inspiration strikes while on the move, there’s a good chance a long time will pass before having access to a computer. Fortunately, smartphones and tablets are powerful enough to run a game prototyping tool, and they are practical enough to allow us to use such a tool while we are out and about.

This chapter will show you how to write your own mobile game prototyping tool using AngelScript1. The first section provides basic information on how to set up your PC for Android programming. Those well versed in Android development, however, may want to skip the section on getting ready. The meat of this article is the script engine implementation in native code, and its integration with Java via the Java Native Interface. JNI is a standard programming interface for writing Java native methods2.

Getting Ready for Development

This first section explains the purpose of this chapter and provides a description of the software to be written. There is also a message on scope, as there is more to the subject than can be covered in these pages. A list of required tools is shown, with a description of each one. And finally, you will find a brief explanation of the code structure for the software.

A Game Prototyping Tool

For our purposes, a game prototyping tool is a specialized, limited version of a game authoring tool. Its goal is to allow the validation of a game idea; the program does not facilitate the creation of a full game. Some examples of complete authoring tools are UDK by Epic Games, Unity Engine by Unity Technologies, and GameMaker by YoYo Games. The tool you will develop in this chapter was envisioned with GameMaker in mind. I’ll show a minor feature set in this chapter, and you can take it further in the direction that suits you best.

Scope

A prototyping tool is made up of many parts, among them a scripting engine, and an entire book can be written just about that. To keep both the text and the code short, some compromises have to be made. First of all, the reader is expected to possess knowledge of certain tools, programming languages, and APIs. Most concepts are not explained from the ground up. Second, since the focus is on functionality, the resulting application’s user interface will be minimal and will allow for 2D prototypes. Third, optimization and best practices in coding are not a priority. Fourth, not every snippet of code is written out. Most notably, all Java import and C/C++ #include statements have been left out. However, the complete source code is included in the download pack on the book’s page at www.apress.com. Finally, implementing features that are not essential to understanding the system are left as an exercise for the reader.

Even though our scope is limited, after finishing this chapter, you will have a powerful tool in your hands. You will be able to add and remove game objects in a scene, as well as give each scene object a script to follow at runtime. After the initial setup, a different screen will allow the scene to be played, paused, and stopped. During playback, game object scripts will control the action. Each object will be capable of modifying its own properties, a feature that should be sufficient for testing various styles of gameplay.

Purpose

This article was written to give the reader a low-level glimpse into the world of game creation tools in a mobile environment. While the subject is massive, I will boil it down to essentials by developing a tool of limited capability. Hopefully, the article will spark an interest and you will explore the subject matter further. It would be interesting to see the end result being improved with unique contributions.

Tools

It is not uncommon for game development tools to rely on other tools, and to build your Android application you’ll need the help of the software listed in Table 12-1.

Table 12-1. Tools Required to Create the Game Prototyping Tool (GPT)

Software

Purpose

Android SDK

Java APIs for developing Android applications

Android NDK

C++ APIs for developing native Android libraries

Eclipse

IDE for building and debugging Android applications

ADT Plug-in

Allows Eclipse to work with Android applications

Cygwin

Unix-like environment that runs on MS Windows

AngelScript

Library for building and executing game object scripts

The Android SDK is a set of APIs for developing applications using the Java language. Because the SDK has been revised and different devices come with different versions of the Android operating system, there are different API levels. At the time of this writing, the most recent is API Level 19. It corresponds to version 4.4 of the OS, otherwise known as the Kit Kat release3. However, you can use a lower API Level to be able to target older devices. The user interface, which comprises the majority of code in your game prototyping tool, will be written in Java.

As a way to allow native development on the Android platform and for developers to take advantage of existing C/C++ code libraries, the NDK was released. Like the SDK, it has been through several revisions, and the latest one at the time of writing is Revision 9d4. The core of your tool, the scripting system, will be written in C/C++ and compiled into a shared library.

To make development easier, an integrated development environment (IDE) will be used for writing code and building the project. Developers nowadays have their choice of software, and the reader is welcome to use their preferred IDE. However, it is worth mentioning that Eclipse makes it easy to write Android applications. The ADT plug-in adds project templates for Android development, tools to create user interface layouts, Android debugging capabilities, and more.5

Cygwin is an optional tool that provides a Unix-like environment on the Windows platform6. You will use it for building native code into libraries usable by Android applications. The Android NDK comes with the ndk-build shell script that must run in a Unix-like environment. The reason it’s optional is that as of NDK Revision 7, there is a new Windows-compatible build script. The ndk-build.cmd script allows developers to build native code libraries on a Windows system without Cygwin. However, the documentation lists this script as an experimental feature of NDK Revision 7, so it may have issues.

The scripting library used in this article is AngelScript. This is an open source project developed by Andreas Jönsson that includes contributions by many other developers. You will use it to build the core of your game prototyping tool, which is a script engine. It will be responsible for initializing and updating a scene full of game objects.

Code Structure

An Android application is generally written in Java and is made up of one or more Activity classes. Each activity can set one or more View objects as a user interface. One activity is enough for the prototyping tool, which will implement three different views: a scene editing canvas, a script text editor, and a scene-playing screen. A list of game objects will be kept when editing a scene and when playing it, so you’ll write a couple of classes that represent game objects. To run scripts while playing a scene, the tool will interface with a basic scripting engine written in C/C++. Java code will take care of calling native code and getting what it needs from the script engine; this will happen via the Java Native Interface. Some advantages of JNI are reusing an existing code library without converting it to another language, better performance by running some processes in a native environment instead of a virtual machine, and ease of porting to other platforms that support native code. On the C/C++ side there will be a main source file and a pair of .h/.cpp files for a script engine class. Table 12-2 lists all source files and their purpose.

Table 12-2. GPT Source Files

Filename

Purpose

GPTActivity.java

Fulfills the requirement of an Activity class

GPTEditorGameObject.java

GameObject class with properties related to the Scene Editor

GPTSceneEditorView.java

Custom View for placing objects in a scene

GPTScriptEditorView.java

Custom View for editing scripts manually

GPTJNILib.java

Exposes native functions to other Java classes

GPTGameObject.java

GameObject class with properties related to the Scene Player

GPTScenePlayerView.java

Custom View for playing a scene

GPTMain.cpp

Implements the native functions declared in GPTJNILib.java

GPTScriptEngine.h

Declaration of the script engine’s class and members

GPTScriptEngine.cpp

Implementation of the script engine’s member functions

User Interface Implementation

Now that the required tools are installed, let’s create a new Android project. First, open the Eclipse IDE if it’s not already running. The following dialogs may be different on other machines, but should still be fairly consistent. Click the File menu, go to New and click Project. In the New Project dialog box, select the Android Application Project wizard and click Next. Write down a project name, such as “GPT” or “Game Prototyping Tool.” Next, change the package name to com.gametoolgems.gpt or something similar. Now, choose a build SDK and a minimum required SDK. The code I’ve written requires at least API Level 10, but with a few changes, it can compile on a lower SDK version. Select an SDK that is appropriate for the devices you want to target and click Next. Keep going until the wizard asks for an activity name and choose an appropriate name, like “GPTActivity.” Keep going through the wizard until you can click the Finish button. A new Android project will be created in your workspace folder.

GPTActivity

The main activity class doesn’t have to do much. In its onCreate method, it will make instances of the three View classes in the application and then bring the scene editor to the front. When handling the onBackPressed event, it will simply pass it forward to the current View. To do this, it needs to keep track of the current View so it will publicly expose a SetCurrentView method. Listing 12-1 represents the code for GPTActivity.

Listing 12-1. GPTActivity Class

public class GPTActivity extends Activity
{
  private View mCurrentView = null;
  public static GPTScriptEditorView scriptEditorView;
  public static GPTSceneEditorView sceneEditorView;
  public static GPTScenePlayerView scenePlayerView;

  @Override
  public void onCreate(Bundle savedInstanceState)
 
    {
    super.onCreate(savedInstanceState);
 
    // Load native code library
    GPTJNILib.Load();
 
    // Create views and set current view to scene editor
    scriptEditorView = new GPTScriptEditorView(this);
    scenePlayerView = new GPTScenePlayerView(this);
    sceneEditorView = new GPTSceneEditorView(this);
    SetCurrentView(sceneEditorView);
  }
  
  public void SetCurrentView(View v)
  {
    mCurrentView = v;
    setContentView(v);
  }
    
  @Override
  public void onBackPressed()
  {
    if (mCurrentView == scriptEditorView)
      scriptEditorView.onBackPressed(this);
    else if (mCurrentView == sceneEditorView)
      sceneEditorView.onBackPressed(this);
    else if(mCurrentView == scenePlayerView)
      scenePlayerView.onBackPressed(this);
  }
}

GPTJNILib

Rather than give native member methods to each class that requires C/C++ interaction, the project will have an abstract class that exposes all the necessary native functions to the rest of the Java code. This simplifies JNI function naming on the C/C++ side. There are two main things the native code should do to interact with the scripting engine: set up a scene and update a scene. GPTJNILib.java only declares those two static functions, plus an additional function to load the C/C++ library itself, as shown in Listing 12-2.

Listing 12-2. GPTJNILib Abstract Class

public abstract class GPTJNILib
{
  public static void Load()
  {
    System.loadLibrary("gptjni");
  }
 
  public static native void Initialize(
    GPTEditorGameObject[] gameObjects);

  public static native void Update(
    float deltaTime,
    ArrayList<GPTGameObject> updatedGameObjects);
}

Scene Editor and Script Editor

Generally, a game creation tool allows drawing or importing sprites, then generating prefabricated objects, or “prefabs,” that use those sprites. Behavior-modifying scripts can also be added to the prefabs. When placed in a scene, those prefabs become unique game objects. For your simplified prototyping tool, colored circles take the place of objects and each object has exactly one script. The first step in creating a scene editor is to add a couple of classes that represent a game object, as shown in Listings 12-3 and 12-4.

Listing 12-3. GPTEditorGameObject Class

public class GPTEditorGameObject
{
  private static int NEXT_ID = 0;
  public static final int DEFAULT_COLOR = Color.WHITE;
  private static final String DEFAULT_SCRIPT =
  ""                                                          + " " +
  "void OnUpdate(float deltaTime)"                            + " " +
  "{"                                                         + " " +
  "  "                                                        + " " +
  "}"                                                         + " " +
  ""                                                          + " ";

  public int id;
  public float x;
  public float y;
  public int color;
  public String script = DEFAULT_SCRIPT;

  GPTEditorGameObject(float objX, float objY, int objColor)
{
    id = NEXT_ID++; // assign unique ID to game object
    x = objX;
    y = objY;
    color = objColor;
  }
}

Listing 12-4. GPTGameObject Class

public class GPTGameObject
{
  public int id;
  public float x;
  public float y;
  public int color;

  GPTGameObject
    (int objId, float objX, float objY, int objColor)
  {
    id = objId;
    x = objX;
    y = objY;
    color = objColor;
  }
}

As you can see, all the class does is store five properties for an object: an identifier, position along the X axis, position along the Y axis, a color, and a script to run when playing the scene. The difference between the editor and player versions of the class is that the editor sets an identifier automatically, and the player doesn’t need to store the script code. The second step in creating a scene editor is adding a View class, which you’ll call GPTSceneEditorView. Aside from creating a formal design document, the best way to explain what the code will do is to look at a screenshot of the result, such as Figure 12-1.

9781430267003_Fig12-01.jpg

Figure 12-1. A screenshot of the scene editor

Figure 12-1 shows the finished scene editor. The user interface has three buttons and an empty area that represents the scene. The GUI button on the left shows the current color; every time a new object is added, its color is set to the current color. Tapping on this button cycles between different colors. The middle button is for editing the selected object’s script. It brings up the GPTScriptEditorView. When users are done editing the scene, the button on the right takes them to the scene player screen. Tapping inside the empty area adds a new object. If the tap is on top of an existing object, it becomes the selected object. Selection is visually represented with a gray outline. Dragging works as one would expect it to: the selected object is moved to the desired location.

Because the main focus of this chapter is the scripting engine, code for the GPTSceneEditorView and GPTScriptEditorView classes has been omitted from these pages. As an exercise, feel free to implement those classes in a custom way. But if you prefer the original code, it can be downloaded from www.apress.com.

Scene Player

The scene player is where the game prototype comes to life. This is the screen where the objects’ scripts run and all the action takes place. The user interface is presented in a similar fashion to that of the scene editor: an empty area represents the scene, and the GUI area consists of a play/pause button and a stop button. Pressing the play button runs the GPTJNILib class’s native Update function in a loop and swaps the button with a pause button. Pressing the Pause button causes the application to skip updates, which keeps every game object in its current position. The Stop button resets the scene, bringing all the game objects back to their initial state. Finally, pressing the Back button on the device closes the scene player and shows the scene editor.

The first thing you need for playing is a list of game objects that will be kept up to date. Then you’ll need a variable to tell if the simulation is playing, as opposed to being paused or stopped. You’ll also need a variable that holds the last frame’s timestamp, which is used to compute delta time. The last two member variables in the class, OBJECT_RADIUS and GUI_SCREEN_HEIGHT_PCT, determine the size of each game object and the height of the simulation control buttons, respectively. See Listing 12-5 for the code.

Listing 12-5. GPTScenePlayerView Member Variables, Constructor, and Example of OnBackPressed Function

private ArrayList<GPTGameObject> mUpdatedGameObjects;
private boolean mIsPlaying;
private long mLastTime;
private static final float OBJECT_RADIUS = 20.0f;
private static final float GUI_SCREEN_HEIGHT_PCT = 0.125f;
 
public GPTScenePlayerView(Context context) {
  super(context);
  mIsPlaying = false;
  mUpdatedGameObjects = newArrayList<GPTGameObject>();
}
 
  public void onBackPressed(GPTActivity activity) {
    activity.SetCurrentView(GPTActivity.sceneEditorView);
  }

There is nothing special about the constructor or the OnBackPressed function. The first just calls the base constructor and sets some default values, while the second is a handler function that gets called by GPTActivity whenever the user presses the back key on their device. In this particular case, pressing this key on the scene player takes the user back to the scene editor.

SetSceneObjectsis the first major function of the scene player class. It takes a list of GPTEditorGameObject instances and populates an internal list of GPTGameObject instances, losing the String-typed script property in the process. It also turns the list of editor objects into an array and passes the array to native code via GPTJNILib’s Initialize function as shown in Listing 12-6.

Listing 12-6. GPTScenePlayerView SetSceneObjects Function

public void SetSceneObjects(
ArrayList<GPTEditorGameObject> sceneObjects)
{
  mUpdatedGameObjects.clear();

  for (GPTEditorGameObject obj : sceneObjects)
{
    mUpdatedGameObjects.add(new GPTGameObject(
    obj.id, obj.x, obj.y, obj.color));
}

  GPTEditorGameObject[] gameObjects =
new GPTEditorGameObject[mUpdatedGameObjects.size()];
 
  GPTJNILib.Initialize(sceneObjects.toArray(gameObjects));
}

These two functions are fairly straightforward; if the user touches the GUI, the HandleGUITouch function is called, as shown in Listing 12-7.

Listing 12-7. GPTScenePlayerView Touch Functions

@Override
public boolean onTouchEvent(MotionEvent event) {

  PointerCoords coords = new PointerCoords();
  event.getPointerCoords(0, coords);
 
    if(event.getAction() == MotionEvent.ACTION_DOWN)
      HandleTouchDownEvent(coords.x, coords.y);
    
    return true;
}
 
private void HandleTouchDownEvent(float x, float y) {
 
  // If we touched the GUI, interact with it
  Rect guiRect = new Rect(0, 0, getWidth(),
    (int)((getHeight() * GUI_SCREEN_HEIGHT_PCT)));

  if (guiRect.contains((int)x, (int)y))
    HandleGUITouch(x, y);
}

If the user presses the play button, the mIsPlaying flag gets set to true, the timer is started, and the scene player’s Update function will continually request an up-to-date list of game objects. If the Pause button is pressed, mIsPlaying is set to false. If the Stop button is pressed, the scene is reset directly; all game objects move back to their original positions as shown in Listing 12-8. The Update function is shown in Listing 12-9.

Listing 12-8. GPTScenePlayerView HandleGUITouch Function

private void HandleGUITouch(float x, float y) {
 
  Rect playPauseButtonRect =
    new Rect(0, 0, getWidth() / 2,
    (int)((getHeight() * GUI_SCREEN_HEIGHT_PCT)));
 
  Rect stopButtonRect =
    new Rect(getWidth() / 2, 0, getWidth(),
    (int)((getHeight() * GUI_SCREEN_HEIGHT_PCT)));

  if (playPauseButtonRect.contains((int)x, (int)y))
  {
    mIsPlaying = !mIsPlaying;
 
    // start timing
    if (mIsPlaying)
      mLastTime = System.nanoTime();
  }
  else if (stopButtonRect.contains((int)x, (int)y))
  {
    // reset game objects
    mIsPlaying = false;
    SetSceneObjects(
      GPTActivity.sceneEditorView.
          GetSceneObjects());
  }
}

Listing 12-9. GPTScenePlayerView Update Function

public void Update()
{
  if (mIsPlaying)
  {
    // Get updated game objects
    long currTime = System.nanoTime();
    float dt = (currTime - mLastTime) / 1000000000.0f;
    mLastTime = currTime;

    mUpdatedGameObjects.clear();
    GPTJNILib.Update(dt, mUpdatedGameObjects);
}
}

Drawing the scene happens via Java’s onDraw function. The desired behavior is for drawing to happen over and over again in a loop. But by default, the function is only called once. The way to get around that is by invalidating the draw region at the end of onDraw, which will force another call to the function. By forcing execution on every function call, you guarantee the desired loop. Pausing will set mIsPlaying to false, which will skip the body of the Update function. Stopping will do the same, but will additionally cause the state of the scene to be reset. Now let’s look at the onDraw function as shown in Listing 12-10.

Listing 12-10. GPTScenePlayerView onDraw Function

@Override
protected void onDraw(Canvas c)
{
  // update scene first, then draw
  Update();
 
  super.onDraw(c);

  // Draw background
  Paint paint = new Paint();
  paint.setAntiAlias(true);
  paint.setColor(Color.BLACK);
  c.drawPaint(paint);

  // draw game objects
  for (int i = 0; i < mUpdatedGameObjects.size(); ++i)
  {
    GPTGameObject obj = mUpdatedGameObjects.get(i);

    paint.setColor(obj.color);
    c.drawCircle(obj.x, obj.y, OBJECT_RADIUS, paint);
  }

  // Draw UI
  paint.setColor(Color.GRAY);
  Rect rect = new Rect(0, 0, getWidth(),
    (int)(getHeight() * GUI_SCREEN_HEIGHT_PCT));

c.drawRect(rect, paint);

  String playPauseString = mIsPlaying ? "Pause" : "Play";
  Rect strBounds = new Rect();
  paint.setColor(Color.WHITE);
  paint.setTextSize(30);
  paint.setTextAlign(Align.CENTER);
  paint.getTextBounds(playPauseString, 0,
    playPauseString.length(), strBounds);
  c.drawText(playPauseString, getWidth() / 4,
    ((getHeight() * GUI_SCREEN_HEIGHT_PCT) +
    (strBounds.bottom - strBounds.top)) / 2, paint);
  paint.setColor(Color.BLACK);
  c.drawLine(getWidth() / 2, 0, getWidth() / 2,
    (getHeight() * GUI_SCREEN_HEIGHT_PCT), paint);

  String stopStr = "Stop";
  strBounds = new Rect();
  paint.setColor(Color.WHITE);
  paint.setTextSize(30);
  paint.setTextAlign(Align.CENTER);
  paint.getTextBounds(stopStr,0,stopStr.length(),strBounds);
  c.drawText(stopStr, getWidth() - (getWidth() / 4),
    ((getHeight() * GUI_SCREEN_HEIGHT_PCT) +
    (strBounds.bottom - strBounds.top)) / 2, paint);
 
  // Force re-draw
postInvalidate();
}

The first thing that happens, even before drawing, is updating the game objects with results from the script engine. Then, along with the GUI elements, all the objects are drawn. Finally, the postInvalidate function call makes sure onDraw is called again.

Good news: this function marks the last of the Java code! You are now ready to move on to native C/C++ with the scripting engine implementation.

Native Script Engine Implementation

In the last section, you declared two native functions in the GPTJNILib class, Initialize and Update. Their implementations are still missing, so you will take care of that in this section. What does it mean to initialize the script engine? It means setting up the scene as it was created in the editor, but now in the context of scripted objects. How about updating? So many different things can be thought of as taking part in updating a scene. For example, collision detection and input handling may be a part of updating. Creating and destroying objects can also be considered updating. But for your purposes, updating a scene means calling the OnUpdate script function on every game object. Detecting collisions, handling touch input, and creating and destroying objects are left as an exercise to the reader.

Building GPT’s Native Library with Android NDK

Preparing the build setup for native code can be tricky at first, but once it works, it’s highly unlikely that it will require changes. The first thing to do is build AngelScript for Android as a static library. There are many ways to set up for building, but the following is an easy way.

  1. Inside the AngelScript source distribution, there is a /projects/android folder. Navigate to it, and create a subfolder named jni.
  2. Move the Android.mk file from /projects/android to the new /projects/android/jni folder.
  3. Open the Android.mk file and make the following changes:
    • Replace the line
    LOCAL_PATH:= $(call my-dir)/../../source

    with

    LOCAL_PATH:= $(call my-dir)/../../../source
    
    • Add the following lines at the end of the file:
    include $(CLEAR_VARS)
    LOCAL_MODULE := dummy
    LOCAL_STATIC_LIBRARIES := libangelscript
    include $(BUILD_SHARED_LIBRARY)
  4. Open Cygwin, navigate to the android project’s folder within AngelScript (i.e., /cygdrive/c/angelcode/angelscript/projects/android) and run the ndk-build script. For example, if the Android NDK was installed in C:android-ndk-r9d, then the command would be
    /cygdrive/c/android-ndk-r9d/ndk-build [ENTER]

Adding an extra ../ to Android.mk is required because in the previous step, that file was moved one folder deeper than it used to be. Adding the four lines at the end is necessary because a static library doesn’t have to build on its own—unless other code depends on it. In this case, you are using a dummy library to create a dependency. After running ndk-build, you should have a libangelscript.a static library file.

The next step is to create a jni folder inside the game prototype tool’s project folder. Copy the libangelscript.a file into the jni folder and create two files, Android.mk and Application.mk, inside that same folder.

The Android.mk file tells the build system which source files to compile, which libraries to link, and which type of library to output, as shown in Listing 12-11.

Listing 12-11. Android.mk File

LOCAL_PATH:= $(call my-dir)
 
# Declare the prebuilt AngelScript static library
 
include $(CLEAR_VARS)
LOCAL_MODULE    := libangelscript
LOCAL_SRC_FILES := libangelscript.a
 
include $(PREBUILT_STATIC_LIBRARY)
 
# Build our JNI shared library
 
include $(CLEAR_VARS)
 
LOCAL_MODULE    := libgptjni
LOCAL_CFLAGS    := -Werror
LOCAL_CPPFLAGS  := -fexceptions
 
LOCAL_SRC_FILES :=      GPTMain.cpp
                        GPTScriptEngine.cpp
 
LOCAL_LDLIBS    := -llog
LOCAL_STATIC_LIBRARIES := libangelscript
 
include $(BUILD_SHARED_LIBRARY)

You will use containers from the Standard Template Library, or STL, in your native code. The Android NDK comes with its own version of STL and must be built. The Application.mk file has a single line that tells the NDK to build STL and to give us access to it (see Listing 12-12).

Listing 12-12. Application.mk File

APP_STL := gnustl_static

GPTScriptEngine

Let’s briefly go over the requirements for the script engine. It should be able to take a scene as a collection of game objects, then create and manage a representation of that scene. The goal is to allow calling script functions on each object from within C/C++, which, along with compiling scripts, is the next requirement for the engine. The only script function you want to call in your simplified system is OnUpdate. But each object can implement it differently; this shows the power and flexibility of the prototyping tool. One last requirement is a way to reset the scene. The idea is to forget about the current collection of game objects and initialize the system with a new set of objects. An example is a situation in which the user navigates back to the editor, makes changes to the scene, and returns to the scene player.

The header file GPTScriptEngine.h is the blueprint for your scripting implementation, as shown in Listing 12-13.

Listing 12-13. GPTScriptEngine.h File

#ifndef _GPT_SCRIPT_ENGINE_H_
#define _GPT_SCRIPT_ENGINE_H_
 
#include "angelscript.h"
#include <vector>
 
struct ScriptModule
{
  int id;
  asIScriptModule* pModule;
};
 
struct GameObject
{
  int id;
  float x;
  float y;
  int color;
};
 
class GPTScriptEngine
{
public:
 
  GPTScriptEngine();
  ~GPTScriptEngine();

  bool AddGameObject(int id, float x, float y, int color,
    const char* scriptCode);

  void Update(float deltaTime);

  void GetUpdatedObjects(std::vector<GameObject>& objects);
 
  void Reset();

private:
 
  typedef std::vector<ScriptModule> ScriptModuleVector;
 
  asIScriptModule* BuildScriptModule(int gameObjectId, const
    char* scriptCode);
 
  asIScriptModule* FindScriptModule(int moduleId);
 
  asIScriptEngine* m_pScriptEngine;
 
  asIScriptContext* m_pScriptContext;
 
  std::vector<ScriptModule> m_scriptModules;
};
 
#endif  // _GPT_SCRIPT_ENGINE_H_

The ScriptModule structure maps a game object to an AngelScript module. Your basic implementation uses one module per game object, which is a very simple way to integrate scripting, however it imposes a big overhead for the application. A better way would be to use script classes, which are a standard part of AngelScript. For your purposes, one module for each game object is good enough.

Having a vector of ScriptModule instances helps iterate through all the game objects and call special script functions on them, such as Update. The GameObject structure is a helper that allows the script engine to provide an updated vector of objects via the GetUpdatedObjects function. The other important members are the AngelScript engine and context. Only one context is necessary for simple scripting.

Now that the blueprint is finished, let’s begin with the GPTScriptEngine.cpp implementation file. You will define some common script code that will be added to all of your AngelScript modules. First, you need a scripted representation of a game object to be able to modify properties like position and color. Second, a global game object is created. Since this code will be added to every module, each instance will be global only to its unique module. The reason to make it global is to allow access to game object properties via a named reference (i.e., me.color). Finally, a set of global functions is defined. PreInit is called automatically when adding a game object to the system and it copies the property values to the scripted representation. GetProperties is called in a post-update step to gather the values of each game object and pass them along to the GUI. Listing 12-14 shows C/C++ code that defines strings with AngelScript code inside.

Listing 12-14. Common Script Code for All Modules

const char* classesCode =                                          
"                                                               "
"class CGameObject                                             "
"{                                                             "
"  public int  id;                                             "
"  public float  x;                                             "
"  public float  y;                                             "
"  public int  color;                                           "
"}                                                             "
"                                                               "
;
 
const char* globalObjectsCode =                                    
"CGameObject me;                                               "
;
 
const char* globalFunctionsCode =                                  
"                                                               "
"void PreInit(float x, float y, int color)                     "
"{                                                             "
"  me.id     = id;                                             "
"  me.x      = x;                                               "
"  me.y      = y;                                               "
"  me.color  = color;                                           "
"}                                                             "
"                                                               "
"void GetProperties(float&out x,float&out y,int&out color)     "
"{                                                             "
"  id        = me.x;                                           "
"  x         = me.x;                                           "
"  y         = me.y;                                           "
"  color     = me.color;                                       "
"}                                                             "
"                                                               "
;

The lifecycle of the script system requires two things: an AngelScript engine and an AngelScript context, which you’ll use for executing scripts. An optional but important step is to set a callback function that AngelScript uses for reporting errors, warnings, and other information. The current one just writes the message to the Android log. But a better system would pass the message to the GUI. Since the user should find out about script errors as soon as possible, a good idea would be to build the code in the script editor and inform of any errors then. Listing 12-15 shows the implementation of the script engine’s constructor, destructor, and message callback functions.

Listing 12-15. Script Engine Constructor and Destructor

void MessageCallback(const asSMessageInfo *msg, void *param)
{
  const char* type = "UNKNOWN";
  if(msg->type == asMSGTYPE_ERROR)
    type = "ERROR";
  else if(msg->type == asMSGTYPE_WARNING)
    type = "WARNING";
  else if(msg->type == asMSGTYPE_INFORMATION)
    type = "INFO";
 
  LOGE("%s (%d, %d) : %s : %s ",
    msg->section, msg->row, msg->col,
    type, msg->message);
}
 
GPTScriptEngine:: GPTScriptEngine()
{
  m_pScriptEngine=asCreateScriptEngine(ANGELSCRIPT_VERSION);
  m_pScriptContext = m_pScriptEngine->CreateContext();
 
  m_pScriptEngine->SetMessageCallback(
    asFUNCTION(MessageCallback), 0, asCALL_CDECL);
}
 
GPTScriptEngine::~GPTScriptEngine()
{
  m_pScriptContext->Release();
  m_pScriptContext = NULL;

  m_pScriptEngine->Release();
  m_pScriptEngine = NULL;
}

The next member function to be implemented is AddGameObject. This method receives all the properties of a game object and the script it should run. The function builds the script into a module and creates a new entry in the m_scriptModules vector. In order to give the scripted game object the same values as the native object, a call is made to the PreInit script function in the module. And now that the new object is in a container, the system keeps track of it and later calls the OnUpdate script function on it, as Listing 12-16 demonstrates.

Listing 12-16. AddGameObject and BuildScriptModule Functions

bool GPTScriptEngine::AddGameObject(int id, float x, float y, int color, const char* scriptCode)
{
  // Build an AS module using the code
  asIScriptModule* pScriptModule =
    BuildScriptModule(id, scriptCode);

  if (pScriptModule)
  {
    // Call 'PreInit' on the script's game object
    asIScriptFunction* pPreInitFunction =
      pScriptModule->GetFunctionByDecl(
        "void PreInit(int id,float x,
        float y, int color)");
 
    m_pScriptContext->Prepare(pPreInitFunction);
 
    m_pScriptContext->SetArgDWord(0, id);
    m_pScriptContext->SetArgFloat(1, x);
    m_pScriptContext->SetArgFloat(2, y);
    m_pScriptContext->SetArgDWord(3, color);

    int retCode = m_pScriptContext->Execute();
    if(retCode != asEXECUTION_FINISHED)
    {
      // PreInit didn’t finish executing.
      return false;
    }

    // Add module to list
    ScriptModule module;
    module.id = id;
    module.pModule = pScriptModule;
    m_scriptModules.push_back(module);

    return true;
  }
  else
  {
    // PreInit didn’t compile. Do error handling.
  }
}
 
asIScriptModule* GPTScriptEngine::BuildScriptModule
(int id, const char* scriptCode)
{
  // Create a new script module
  std::stringstream moduleStream;
  moduleStream << id;
  std::string modName = moduleStream.str();

  asIScriptModule *pScriptModule =
    m_pScriptEngine->GetModule(modName.c_str(),
      asGM_ALWAYS_CREATE);

  // Load and add the script sections to the module
  pScriptModule->AddScriptSection(
    "mainSection", scriptCode);
 
  pScriptModule->AddScriptSection(
    "classesSection", classesCode);
 
  pScriptModule->AddScriptSection(
    "globalObjectsSection", globalObjectsCode);
 
  pScriptModule->AddScriptSection(
    "globalFunctionsSection", globalFunctionsCode);
 
  // Build the module
  int retCode = pScriptModule->Build();

  if( retCode < 0 )
  {
    // Build failed, delete the module and return NULL
    m_pScriptEngine->DiscardModule(modName.c_str());
    return NULL;
  }

  return pScriptModule;
}

The BuildScriptModule function creates an empty module, adds the object’s source code, and also adds the common code you defined previously. It compiles the combined script and returns an AngelScript module if successful.

So far, the functions you’ve implemented don’t make the game objects come to life; this will happen when you update the scripting system. Each object has a script function called OnUpdate that dictates its behavior at every step of the game loop. The scripting engine iterates through the collection of game objects and calls that function on each module, as shown in Listing 12-17.

Listing 12-17. The Script Engine’s Update Function

void GPTScriptEngine::Update(float deltaTime)
{
  // Call the "OnUpdate" script function on every module
  for (size_t i = 0; i < m_scriptModules.size(); ++i)
  {
    asIScriptModule* pScriptModule =
      m_scriptModules[i].pModule;
 
    asIScriptFunction* pOnUpdateFunction =
      pScriptModule->GetFunctionByDecl(
        "void OnUpdate(float dt)"
        );

    m_pScriptContext->Prepare(pOnUpdateFunction);
    m_pScriptContext->SetArgFloat(0, deltaTime);
    int retCode = m_pScriptContext->Execute();
    if(retCode != asEXECUTION_FINISHED)
    {
      // OnUpdate didn’t finish executing.
    }
  }
}

With the game objects now being updated every frame, their behavior is alive inside the script engine. However, you still can’t visualize it. The Java-based user interface doesn’t know what’s happening in the script world just yet. One way to change that situation is to gather an updated collection of game objects from the script engine and send it via JNI. The GUI will then replace its current object collection with the updated one. As a first step, let’s implement the GetUpdatedObjects member function, which builds a collection of game objects from the current state in the script engine, as shown in Listing 12-18.

Listing 12-18. The GetUpdatedObjects Function

void GPTScriptEngine::
GetUpdatedObjects(std::vector<GameObject>& updatedObjects)
{
  // For each script, call the GetProperties function
  for (size_t i = 0; i < m_scriptModules.size(); ++i)
  {
    asIScriptModule* pScriptModule =
      m_scriptModules[i].pModule;

    asIScriptFunction* pGetPropertiesFunction =
      pScriptModule->GetFunctionByDecl(
        "void GetPropertyValues(
          int&out id, float&out x,
          float&out y,
          int&out color)");

    m_pScriptContext->Prepare(pGetPropertiesFunction);

    GameObject obj;
    obj.id = m_scriptModules[i].id;

    m_pScriptContext->SetArgAddress(0, &obj.x);
    m_pScriptContext->SetArgAddress(1, &obj.y);
    m_pScriptContext->SetArgAddress(2, &obj.color);

    int retCode = m_pScriptContext->Execute();
    if(retCode != asEXECUTION_FINISHED)
    {
      // GetProperties didn’t finish executing.
      continue;
    }

    updatedObjects.push_back(obj);
  }
}

You now have enough code to add objects to a scripted scene and update them. You also are halfway into sending the updated objects back to the Java GUI. But in its essence, scene playback is already possible. There’s only one thing missing. What if the user, after watching a playback of the scene, decides to go back to the scene editor and delete an object? What if they want to add objects or edit the script of an existing object? The script system needs a way to reset itself to allow initialization of a new or modified scene. The Reset function removes all the script modules from the engine, as shown in Listing 12-19.

Listing 12-19. The Reset Function

void GPTScriptEngine::Reset()
{
  // Remove all modules from script engine
  for (size_t i = 0; i < m_scriptModules.size(); ++i)
  {
    std::stringstream moduleStream;
    moduleStream << m_scriptModules[i].id;
    std::string modName = moduleStream.str();
    m_pScriptEngine->DiscardModule(modName.c_str());
  }

  // Clear modules
  m_scriptModules.clear();
}

The code that glues the script engine and the Java GUI together is in GPTMain.cpp. It contains the two functions declared in GPTJNILib.java: Initialize and Updateas well as a declaration for the global variable gScriptEngine. Due to JNI naming conventions, function signatures are longer than you may be used to. They start with “Java” and then include the package name, followed by the class name, ending with the function name, all separated by underscores.

The Initialize function takes a container of game objects, as sent by the scene player. It resets the script system to start from scratch and proceeds to add each object. Because game objects are sent in the form of an ArrayList of GPTGameObject Java instances, special JNI functions are used to get individual instances and their property values. For example, to get the color of the first object in the gameObjects container, you would write the code shown in Listing 12-20.

Listing 12-20. How to Get a Property Value from a Java Object in C/C++

jclass editorGameObjClass =
env->FindClass("com/gametoolgems/gpt/GPTEditorGameObject");
 
jfieldID gameObjColorField =
env->GetFieldID(editorGameObjClass, "color", "I");
 
jobject gameObj =
(jobject)env->GetObjectArrayElement(gameObjects, 0);
 
jint objColor = env->GetIntField(gameObj, gameObjColorField);

First, you get a handle on the Java code’s GPTEditorGameObject class. The fully qualified name is required, which also includes the package name. Second, you get a handle on the color property of the class by calling the GetFieldID function with the class handle and the name of the property. The last parameter in the GetFieldID function is a way to tell it what the data type is. The “I” means it’s an integer type. Next, you use the GetObjectArrayElement function and send it an index of 0 to get the first object in the array list. And last, the color value is obtained by calling the GetIntField function and passing it the object and the handle to the color property. The full code for the Initialize function is shown in Listing 12-21.

Listing 12-21. JNI Initialize Function

JNIEXPORT void JNICALL
Java_com_gametoolgems_gpt_GPTJNILib_Initialize(JNIEnv * env,
  jobject obj, jobjectArray objects)
{
  // Reset script engine
  gScriptEngine.Reset();

  //  Initialize script system with scene
  const char* className =
    "com/gametoolgems/gpt/GPTEditorGameObject";
 
  jclass editorGameObjClass = env->FindClass(className);
 
  jfieldID goId =
    env->GetFieldID(editorGameObjClass, "id", "I");
 
  jfieldID goX =
    env->GetFieldID(editorGameObjClass, "x", "F");
 
  jfieldID goY =
    env->GetFieldID(editorGameObjClass, "y", "F");
 
  jfieldID goColor =
    env->GetFieldID(editorGameObjClass, "color", "I");
 
  jfieldID goScript =
    env->GetFieldID(editorGameObjClass,
      "script", "Ljava/lang/String;");

  jsize len = env->GetArrayLength(objects);
  for (int i = 0; i < len; ++i)
  {
    jobject gameObj =
    (jobject)env->GetObjectArrayElement(objects, i);
 
    jint objId =
      env->GetIntField(gameObj, goId);
 
    jfloat objX =
      env->GetFloatField(gameObj, goX);
 
    jfloat objY =
      env->GetFloatField(gameObj, goY);
 
    jint objColor =
      env->GetIntField(gameObj, goColor);
 
    jstring objScript =
      (jstring)env->GetObjectField(gameObj, goScript);

    const char* scriptCode =
      env->GetStringUTFChars(objScript, NULL);
    gScriptEngine.AddGameObject(
      objId, objX, objY, objColor, scriptCode);

    // Release string
    env->ReleaseStringUTFChars(objScript, scriptCode);
  }
}

The Update function receives the number of seconds elapsed since the last time you updated, and an empty Java ArrayList, which will be filled with GPTGameObject instances. The script system’s Update method is called to run one step on every object, and then the GetUpdatedObjects function is called to collect the updated objects in a std::vector. For every element in the vector, a GPTGameObject instance will be created and added to the ArrayList. And as you saw earlier in the Java GUI code, this is how you draw the updated scene (see Listing 12-22).

Listing 12-22. JNI Update Function

JNIEXPORT void JNICALL
Java_com_gametoolgems_gpt_GPTJNILib_Update(
JNIEnv * env, jobject obj, jfloat deltaTime,
jobject objArrayList)
{
  // Update game objects and script system
  gScriptEngine.Update(deltaTime);

  // Get updated game objects
  std::vector<GameObject> gameObjects;
gScriptEngine.GetUpdatedObjects(gameObjects);
 
// Send updated objects to Java
const char* className =
  "com/gametoolgems/gpt/GPTGameObject";

jclass gameObjectClass =
  env->FindClass(className);
 
jmethodID gameObjectCtor =
  env->GetMethodID(gameObjectClass,
    "<init>", "(IFFI)V");
 
jclass arrayListClass =
  env->FindClass("java/util/ArrayList");
 
jmethodID arrayListCtor =
  env->GetMethodID(arrayListClass, "<init>", "()V");
 
jmethodID addFunction =
  env->GetMethodID(arrayListClass,
    "add", "(Ljava/lang/Object;)Z");
 
for (size_t i = 0; i < gameObjects.size(); ++i)
{
  const GameObject& gameObj = gameObjects[i];
  jobject newGameObj =
    env->NewObject(gameObjectClass,
      gameObjectCtor, gameObj.id,
      gameObj.x, gameObj.y, gameObj.color
      );
 
  jboolean result =
  env->CallBooleanMethod(objArrayList,
    addFunction, newGameObj);
}
}

Test Drive

Hopefully, by working with the code, you now realize the power of this prototyping system. However, the fun part is trying it out, so you will create a very simple scene in a matter of minutes.

Build both the native code and Java code, and run the application on your mobile device or emulator. You should see the scene editor come up. Tap on the colored circle button several times until the green circle appears. Then tap once in the empty scene area, preferably near the top. A green object should appear, and it should have a gray outline, indicating it’s selected. Now tap on the Edit Script button. The script editor should come up, with a default script that contains an empty OnUpdate function. You will write code that makes the object move, given a constant velocity. The code should look like that in Listing 12-23.

Listing 12-23. AngelScript Code for the One-Minute Scene

float velocity = 100.0f;
 
void OnUpdate(float dt)
{
  me.y += velocity * dt;
}

Now press the Back button on your device twice, once to hide the keyboard and a second time to go back to the scene editor. Tap on the Play Scene button to go to the scene player. When there, press Play and watch the magic happen!

What’s Next

While the prototyping tool you just created gives you some power, there are many features it still lacks. Previously I mentioned sending touch input to the scripted objects, creating and destroying objects, and the ability to detect collisions with other objects. Additional features include creating object prefabs before placing them as object instances, using textured sprites instead of colored circles, and a visual scripting system. The important takeaway from this chapter is that the prototyping tool should be bound only by the limits of your own imagination. In other words, the tool’s features can grow with every new idea. I invite you to improve it, or reimagine it completely. Hopefully this chapter has sparked an interest in the internal workings of authoring tools, and given you the knowledge to continue learning.

________________

1Andreas Jönsson. “Angelscript.” Accessed November 12, 2011. www.angelcode.com/angelscript/.

2Oracle Corporation. “JNI APIs and Developer Guides.” Accessed April 9, 2014. http://docs.oracle.com/javase/8/docs/technotes/guides/jni/index.html.

3Google Inc. “Codenames, Tags, and Build Numbers.” Accessed April 9, 2014. http://source.android.com/source/build-numbers.html.

4Google Inc. “Android NDK.” Accessed November 12, 2011. http://developer.android.com/tools/sdk/ndk/index.html.

5Google Inc. “ADT Plug-in.” Accessed November 12, 2011. http://developer.android.com/tools/sdk/eclipse-adt.html.

6Red Hat. “Cygwin.” Accessed November 12, 2011. www.cygwin.com/.

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

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