Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Mobile / iOS

Air Hockey

5.00/5 (25 votes)
10 Aug 2012BSD11 min read 98.9K   8.1K  
How to create an air hockey game for one player against the artificial intelligence for three mobile platforms with only one code?

Table of Contents

Introduction

Have you ever played Air Hockey? Air Hockey is a fairly popular game in game centers and other places where people hang out. Not that many people had a chance to try it as a pre-installed game on operating systems. The purpose of the game is to hit the puck to the opponent’s goal with a paddle. The puck is set on a special table, which produces a cushion of air on the play surface through tiny holes with the purpose of reducing friction and increasing play speed. The winner is the player who scores 7 points first.

In this article, we will create an air hockey game for three mobile platforms: iOS, Android and Bada using Moscrif SDK.

About the Game

Our goal is to create a single player game playing against artificial intelligence. When the game starts, the playground shows up right away. Menu will be created as a dialog window with only three buttons: New game, Continue and Quit (Quit button will not be available on iOS).

Graphic Design

Let’s start our development process by preparing the graphics. Our game is really simple so the graphics do not need to be too complicated either. They consist of the menu, playground backgrounds, buttons, paddles and the puck.

Image: graphic design

Image 1

The current mobile market offers devices with many various resolutions. To ensure the best appearance on every device, the graphics are created separately for all commonly used resolutions.

Physics Engine

The puck in the game behaves according to physical laws. It bounces from the paddles and barriers, and it moves with a small damping. To simulate this physical behavior, we used box2d physical engine which is supported by Moscrif SDK. This engine can be seen also in other platforms like Nintendo DS or Wii. The chapters about box2d world and bodies are similar as in my last article. If you read it in the last article, you can skip to Let’s start – start up file & resources chapter.

World

The world creates a background for all bodies, joints or contacts. The world has always width which is equivalent of 10 metres in real world. All objects and things in the world are scaled to ensure this width. The scale property says how many pixels are in one meter. The box2d also uses its own coordinates which start in the left bottom corner and are counted in meters.

Image: box2d coordinates

Image 2

Fortunately, Moscrif’s framework usually uses normal pixel coordinates and conversions are made automatically.

Bodies

Bodies are another important part of box2d physical engine. Using bodies, we create all objects which interact in the world. In our game puck, paddles, barriers and goals are created as bodies. Box2d supports three types of bodies which have different behaviour. First difference is that not all types of bodies collide together. The next table shows which bodies collide together.

Image 3

Different types of bodies also behave differently under simulation. Static bodies do not move under simulation and have infinite mass. They do not move according to forces or velocity. In our game, static bodies are barriers and goals. Kinematic bodies move only according to its velocity and it also behaves as infinite mass. Dynamic bodies are fully simulated. In our game, the dynamic bodies are paddles and goals.

Other important body properties are density, friction and bounce. The bounce property affects the speed of the body after bounce. V 1.0 means that the body bounces with the same speed as it fell; however, there is a possibility to increase the bounce rate and the ball will bounce off with more velocity.

Let’s Start – Start Up File & Resources

Start Up File

We are going to create our game in Moscrif IDE where we need to start a new project based on the game’s framework. By default, the start up file is main.ms. This file contains an instance of Game framework class, which is a base class for all game projects in Moscrif. The Moscrif‘s game framework also offers PhysicsScene class which creates physics world and combines all other physical and non-physical objects. PhysicsScene class is also used for creating a GameScene class, which puts playground into existence and its instance is created in onStart event of Game class which is called when the game starts.

Example: Create a new instance of GameScene

C++
game.onStart = function(sender)
{
    if (res.supportedResolution) {
        // push game scene to application
        this.gameScene = new GameScene();
        this.push(this.gameScene);
        // restart game (start new)
        this.gameScene.reset();
    }
    this._paint = new Paint();
    this._paint.textSize = System.height / 30;
    var (w, h) = this._paint.measureText("Unsupported resolution");
    this._textWidth = w;
}  

In main.ms are also managed users‘ events like pointer or key pressed.

Example: Manage user events

