Source:
Direct:
Table of Contents
This article will explain how to create a simple game on Android platform using SurfaceView
class only by managed Java code.
We will examine the basic concepts and create a display interaction by touch events.
We are going to implement the fundamental stage of a 2D bubble game, which includes a cannon shooting bubbles to our touches direction.
Your game will look something like this:
Basic Game concept
We need to establish several modules in our program:
1. Get the input data from the user.
2. Store that data somewhere in our program.
3. Display the result on the screen.
Each figure that is displayed on the screen of our Android device is a java object with its own logic. In our game we have 3 objects of that type: Cannon, Bubble and Aim.
Every object has its x and y coordinates that identifies its location on the screen. Objects that constantly move, such as bubbles, keep their deltas which identify their direction.
We are going to create an activity that will listen to touch events (user inputs) and set its content view as custom view that we will implement afterwards.
Let’s examine our GameActivity
onCreate() method:
@Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
SurfaceView view = new GameView(this, AppConstants.GetEngine());
setContentView(view);
getActionBar().hide();
}
Regularly we will see something like this in onCreate
method:
setContentView(R.layout.main_activity);
This method sets the xml layout as the content view of the activity, which will be generated to a View
automatically.
In our case, we are setting the content view as our custom View class, because we need to implement our own methods and modifications on the View
that cannot be performed by the regular convention.
Next, we will listen to touch events by overriding the onTouchEvent(MotionEvent event)
method and accordingly to MotionEvent
value we’ll call our methods.
private void OnActionMove(MotionEvent event)
{
int x = (int)event.getX();
int y = (int)event.getY();
if(GetIfTouchInTheZone(x, y))
{
AppConstants.GetEngine().SetCannonRotaion(x, y);
}
AppConstants.GetEngine().SetLastTouch(event.getX(), event.getY());
}
private void OnActionUp(MotionEvent event)
{
int x = (int)event.getX();
int y = (int)event.getY();
if(GetIfTouchInTheZone(x, y))
{
AppConstants.GetEngine().SetCannonRotaion(x, y);
AppConstants.GetEngine().SetLastTouch(FingerAim.DO_NOT_DRAW_X
,FingerAim.DO_NOT_DRAW_Y);
AppConstants.GetEngine().CreateNewBubble(x,y);
}
}
private void OnActionDown(MotionEvent event)
{
AppConstants.GetEngine()
.SetLastTouch(event.getX(), event.getY());
}
These methods are invoked as a user touches the screen of our activity.
They call appropriate methods in our GameEngine
object. That will change our business logic and create an interaction between the user and our application.
GameView
class extends SurficeView
that extends View
class, that’s why we could set it as our activity content view.
This class implements the logic for our display.
When created (when called new in our GameActvity
), GameView
initializes and starts the DisplayThread
object.
More on SurficeView
class:
http://developer.android.com/reference/android/view/SurfaceView.html
Our GameView
implements SurfaceHolder.Callback
interface:
@Override
public void surfaceChanged(SurfaceHolder arg0, int arg1, int arg2, int arg3)
{
}
In this method, we don’t do anything, since it’s not in our interest. But, we have to implement it in our class because it is a part of the interface.
All the overridden methods in GameView are part of the SurfaceHolder.Callback interface
.
@Override
public void surfaceCreated(SurfaceHolder arg0)
{
if(!_displayThread.IsRunning())
{
_displayThread = new DisplayThread(getHolder(), _context);
_displayThread.start();
}
else
{
_displayThread.start();
}
}
surfaceCreated
()
is invoked when GameActivity
content view is initialized, when called OnCreate()
, onStart()
or onResume()
methods, it simply starts the DisplayThread
.
@Override
public void surfaceDestroyed(SurfaceHolder arg0)
{
_displayThread.SetIsRunning(false);
AppConstants.StopThread(_displayThread);
}
SurfaceDestroyed
is called when GameActivity
OnPause(), OnStop() or OnDestroy() life cycle methods are called, this method stops the DisplayThread
.
DisplayThread
is the one that refreshes our screen. This magic happens in its run()
method.
@Override
public void run()
{
while (_isOnRun)
{
AppConstants.GetEngine().Update();
Canvas canvas = _surfaceHolder.lockCanvas(null);
if (canvas != null)
{
synchronized (_surfaceHolder)
{
canvas.drawRect(0, 0,
canvas.getWidth(),canvas.getHeight(), _backgroundPaint);
AppConstants.GetEngine().Draw(canvas);
}
_surfaceHolder.unlockCanvasAndPost(canvas);
}
try
{
Thread.sleep(DELAY);
}
catch (InterruptedException ex)
{
}
}
}
run()
method will loop in its while
loop as long as the boolean _isOnRun
variable is not set to false, which happens only by invoking the SetIsRunning(boolean state)
method.
SetIsRunning(boolean state)
is called only from SurfaceHolder.Callback
interface methods that implemented in our GameView
class.
First thing that run()
method does as it enters the while
loop, is to call the Update()
method in the GameEngine
object. That will update our business logic (in our case advance the balloons).
Next it locks the canvas (from our SurficeView
class), paints the whole view with the black background and passes the canvas to GameEngine Draw()
method. That will display our objects with updated parameters on our display.
Finally it unlocks the canvas, and goes to sleep for a specified amount of time, that calls fps (frames per second).
Our brain process about 20 frames per second, our code refreshes the screen every 45 millisecond, which makes it 22 fps.
In our implementation we are calling Thread.sleep()
with a hardcoded value, expecting it to stop for a while and then continue the logic execution.
We need to be aware of the fact that the time that it takes to render and update the game is not consistent.
For example, if you have one bubble to advance, it wouldn’t take the same time as to advance 550 bubbles on the screen. That makes our game rendering not smooth, since loop execution time is different from one another.
This problem can be solved by taking timestamp at the beginning of the loop and subtracting it from the sleep time, which will make our each loop execution time the same.
*I didn’t include that solution in our DisplayThread
implementation because I wanted to keep it simple.
This object is responsible for all game business logic, it keeps the instances of all displayed objects (in our case: Bubbles, Aim and the Cannon).
public void Update()
{
AdvanceBubbles();
}
Update()
method updates all our game’s objects, it advances the bubbles. However, it is not updating the Cannon
object, since its rotation value is changes only when the user touches the screen.
public void Draw(Canvas canvas)
{
DrawCanon(canvas);
DrawBubles(canvas);
DrawAim(canvas);
}
Draw(Canvas canvas)
method is called after the Update()
is called from the DisplayThread
class. Draw(Canvas canvas)
method is drawing all objects that are relevant to the game display, on the given canvas.
public void SetCannonRotaion(int touch_x, int touch_y)
{
float cannonRotation = RotationHandler
.CannonRotationByTouch(touch_x, touch_y, _cannon);
_cannon.SetRotation(cannonRotation);
}
SetCannonRotaion()
method is called from the GameActivity when touch events occurred.
We are rotating the bitmap in direct relation to the touch event coordinates.
The first stage is the user input, which invokes SetCannonRotation()
in the GameEngine
that calls RotationHandler.CannonRotationByTouch()
and _cannon.SetRotation()
with the given result.
public static float CannonRotationByTouch(int touch_x, int touch_y, Cannon cannon)
{
float result = cannon.GetRotation();
if(CheckIfTouchIsInTheZone(touch_x, touch_y, cannon))
{
if(CheckIsOnLeftSideScreen(touch_x))
{
int Opposite = touch_x - cannon.GetX();
int Adjacent = cannon.GetY() - touch_y;
double angle = Math.atan2(Opposite, Adjacent);
result = (float)Math.toDegrees(angle);
}
else
{
int Opposite = cannon.GetX() - touch_x;
int Adjacent = cannon.GetY() - touch_y;
double angle = Math.atan2(Opposite, Adjacent);
result = ANGLE_360 - (float)Math.toDegrees(angle) ;
}
}
return result;
}
CannonRotationByTouch()
method uses a little bit of geometry, it identifies whether the touch occurred on the right or on the left side of the cannon, and accordingly to its location, calculates the angle.
We are using tangent to calculate the angle, since we can calculate the adjacent and opposite length of the created right triangle between the cannon location and the touch coordinates.
Notice that as we rotate the cannon bitmap using Matrix object:
public static Bitmap RotateBitmap(Bitmap source, float angle)
{
Matrix matrix = new Matrix();
matrix.postRotate(angle);
return Bitmap.createBitmap
(
source,
0, 0,
source.getWidth(),
source.getHeight(),
matrix,
true
);
}
We are fitting the rotated cannon to our original width and height. That’s why our cannon (android icon) is getting smaller on bitmap rotation.
That is not right to do so; we should fit the new dimensions of the rotated image appropriately. We did not implement it here, just to simplify our code.
public void CreateNewBubble(int touchX, int touchY)
{
synchronized (_sync)
{
_bubles.add
(
new Bubble
(
_cannon.GetX(),
_cannon.GetY(),
_cannon.GetRotation(),
touchX,
touchY
)
);
}
}
CreateNewBubble()
is called on touch event as well, it creates new Bubble object and added to the bubble list, on which the Update()
and Draw()
method will iterate.
We created a very basic game, of course there is much more that we could implement, right now it’s not a very attractive game for a user. But if you understood the basic idea of the game loop, you can continue to develop on that platform as you wish.
It can be easily changed to any other 2D game concept, keeping the game loop fundamentals as is, by changing only the GameEngine
implementation of the Update
and Draw
methods and involved objects.