Mobile game engine introduction
For new employee training
Document ID :
|
|
Department :
|
SW
|
Author(s) :
|
|
Editor(s) :
|
|
Date :
|
|
Version :
|
V0.002
|
Revision History
Version
|
Change Descriptions
|
Date
|
Editors
|
V
0.001
|
First draft
|
10/08/2004
|
Su Ning
|
V
0.002
|
|
10/14/2004
|
Su Ning
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Approval
Name
|
Function
|
Dept.
|
Date
|
Signature
|
<Reviewer’s name
>
|
<Reviewer’s function in this review>
|
|
<mm/dd/yyyy>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Table of Contents
This document is for new employee training.
It covers main structure of game state machine, graphics, controller input.
GD Game Design
MIDP Mobile Information Device Profile
GC Garbage Collection
OS Operating System
AI Artificial Intelligence
Mobile games are applications on mobile devices. Generally, we only use the fundamental application features. Accessory features, such as standard controls for UI, are not used in our games.
Fundamental features, I mentioned above, includes frame control (It may be a main loop / event callback to give your game a time piece for gaming. It also ensures time piece for system – key event, memory GC, etc.), graphics, key input, sound, etc.
For all fundamental features, the most important one is frame control. To implement a frame control mechanism, we should found the application entry / exit, and find out where the game engine starts, pauses / stop and how the game engine shared time pieces with system.
For example, a java mobile game is an MIDP application. Each MIDP application has a class derived from MIDlet (base class in j2me library), just like the following code from POPSOT.
public class Pop2MIDlet extends MIDlet
{
public Pop2MIDlet()
{
}
//----------------------------------------------------------
protected void startApp()
{
try
{
if (Game engine has not instantiated)
{
Game engine instantiates.
}
Game engine starts running. / Game engine resumes running.
}
catch(Exception e)
{
}
}
//----------------------------------------------------------
protected void pauseApp()
{
if (Game engine has instantiated)
{
Game engine pauses.
}
}
//----------------------------------------------------------
protected void destroyApp(boolean unconditional)
{
if (Game engine has instantiated)
{
Game engine destroys.
notifyDestroyed()
}
}
}
For each MIDP application, the entry point is class MIDlet. Mobile OS will make calling to MIDlet
à startApp to signal your application to run, and pauseApp to notify your application to pause, and destroyApp to notify to quit.
For each MIDP application, the exit point is the calling of notifyDestroyed, which notify the mobile OS to shutdown your application.
Following is figure to describe how to switch to start and stop your application.
Running state
(First time enters this state, is the entry of your game.)
|
Pause state
|
Destroy state
(You can notify OS to destroy you by notifyDestroyed, which might be the exit of your game)
|
pauseApp
|
startApp
|
destroyApp
|
destroyApp
|
Application Starts
|
startApp
|
(This is the figure of state machine of MIDlet.)
2.1.2 Game main states (Loop Control)
You may notice in the previous source code, when your application state is changed, your game engine’s state is also changed.
startApp
à Game engine starts running
pauseApp
à Game engine pause
destroyApp
à Game engine destroy
Generally, game engine has more states than running, pause, destroyed. How can we organize other many states from these three states? Generally we handle all other sub states in running state and we use a main loop / event handler to switch between run / pause / destroy states.
Following is the code piece from POPSOT
This is a main loop example, the pause state is handled by other approaches.
Public void run()
{
If not initialized
{
Do some thing initialize
}
While (state is RUNNING)
{
//Frame control
If current frame time < shortest frame time
{
//Here we give some time piece to system
Thread.yield() or Thread.sleep()
}
//Handle all other game sub states
Game engine state machine
}
//not RUNNING, destroy it
Call MIDlet
à
destroyApp
}
Following are some technical explanations for the above code pieces. Generally, game engine is derived from Runnable interface. The function “public void run()” must be implemented for the Runnable interface. We use sleep / yield, depending on different device, to control the time of each frame.
Main loop method: we setup a loop in run(). When game state change into game end, we quit from loop and game ends.
Game sub states are states not about game’s life cycle but about game content. In most games, we always have states such as splash or first screen to display a company logo, menu, game play, story telling etc.
Following is a sample code piece from POPSOT.
Firstly, in “public void run()”, we replace
//Handle all other game sub states
Game engine state machine
By a function calling
//Handle all other game sub states
udpateGameStates()
Then, here comes the source code to implement updateGameStates
Void updateGameStates()
{
Switch (current state)
{
Case STATE_GAMEPLAY:
Handle Gameplay Sub States
Break;
Case STATE_PAUSE:
Handle Pause Sub States
Break;
Case STATE_MENU:
Handle Menu Sub States
Break;
Case STATE_SPLASH:
Handle Splash Sub States
Break;
Case STATE_STORY:
Handle Story Sub States
Break;
}
}
Following is the figure for game sub state switching due to the game POPSOT
Splash state
|
Menu
State
|
Gameplay state
|
Story state
|
Pause state
|
Here we handle pause state in game running main state. In fact, the game is still running in a main state, but paused in game contents.
If we regard mobile game as a state machine, the most important state is STATE_GAMEPLAY.
In this state, player is enjoying the game. Generally, there is a background representing the environment and there are some “actors”, which are small objects like trees, animals, soldiers, and our hero of the game, on the background. The background and objects will be rendered on screen. And, some of the objects will do AI.
This chapter, “structure”, is to describe how to organize the rendering, AI, and life cycle of background and these game objects.
2.2.1 Implementation of game state machine
In the previous code
//Handle all other game sub states
updateGameStates
In fact we have more things to do than only update game sub state. We have to pass a Graphics object for rendering image on screen. So we use paint event callback to do render on screen. Sometimes, we also put state-update in paint event callback. Here is the sample code from POPSOT, the update part is not in paint(), and paint() did the render part.
Following is the “real world” source code from POPSOT.
public void run()
{
if (!m_inited)
{
setFullScreenMode(true);
m_inited = true;
m_gameState = Def.STATE_FIRST_SCREEN;
repaint();
serviceRepaints();
try{ Thread.sleep(1000); }catch(Exception e){}
gameInit();
}
while ( m_running != 0)
{
m_curFrameTime = System.currentTimeMillis();
updateState();
m_painted = 0;
repaint();
while (m_painted == 0)
{
Thread.yield();
}
///* time to sleep
m_lastFrameTime = System.currentTimeMillis() - m_curFrameTime;
while (m_lastFrameTime < m_timeRate)
{
Thread.yield();
m_lastFrameTimeSystem.currentTimeMillis() - m_curFrameTime;
}//*/
}
m_sound.m_running = false;
m_midlet.notifyDestroyed();
m_midlet.destroyApp(true);
}
In the main loop, “while (m_running != 0)”, the function “updateState” is update game sub state.
Following code
m_painted = 0;
repaint();
while (m_painted == 0)
{
Thread.yield();
}
Processing paint event callback. At the end of event callback function “public void paint()”, m_painted is set to non-zero, then exit from yield loop. This mechanism ensures after executing this part of code, the paint event callback function is called and return, that is, the paint process is finished.
The following code,
while (m_lastFrameTime < m_timeRate)
{
Thread.yield(); m_lastFrameTimeSystem.currentTimeMillis() - m_curFrameTime;
}
Limit the frame time of the game, when we still have spare time in this frame tick, we give up some time piece by “Thread.yield” to other system task handling.
2.2.2 Game play state
Here is a sequence for our task in game play state, which is the most important state.
Do AI
Render background
Render actors
Here is a piece of sample code from POPSOT
Do AI
public void updatePlayer()
{
//GE.m_updatedAI = 0;
Actor a;
for (int i = m_nActors - 1; i >= 0; i--)
{
a = m_actors[i];
if (inActiveZone(m_camX, m_camY, i))
{
if (a == null)
a = allocActor(i);
}
else if (a != null && !a.m_flag[2])
{
freeActor(i);
continue;
}
if (a != null)
{
//GE.m_updatedAI++;
a.updateAI(m_camX, m_camY);
}
}
updateCamera(false);
}
In source code, Player means “actor manager” which we could discuss later.
This function’s main purpose is to loop in all objects, and give them chances to do AI. In the source code, this chance is “a.updateAI(m_camX, m_camY)”.
And at the last line, “updateCamera” is to update game region on background. In most games, our screen contains only part of the whole level / world. “Camera” is to determine which part of level / world we are, then render this part background and actors in it.
Render background
void mapFastDraw(int camX, int camY)
This function is to render background with two parameters indicating the camera position. The region rendered on screen will be the part of that camera “see” by the given position.
The content of this function is omitted. When rendering background fast and memory saving, we need some special tricks which will be discussed later.
Render actors
public void displayPlayer()
{
// draw actors
Actor a;
for (int i = 0; i < m_nActors; i++)
{
a = m_actors[i];
if (a != null && a.m_flag[3])
a.draw(m_camX, m_camY-Def.PF_TOP);
}
// Lyman DEBUG
//GE.m_piece[5] = System.currentTimeMillis() - GE.m_piece[3] - GE.m_piece[4];
// draw interface
GE.m_g.setClip(0, 0, Def.SCREEN_WIDTH, Def.SCREEN_HEIGHT);
if (m_prince != null && m_showBar)
drawBloodEnergy();
}
In the above code, Player also means actor manager. It loops in all actors and render on screen.
In POPSOT, the main function of actor management is encapsulated into class Player. It always contains following functions.
Camera management
Actors’ lifecycle management
Actors’ AI and rendering on screen
Since we have discussed actors’ AI and rendering on screen in the previous chapter. Here I am emphasis on the Actors’ lifecycle management and camera management.
following is code from POSOT.
public void initPlayer()
{
int i;
for (i = 0; i < m_anims.length; i++)
{
m_anims[i].loadImage();
}
// init camera
Actor a;
for (i = 0; i < m_nActors; i++)
{
a = m_actors[i];
if (a != null)
{
a.loadActor(m_originData, m_actorOffset[i]);
}
}
m_focusEnemy = null;
updateCamera(true);
// init actors
for (i = 0; i < m_nActors; i++)
{
if (inActiveZone(m_camX, m_camY, i))
{
if (m_actors[i] == null)
allocActor(i);
}
}
// init prince
m_hpLast = 0;
m_energyLast = 0;
m_prince.checkEnvironment(true);
}
During the initialization of actor manager, these tasks must be done.
First several lines, load some necessary resource, image for animation, actor attribute data, etc.
Next, initialize camera position by calling “updateCamera(true)”, to put camera in the start position of each level.
Then activate actors in “active Zone”.
An active zone, is a region (a little bit bigger than screen) in the whole level / world. Actors in this region will do AI. Actors out of this region will not do AI. During camera moving, active zone is changing. Actors are activated / deactivated. This mechanism is to save memory and time piece. It is based on a fact that active actor costs more memory and CPU time than de-active actor.
Following figure describe the idea of an active zone.
![](https://p-blog.csdn.net/images/p_blog_csdn_net/c30gcrk/99c4b9833cc549da98e72c65c5ef07e9.png)
In previous
source code, the function updatePlayer and displayPlayer is the two tasks in update of actor manager.
updatePlayer
Loops in all actors
If actor is in active zone, and not allocated, allocate actor.
If actor is not in active zone, and allocated, free actor.
In the source code, allocate / free equals to activate and deactivate we mentioned before.
displayPlayer
Loops in all actors
If actor is in camera, render this actor
If actor is total out of camera, skip this actor.
Following is a piece of code from POPSOT
public void freePlayer()
{
int i;
if (m_anims != null)
{
for (i = 0; i < m_anims.length; i++)
{
m_anims[i].m_image = null;
}
}
if (m_actors != null)
{
for (i = 0; i < m_nActors; i++)
{
m_actors[i] = null;
}
m_actors = null;
}
activeBoxes = null;
m_actorOffset = null;
m_nActors = 0;
m_originData = null;
m_recordData = null;
}
Free all related resources and deactivated all actors.
Camera management includes two main tasks, to move camera around a focus player – mostly the hero of game, and to move camera smoothly. And it will ensure camera moving in the level / world boundary.
Following is code to move camera to focus player
static void updateCamera(boolean init)
{
int offsetX = -1;
int offsetY = -1;
if (m_focusEnemy != null)
{
offsetX = m_prince.m_posX - m_focusEnemy.m_posX >> 8;
offsetY = m_prince.m_posY - m_focusEnemy.m_posY >> 8;
if (offsetX < 0) offsetX = -offsetX;
if (offsetY < 0) offsetY = -offsetX;
if (offsetX < (Def.PF_WIDTH * 3 / 4) && offsetY < (Def.PF_HEIGHT * 3 / 4))
{
offsetX = (((m_prince.m_posX + m_focusEnemy.m_posX)/2) >> 8) - Def.PF_WIDTH / 2 - m_camX;
offsetY = (((m_prince.m_posY + m_focusEnemy.m_posY)/2) >> 8) + (m_prince.getColBox()[3] + m_prince.getColBox()[1])/2 - Def.PF_HEIGHT / 2 - m_camY;
}
else
{
offsetX = -1;
offsetY = -1;
}
}
if (offsetX == -1)
{
offsetX = (m_prince.m_posX >> 8) - Def.PF_WIDTH / 2;
offsetY = (m_prince.m_posY >> 8) + (m_prince.getColBox()[3] + m_prince.getColBox()[1])/2 - Def.PF_HEIGHT / 2;
offsetX += m_prince.m_camOffsetX - m_camX;
offsetY += m_prince.m_camOffsetY - m_camY;
}
if (init)
{
GE.m_updateBg = true;
m_camX += offsetX;
m_camY += offsetY;
}
else if (!m_prince.blocked)
{
m_camX += getCamSmoothMove(offsetX, m_prince.m_vX >> 8);
m_camY += getCamSmoothMove(offsetY, m_prince.m_vY >> 8);
if (m_shakeCamera > 0)
{
m_camY += (m_shakeCamera % 2 == 1) ? 8 : -8;
m_shakeCamera--;
}
}
if (m_camX < 0) m_camX = 0;
if (m_camX > GE.m_bgWidth*GE.m_tileWidth - Def.PF_WIDTH) m_camX = GE.m_bgWidth*GE.m_tileWidth - Def.PF_WIDTH;
if (m_camY < 0) m_camY = 0;
if (m_camY > GE.m_bgHeight * GE.m_tileHeight - Def.PF_HEIGHT) m_camY = GE.m_bgHeight*GE.m_tileHeight - Def.PF_HEIGHT;
}
In the first several lines, it determines a focus actor. In most cases, it would be a hero (In POSOT, he is a prince), but it also could be an enemy that the game player would like to notice.
Then it calculates an offset of the camera. This offset is calculated according to the focus actor’s position.
Before adjust camera position, by modifying m_camX and m_camY, it calls getCamSmoothMove to ensure a smoothly moving of camera.
At last, it limits the camera postion inside the boundary of level / world.
Following is the code to make camera movement smoothly
static private int getCamSmoothMove(int offset, int speed)
{
if (Math.abs(offset) > Math.abs(speed))
{
if (speed != 0 && offset/speed > 0)
offset -= speed;
if (offset > 0)
{
if (offset < 8)
offset = 1;
else if (offset < 32)
offset = 2;
else
offset = 4;
if (speed > 0)
offset += speed;
}
else
{
if (offset > -8)
offset = -1;
else if (offset > -32)
offset = -2;
else
offset = -4;
if (speed < 0)
offset += speed;
}
}
return offset;
}
This method limit the moving speed of camera to a smaller value each frame tick. The smaller offset value (1, 2, 4) is also depends on the target offset range of camera. For each frame tick, the camera only moves a small value (1, 2, 4 pixels on screen). Although the target offset would be big, user will feel the camera is moving smoothly.
In previous chapter, update stage of actor manager, each actor does its own AI if necessary by calling Actor
à updateAI
Generally, AI is implemented as a state machine. Following is the suede code from POPSOT, AI of the hero – prince (because the real code of this part is too long).
public boolean updateAI(int camX, int camY)
{
Switch (state of prince)
{
Case PRINCE_STAND:
//make prince to stand on ground
//handle key input, switch to other state
Break;
Case PRINCE_RUN:
//make prince to stand if facing wall
//make prince to drop if out of edge
//handle key input, switch to other state
Break;
Case PRINCE_JUMP:
//make prince to stand on a higher level platform //make prince to drop if out of edge
//handle key input, switch to other state
Break;
Case PRINCE_STRIKE:
//attack enemy
//then make prince to stand / run
Break;
Case PRINCE_DIE:
//game over
Break;
}
If (state is updated)
{
State switching from one state to another
}
}
The comments is also not the same as real AI. It is only a sample of AI by state machine.
In the previous
source code, “repaint()” is called to request a paint event. Then wait until a paint event callback is finished.
A paint event callback function is derived from Canvas
à paint. “public void paint(Graphics g)”. A graphics object passed to callback function as a parameter. Then we can use this graphics object to paint on screen.
In MIDP2.0, we can also get a graphics object fro painting on screen by calling GameCanvas
à getGraphics in main loop (the run() function) instead of by getting it by event scheduler and event callback.
A paint function always looks like
public void paint(Graphics g)
{
if (g==null || !m_inited)
return;
//do painting on screen
m_painted = 1;
}
There are several tasks we need to do in “doing painting on screen”.
Simple image / strings
Background
Actors
Drawing Simple image / strings is simply calling method of a graphics object. So we will be focus on background and actors which are rendered as animations.
Background is indicating the environment and atmosphere of level / world by drawing some pictures on the screen and actors moving on it. The pictures could be trees, rocks on sands or a side view of a building.
The simplest approach is to draw a portion of a big picture on the screen to indicate the environment.
Ø The advantage of this method is fast – it does not need further calculations by simply copying the whole image on screen, good for representing the creation of artists – it contains a whole picture with full details and creation from artist.
Ø The disadvantage of this method is the limitation of the whole image size. If the game is like a RPG, the whole map would be HUGE enough to containing the whole world. In most cases it becomes impossible.
A better idea is to tile a whole map with smaller bricks.
Following are some samples of tiled map.
Map is a big size palace from side view. It is a level from POSOT.
![](https://p-blog.csdn.net/images/p_blog_csdn_net/c30gcrk/65b692eca43543c7ad7a5303ee1772a1.png)
Image for bricks is a very small picture. But we use it to tile the whole map.
![](https://p-blog.csdn.net/images/p_blog_csdn_net/c30gcrk/532a7f43094e418e93d102a5c1dfc99e.png)
Ø The advantage of tiled map is that we can present a very HUGE map with a relatively small size of image.
Ø The disadvantage of tiled map is speed. To repaint full screen, we have to tile all small bricks on full screen in each frame tick. In most devices, calling to draw image function has overhead – it cost special time each time calling. So tile so many brick on full screen each frame will cost much time and game running speed will be slow.
Practically, we use an evolution version of tiled map. The disadvantage of tiled map is the speed. The speed is slow because of updating full screen each frame tick with calling image drawing function for each tile, then cost much time. We can avoid calling too many times of image drawing function and reduce the update region each frame tick. That is, we only redraw updated region of the map on screen.
Original screen
|
Screen position after 1 frame tick camera movement
|
We only need to update this part.
|
We won’t update this big part on screen. Since it is as same as previous frame tick.
|
Following function from POPSOT is for fast tiled map.
void mapFastDraw(int camX, int camY)
Please check source code for detailed algorithm.
Ø The advantage of fast tiled map is the speed is much better than original tiled map. Since for each frame tick, the update region and drawing time is reduced significantly. And it also can present a big map with relatively small image.
Ø The disadvantage of fast tiled map is memory cost. Although its memory cost is better than full image drawing, compared to tiled map, it must allocate an additional memory buffer for tiled map of previous screen.
When we finish rendering background, we are going to draw actors on background. Generally, actors can animate. As a plane, she can dive, rotate, shoot etc. As a prince, he can run, jump, stand. To make it animated, we must draw each frame of animation for each continuous frame tick.
Here are two common methods for animation.
- The simplest method is using one image for each frame. Here is a example.
This is the image for animation of enemy planes for a shooting game. It can fly to you, turn left / right, and rotate up. For each frame, we draw a rectangle (containing a small plane) of image. While iterating between different small planes, we make the plane “looks” animated.
- An evolutionary way is using multiple images for one frame. Among the small pieces of image composed into one frame, many small pieces could be reused in many frames. So the total image size for the animation is reduced. For example, to make a prince animation, we use several pieces of image of legs, arms and heads to compose many different frames representing different posture of prince. Here is an example.
This is the image of animation from POPSOT. Many small pieces of image are composed to all prince animation frames, including run, stand, jump, roll, strike, etc. The whole image size is much smaller than use one image for each frames. For each frame, we draw several small pieces of image composed into an animation frame. While iterating between different frames, we make the prince “looks” animated.
Generally we use event callback function derived from Canvas
à keyPressed and Canvas
à keyReleased to handle key pad messages. When key is pressed / released, corresponding event callback will be called by mobile OS with a key code as parameter.
The key handling contains two phase. First step is to translate the native key code to game key value. The purpose is to add a middle layer between device and key handling functions. And future immigration works easier.
Here is the sample code from POPSOT
protected void keyPressed( int code )
{
//m_keycode = code;
if (m_blockKey) return;
byte key = Def.KEY_NONE;
switch( code )
{
case Canvas.KEY_NUM2: // UP
case Def.PADDLE_UP: //Canvas.UP:
key = Def.KEY_U;
break;
case Canvas.KEY_NUM4: // LEFT
case Def.PADDLE_LEFT:
key = Def.KEY_L;
break;
case Canvas.KEY_NUM5: //DOWN
case Def.PADDLE_FIRE:
m_lastInput = Def.KEY_NONE;
if (m_gameState == Def.STATE_RUN)
key = Def.KEY_ATTACK;
else
key = Def.KEY_SOFT2;
break;
case Canvas.KEY_NUM6: // RIGHT
case Def.PADDLE_RIGHT:
key = Def.KEY_R;
break;
case Canvas.KEY_NUM8: //DOWN
case Def.PADDLE_DOWN: //Canvas.DOWN:
key = Def.KEY_D;
break;
case Def.PADDLE_SOFT1: //SOFT1
key = Def.KEY_SOFT1;
break;
case Def.PADDLE_SOFT2: //SOFT2
key = Def.KEY_SOFT2;
break;
case Def.PADDLE_SOFT3: //SOFT3
key = Def.KEY_SOFT3;
break;
}
if (key > Def.KEY_NONE)
{
//Handle Translated Key Code
}
}
The second step is the key handling functions. These functions provide interface to determine whether a key is pressed, released or hold, how long is the interval between two keypressed.
Following function is the real key handler.
static void updateKeys()
This function set keys to different status value. Then we can determine the key status by checking these values.
This document introduces the basic structure for a mobile game engine.
Some important issues, like sound and resource management, are not included. Because, firstly, they are quite different due to different devices, secondly, they are relatively easy to understand (their functions are not game-specific). I recommend we introduce these contents in special topic / practical working.
Some of the topics in the document, like graphics and key input, could be discussed later with more details. Because each topic contains a lot of tricks and we only emphases on main structure and basic game features in this document, document in specific field could be provided.