Upcoming Events
Unite 2010
11/10 - 11/12 @ Montréal, Canada

GDC China
12/5 - 12/7 @ Shanghai, China

Asia Game Show 2010
12/24 - 12/27  

GDC 2011
2/28 - 3/4 @ San Francisco, CA

More events...
Quick Stats
111 people currently visiting GDNet.
2406 articles in the reference section.

Help us fight cancer!
Join SETI Team GDNet!
Link to us Events 4 Gamers
Intel sponsors gamedev.net search:

An Introduction to BREW and OpenGL ES


The best laid plans...

Coding for limited devices like mobile phones can be a nightmare, especially if you come from a PC background and are used to luxuries like having more than a few hundred kb of heap, and more than a few hundred bytes of stack space.

Although it must be a Design Patterns advocates wildest fantasy, there is no static or global data on BREW. Also, BREW is completly event driven. Unlike "normal" programming where you typically have a while(..) loop to do your stuff, with BREW you can only respond to events like key presses or timers going off. There's also no floating point math, GL ES expected its values in 16.16 fixed point format. I'll address each of these in turn.

Storage space

So with no global or static data, where do we store our variables? BREW will store a single struct for us, which must first contain an AEEAplet structure, but can then contain any other data we want. Check out the main .c file the app wizard created for you. Right at the top is a structure named after your application, and in the AEEClsCreateInstance function is a call to AEEApplet_New which allocates heap space for it. BREW will look after a pointer to this data for us, and will pass it to us as a parameter to most things. I am going to refer to this as "the global BREW structure".

Some people like to just put all their data straight into that structure. However I prefer a slightly more oo approach.

Assuming you are going to write more than one BREW application you want to structure your startup/shutdown/event handling code into a shell so you dont have to rewrite it for every single application you create. First, change your main .c file to a .cpp file so it compiles as C++ (else you will get errors using classes). Create a class called Game with functions boolean Create(), void Destroy() and void Tick(int timeElapsed). Add an instance of Game into your global BREW data structure, right after AEEApplet, and remove the other data from the struct. I have also added int mOldTime which will will use later to track the elapsed time between frames.

// From test_project1.cpp
struct test_project1
{
  AEEApplet  a;         // The compulsory applet structure
  Game       mGame;     // Our game class
  int        mOldTime;  // used to track the speed we are running at
};
// From Game.h
class Game
{
private:
  IShell *       mShell;
  IDisplay *     mDisplay;

  AEEDeviceInfo  mDeviceInfo;

public:
  boolean Create(IShell * shell, IDisplay * display);
  void Destroy();

  void Tick(int timeElapsed);

  IShell * GetShell()      { return mShell;    }
  IDisplay * GetDisplay()  { return mDisplay;    }

  int GetWidth()           { return mDeviceInfo.cxScreen;  }
  int GetHeight()          { return mDeviceInfo.cyScreen;  }
};

The AEEDeviceinfo structure contains various information about the current phone and operating environment, most importantly for now it contains the width and height of the screen. Given that virtually all phones have different sized screens you should try and adapt to the screen size at run time. That way your program will have a chance to work on several phones without recompiling.

// From Game.cpp
boolean Game::Create(IShell * shell, IDisplay * display)
{
  mShell = shell;
  mDisplay = display;

  mDeviceInfo.wStructSize = sizeof(mDeviceInfo);
  ISHELL_GetDeviceInfo(mShell, &mDeviceInfo);

  DBGPRINTF(" *** Width %d, Height %d", GetWidth(), GetHeight());

  return TRUE;
}

void Game::Destroy()
{
}

void Game::Tick(int timeElapsed)
{
  // Uncomment this if you want proof the timer callback is working
  //DBGPRINTF("TICK! %d", timeElapsed);
}

The DBGPRINTF function to output text, either to the Visual C++ output pane if you are running in the debugger, or to a window within the emulator. To get access to it you need to include AEEStdLib.h. For now Destroy() doesn't do anything, as you add more functionality you can use it to clean up any resources you allocate.

Now to wire these up. Replace the contents of test_project1_InitAppData with a call to Game::Create, and make a call to Game::Destroy in test_project1_FreeAppData. Both of these functions are passed a pointer to the global BREW data structure, so you have easy access to the instance of your Game class. The other parameters you need are available through the AEEApplet stored within the global BREW structure.

Timers