C++
// quit game when user clicks on the back or home hardware button
game.onKeyPressed = function(sender, keyCode)
{
    if (keyCode == #back || keyCode == #home)
        game.quit();
}
game.onPointerPressed = function()
{
    if (!res.supportedResolution)
        game.quit();
}  // quit game when user clicks on the back or home hardware button
game.onKeyPressed = function(sender, keyCode)
{
    if (keyCode == #back || keyCode == #home)
        game.quit();
}
game.onPointerPressed = function()
{
    if (!res.supportedResolution)
        game.quit();
} 

Resources

As I wrote earlier, the graphics are made separately for more resolutions. However, all images are managed by Resource class which means that we do not need to worry about device resolutions in the next development process. An instance of the resource class is created as a second global variable in main.ms which also ensures that all images will be loaded only once and to be saved into device’s memory and performance.

Example: load resources in class constructor

C++
function this()
{
    this._supportedResolution = true;
    this._images = {
            background          : this._loadImage("backGame", "jpg");
            playerHuman         : this._loadImage("player2");
            playerAI            : this._loadImage("player1");
            puck                : this._loadImage("puck");
            menuBg              : this._loadImage("menuBack");
            menuButton          : this._loadImage("menuBtn");
            menuButtonPressed   : this._loadImage("menuBtnPress");
            menuPart            : this._loadImage("menuPart");
        };
    ...
} 

The _loadImage function loads images according to device resolution.

Example: load images according to device resolution

C++
 function _loadImage(filename, format = "png")
{
    var file = "app://" + System.width + "_" + System.height + "/" + filename + "." + format;
    var bitmap;
    if (System.isFile(file)) {
         bitmap = Bitmap.fromFile(file);
         if (bitmap != null)
            return bitmap;
    }
 
    // Kindle Fire 600x1002
    if (System.width == 600) {
        file = "app://" + System.width + "_1024" + "/" + filename + "." + format;
        bitmap = Bitmap.fromFile(file);
        if (bitmap != null)
            return bitmap;
    }
    // Galaxy tab 800x1232 752x1280
    if (System.width == 800) {
        file = "app://" + System.width + "_1280" + "/" + filename + "." + format;
        bitmap = Bitmap.fromFile(file);
        if (bitmap != null)
            return bitmap;
    }
    // SE xperia 480x854
    if (System.width == 480) {
        file = "app://" + System.width + "_800" + "/" + filename + "." + format;
        bitmap = Bitmap.fromFile(file);
        if (bitmap != null)
            return bitmap;
    }
 
    this._supportedResolution = false;
    return null;
} 

Game Scene

Now, let’s create the most important part of our game -> game scene. The game scene is the playground: table, barriers, goals, puck and paddles. The game scene is created by GameScene class extended from PhysicsScene, which creates the physics world. When framework classes are constructed, they call init (also beforeInit and afterInit) method. In our game, we are going to create the physics world for the scene, set events onBeginContact and onEndContact which are called when two bodies collide in the scene, and also create all other objects in init function. Other game elements like the puck, barriers, goals or paddles are created by separate functions like PhysicsSprite objects.

The game scene also draws table image and score. The image is resized to the full screen for cases when the Resource class cannot find images for current device resolution.

Example: Draw background image and score

C++
function draw(canvas)
{
    canvas.drawBitmapRect(res.images.background, 0, 0, res.images.background.width, 
                          res.images.background.height, 0, 0, System.width, System.height);
    // save current canvas settings
    canvas.save();
    // rotate canvas to 270° CW
    canvas.rotate(270);
    // draw score
    canvas.drawText(this.playerAI.score.toString(), 
                    System.height / - 2 - 2 * this._scoreW, 
                    System.width / 16 + this._scoreH, res.paints.scoreBlue);
    canvas.drawText(this.playerHuman.score.toString(), 
                    System.height / - 2 + this._scoreW, 
                    System.width / 16 + this._scoreH, res.paints.scoreGreen);
    // restore canvas settings (revert rotation)
    canvas.restore()
    super.draw(canvas);
} 

Puck

Puck is created as an instance of PhysicsSprite class. Its type is set to dynamic which means that it collides with other static or dynamic bodies. The puck‘s bounce property is one, which means that it bounces from other bodies with some force as it falls. To achieve the realistic physical behavior, we need to apply linear damping to the body. In real air hockey, the puck moves on the air cushion so the damping between the puck and table is really small and the puck moves fast. It’s image is as well as all other images in the game loaded from the resources.

Example: create puck

C++
function _createPuck()
{
    const density = 1.0, friction = 0.2, bounce = 1.0;
    // create physics body of the puck
    var puck = this.addCircleBody(res.images.puck, #dynamic, 
               density, friction, bounce,  res.images.puck.width / 2/*radius*/);
    // place puck to center of the table
    puck.setPosition(System.width / 2, System.height / 2);
    puck.fixedRotation = true;
    puck.bullet = true;
    puck.setLinearDamping(0.3);
    return puck;
} 

Barriers

Barriers prevent the puck and mallets from leaving the table. They are around the whole table except the goals. Their type is set to static, which means that they do not move under the simulation but collide with other dynamic bodies (puck and paddles). Together with barriers around the playground, we are going to create also four invisible squares in the table corners. Without these squares, the puck gets stuck sometimes near the left or right barrier and moves along up and down. However, when the puck hits one of these squares in the corners, it bounces away from the barrier.

Image: barriers and „inivisible“ squares around the playground

Image 4

Example: create barriers

C++
function _createBarriers()
{
    const density = 0.0, friction = 0.0, bounce = 0.0;
    const width = System.width / 4, height = System.width / 32;
    var topWallA = this.addPolygonBody
                   (null, #static, density, friction, bounce, width, height);
    topWallA.setPosition(System.width / 8, 1);
    var topWallB = this.addPolygonBody
                   (null, #static, density, friction, bounce, width, height);
    topWallB.setPosition(System.width - System.width / 8, 1);
    var bottomWallA = this.addPolygonBody
                      (null, #static, density, friction, bounce, width, height);
    bottomWallA.setPosition(System.width/8, System.height);
    var bottomWallB = this.addPolygonBody
                      (null, #static, density, friction, bounce, width, height);
    bottomWallB.setPosition(System.width - System.width / 8, System.height);
    var leftWall = this.addPolygonBody(null, #static, density, friction, 
                                       bounce, System.width / 32, System.height);
    leftWall.setPosition(1, System.height / 2);
    var rightWall = this.addPolygonBody(null, #static, density, friction, 
                                        bounce, System.width / 32, System.height);
    rightWall.setPosition(System.width, System.height / 2);
    // corners
    var leftTop = this.addPolygonBody(null, #static, density, friction, 
                                      bounce, this.puckRadius, this.puckRadius);
    leftTop.setPosition(this.puckRadius / 2, System.width / 60);
    var rightTop = this.addPolygonBody(null, #static, density, friction, 
                                       bounce, this.puckRadius, this.puckRadius);
    rightTop.setPosition(System.width - this.puckRadius / 2, System.width / 60);
    var leftBottom = this.addPolygonBody(null, #static, density, friction, 
                                         bounce, this.puckRadius, this.puckRadius);
    leftBottom.setPosition(this.puckRadius / 2, System.height - System.width / 60);
    var rightBottom = this.addPolygonBody(null, #static, density, friction, 
                                          bounce, this.puckRadius, this.puckRadius);
    rightBottom.setPosition(System.width - this.puckRadius / 2, 
                            System.height - System.width / 60);
} 

Goals

The goals are situated in the center of the top and bottom barrier. The goals are also bordered by three barriers on their left, right and top (or bottom) side, but these barriers are out of the screen to allow the puck to leave the playground. When the puck collides with some barriers inside the goals, the puck is removed and the score updated.

Image: goals

Image 5

Example: create goals

C++
// creates goals
function _createGoals()
{ 
    const density = 0.0, friction = 0.0, bounce = 0.0;
    var goalA = this.addPolygonBody(null, #static, density, friction, bounce, 
                                    this.goalsWidth, System.width / 32);
    goalA.beginContact = function(contact) { this super._checkGoal(contact, #playerAI); }
    goalA.setPosition(System.width / 2, -2 * this.puckRadius + System.width / 32);
    var goalALeft = this.addPolygonBody(null, #static, density, friction, bounce, 
                                        System.width / 32, 2 * this.puckRadius);
    goalALeft.beginContact = function(contact) { this super._checkGoal(contact, #playerAI); }
    goalALeft.setPosition(System.width / 2 - this.goalsWidth / 2 - this.puckRadius, 
                          -1 * this.puckRadius);
    var goalARight = this.addPolygonBody(null, #static, density, friction, bounce, 
                                         System.width / 32, 2 * this.puckRadius);
    goalARight.beginContact = function(contact) { this super._checkGoal(contact, #playerAI); }
    goalARight.setPosition(System.width / 2 + this.goalsWidth / 2 + this.puckRadius, 
                           -1 * this.puckRadius);
...} 

Paddles

Both paddles are created by the same function _createPaddle function, which creates circular body with different image for human and for AI paddle. The density of the paddle is bigger than the puck’s density, which causes that with bigger size comes bigger mass comparing to the puck. When colliding two bodies with different mass, the movement of the body with larger mass is affected less.

Example: create paddles

C++
// creates paddle (for AI or human player)
function _createPaddle(paddleType)
{
    assert paddleType == #playerAI || paddleType == #playerHuman;
    const density = 1.1, friction = 0.3, bounce = 0.0;
    var paddle = (paddleType == #playerAI)
        ? this.addCircleBody(res.images.playerAI, #dynamic, density, 
                             friction, bounce, res.images.playerAI.width / 2)
        : this.addCircleBody(res.images.playerHuman, #dynamic, density, 
                             friction, bounce, res.images.playerHuman.width / 2);
    paddle.fixedRotation = true;
    paddle.setLinearDamping(5.0);
    return paddle;
} 

Contacts

When two bodies collide in the scene, two events are called: onBeginContact, when collision starts and onEndContact when collision ends. We map both events onto class member functions: _beginConcat and _endContact in game scene’s init function.

Example: map both event functions

C++
function init()
{
    // create physics world
    this._world = new b2World(0.0, 0.0, true, true);
    // world callback
    this.onBeginContact = function(sender, contact) { this super._beginContact(contact); }
    this.onEndContact = function(sender, contact) { this super._endContact(contact); }
    ...
} 

There are many bodies that can come into contact with one another (human paddle & puck, AI paddle & puck, human paddle & AI paddle, puck & barriers, etc.). Checking particular bodies which collide throws any if or switch condition which may be too complicated. To simplify the contact management, we add to all bodies, which should raise an event when they collide, call back function to beginContact or endContact variable. Moscrif’s JavaScript engine allows to create both local and member function anywhere in the code. It means that the variables do not have to be defined in the class, but it can be simply added to any separate objects similar to that in the next example:

Example

C++
// create AI player
this.paddleAI = this._createPaddle(#playerAI);
this.paddleAI.endContact = function(body)
{
    this super.playerAI.hit();
} 

Then when some contact appears, we only call object’s beginContact or endContact method for both bodies, which manages all other needed operations.

Example: begin contact

C++
 // listener for begin of collision
function _beginContact(contact)
{
    var bodyA = contact.getBodyA();
    var bodyB = contact.getBodyB();
    if(bodyA.beginContact)
        bodyA.beginContact(bodyB, contact);
    if(bodyB.beginContact)
        bodyB.beginContact(bodyA, contact);
} 

Human Player

At this point, everything is prepared, but paddles do not move. In order to do that, two classes are created: playerHuman and playerAI. PlayerHuman class moves paddle controlled by the player. To move the paddle, we use mouse joint. Mouse joint allows manipulating with the physical bodies to wanted position. They can be also moved by setPosition method, but when body is moved by this method, it does not interact with other physical objects.

When user taps on the screen, the scene calls human player’s handlePressed method. In handlePressed method, mouse joint is created to manipulate the paddle controlled by the player. If player taps on the opponent’s half of playground, the paddle moves along the central line.

Example: create mouse joint

C++
// called by Table when touch down occurred
function handlePressed(x, y)
{
    // check player's side
    if (y < System.height / 2)
        y = System.height / 2;
    // just simple helper
    const table = this.table;
    // mouse joint definition
    var mouseJointDef = {
        maxForce : 10000,
        frequencyHz : 1000,
        dampingRatio : 0.0,
        targetX : table.x2box2d(x), // specified in box2d coords
        targetY : table.y2box2d(y)  // specified in box2d coords
    };
    // move paddle to touched place
    this.paddle.setTransform(x, y);
    // create mouse joint
    if (this.joint)
        this.table.destroyJoint(this.joint);
    this.joint = table.createMouseJoint(table.ground, this.paddle, mouseJointDef, true);
} 

When user moves his finger on the screen, the scene calls handleDragged method from human player class. In this method, setTarget method of mouseJoint is called. This method moves the paddle to the current finger position.

Example: move the paddle to the current finger position

C++
// called by Table when touch drag occurred
function handleDragged(x, y)
{
    // limit player's side
    if (y < System.height / 2 + this.puckRadius)
        y = System.height / 2 + this.puckRadius;
    // affect mouse joint
    if (this.joint != null)
        this.joint.setTarget(this.table.x2box2d(x), this.table.y2box2d(y));
} 

AI Player

Opponent’s game is controlled by playerAI class. AI player also moves by mouse joint similar to a human player but coordinates to setTarget method is calculated by our algorithms. The AI player does only two actions -> defence or attack. To make player more realistic, the class implements the following features:

  • The gap between two attacks is at least 700 milliseconds
  • The AI player does not respond directly after the line, but only after a small gap after line
  • The AI player does not hit the puck if it is in the table corner.
  • The AI player moves to defend position also if puck is on opponents half of the playground
  • The AI player’s paddle moves with realistic speed

The gap between two opponents attacks is minimally 700 milliseconds. Every 25 milliseconds, method handleProcess is called which checks if it takes at least 700 milliseconds after the last attack. If yes, the opponent attacks again, otherwise, it goes back to defence.

Example: check if last attack was before at least 700 milliseconds

C++
// called by Table object (onProcess)
function handleProcess()
{
    // get position of my paddle
    var (x, y) = this.paddle.getPosition();
    // get position of puck
    var (px, py) = this.table.puck.getPosition();
    // delay & defense after contact
    if (System.tick - this.hitTime < 700) {
        this._defense(x, y, px, py);
        return;
    }
    // otherwise make a decision
    this._makeDecision(x, y, px, py);
} 

When the time from the last attack is at least 700 milliseconds, the player may attack, but does not have to. To decide if attack or not function _makeDecision. If the puck is not too close to table’s corner and it is on the player’s half, the player attacks onto the puck.

Example: decide if the player should attack

C++
function _makeDecision(x, y, px, py)
{
    // attack when puck is in our corner
    var puckInCorner = px < System.width / 5 || px > 4*System.width / 5;
    if (puckInCorner && py < 2 * this.puckRadius ) {
        return ;
    }
    // move to puck's position and hit puck to the second half of table
    if (py < ( 9 * System.height / 20))
        return this._moveTo(x, y, px, py - this.puckRadius / 4);
    return this._defense(x, y, px, py);
} 

The defense function moves the paddle horizontally in the front of the goal. However, the paddle does not move from the left barrier to the right. It moves only to the middle of the playground according to the current puck position.

Example: defense position

C++
// simple defense method
function _defense(x, y, px, py)
{
    if (py < y && Math.abs(System.width / 2 - px) > System.width / 5)
        return this._moveTo(x, y, px, py - this.puckRadius);
    this._moveTo(x, y, System.width / 4 + System.width / 2 * (px / (1.0 * System.width)), 
                 System.height / 6);
    return true;
} 

As you can see in all previous methods, function _moveTo is used to move the paddle. This function moves the paddle gradually according to required speed. This function ensures that AI player’s speed is similar to human speed, because directly using setTarget method may cause the AI player to be too fast.

Example: move AI player's paddle

C++
 // calculates movement for AI paddle
function _moveTo(ox, oy, px, py)
{
    // be random
    var speed = Integer.min(640, System.width) / (40.0 + rand(20));
    // calculate deltas
    const dx = px - ox;
    const dy = py - oy;
    // calculate distance between puck and paddle position (we use Pythagorean theorem)
    const distance = Math.sqrt(dx * dx + dy * dy);
    // if total distance is greater than the distance, 
    // of which we can move in one step calculate new x and y coordinates 
    // somewhere between current puck and paddle position.
    if (distance > speed) {
        // x = current paddle x position + equally part of speed on x axis
        px = ox + speed / distance * dx;
        py = oy + speed / distance * dy;
    }
    // move paddle to the new position
    this.joint.setTarget(this.table.x2box2d(px), this.table.y2box2d(py));
    return true;
} 

The whole logic of AI player game is shown in the next diagram:

Image: AI player logic

Image 6

Summary

This article showed you how to create an air hockey game. Some parts of this example can be easily rewritten to other programming languages and used in your projects. But for the best way to create this game for the largest number of devices with the minimum amount of work, I highly recommend using Moscrif SDK.

History

  • 1st August, 2012: Initial version

License

This article, along with any associated source code and files, is licensed under The BSD License