Every thing in BREW is event based. If you were to try and remain in the startup function forever with a while loop after a few seconds the phone would reboot. BREW detects applications that have stopped responding (in its opinion) and forces a full reboot to try and clear the problem.

To get an application to run in a style resembling a real time game we use a timer to repetedly execute our game loop. BREW makes it really easy to set up a timer to callback a function of our choice with ISHELL_SetTimer. ISHELL_SetTimer takes four parameters, a pointer to the applications IShell (which is now contained in our Game class), the number of miliseconds in the future you want the function called, a pointer to a function to call, and finally a void * to some data you want passed to the callback.

The callback function needs to take a void pointer as a parameter and return void. I usually cast the address of global BREW structure to a void * and use that as my user data, that way in the callback function I can call Game::Tick(int timeElapsed). One thing to note is that timer callback functions are one shot wonders. If you want the callback to happen again you need to set the timer again.

// From test_project1.cpp
void SetTimer(test_project1 * data);

void timer_tick(void * data)
{
  test_project1 * tp1 = static_cast < test_project1 * > (data);

  int TimeElapsed = GETUPTIMEMS() - tp1->mOldTime;
  tp1->mOldTime = GETUPTIMEMS();

  tp1->mGame.Tick(TimeElapsed);

  SetTimer(tp1);
}

void SetTimer(test_project1 * data)
{
  int result = ISHELL_SetTimer(data->mGame.GetShell(), TickTime,
                               timer_tick, static_cast < void * > (data));

  if (result != SUCCESS)
  {
    DBGPRINTF(" *** SetTimer failed");
  }
}

GETUPTIMEMS() returns the number of miliseconds the phone has been on. TickTime is a constant that specifies how often (again in miliseconds) to call the main loop. Its calculated based on the FPS you want, like this:

// From test_project1.cpp
const int WantedFPS = 20;
const int TickTime = 1000 / WantedFPS;

The only thing that remains is to set the timer going for the first time. Do this from the event handler funtion in test_project1.cpp. The function is called test_project1_HandleEvent. Add a call to SetTimer(pMe); to the EVT_APP_START case. This will get called by BREW when (if) your create function has successfully completed.

Fixed Point Math

The ARM chips that power most BREW phones have no floating point units. Instead they use a format called 16.16 fixed point. The 16.16 refers to taking a 32 bit variable, using the first 16 bits for the whole part of a number, and the last 16 bits for the fractional part.

To convert an int to 16.16 format, simply shift it left 16 places. To convert it back, shift the other way. A full fixed point tutorial is outside the scope of this article, but there are plenty of resources on the internet. All you need for this article is a macro to convert numbers to fixed point.

// From Game.h
#define ITOFP(x) ((x)<<16)

Input

We recieve an event to our event handler function when a key is pressed, and another when it is released. Its up to us to track which keys are down at any given time.

To make things slightly more intresting the key codes used dont start at 0. They start at a constant called AVK_FIRST, and end at AVK_END. AVK is the prefix for the key codes too, so the 3 key would be AVK_3, the direction keys are AVK_UP, AVK_DOWN, etc. Check out aeevcodes.h for a complete list.

Lets add an array to our Game class to track the state of keys, and two functions to be called when we recieve key press and release events.

// From game.h
class Game
{
...
  boolean mKeysDown[AVK_LAST - AVK_FIRST];
...
  void KeyPressed(int keyCode);
  void KeyReleased(int keyCode);
...

In Game::Create loop through and set all the mKeysDown[..] to false so we start with a blank slate. The implementation of KeyPressed and KeyReleased is simple enough, just remember to take into consideration the key codes starting at AVK_FIRST not 0.

In your startup file, in your event handler function test_project1_HandleEvent, in the switch statement replace the whole case EVT_KEY with the following to route key events into the Game class.

// From test_project1.cpp
...
  case EVT_KEY_PRESS:
    pMe->mGame.KeyPressed(wParam);
    return TRUE;

  case EVT_KEY_RELEASE:
    pMe->mGame.KeyReleased(wParam);
    return TRUE;
...

Now in game code you can test if (mKeysDown[AVK_UP - AVK_FIRST] == TRUE). Again, don't forget to take into account the offset of AVK_FIRST. It would probably be best to write a wrapper function to do the test which handles the offset internally.

If you compile and run now you should see the code from Game::Create printing out the width and height of the screen to the Visual C++ output pane.



OpenGL ES


Contents
  Introduction
  Data storage, timers, math and input
  OpenGL ES

  Printable version
  Discuss this article