Click here to Skip to main content
Click here to Skip to main content

Air hockey

By , 10 Aug 2012
Rate this:
Please Sign up or sign in to vote.

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 out 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



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 mowes 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 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



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 body which have different behaviour. First difference is that not all types of bodies collide together. The next table shows which bodies collide together.  

body type#static#dynamic#kinematic
#static








#dynamic








#kinematic










collide together



do not collide together

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

game.onStart = function(sender)
{
    if (res.supportedResolution) {
        // push game scene to 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

// 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 

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 

 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 

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 setings
    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

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

Example: create barriers
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 leave the playground. When the puck collides with some barriers inside the goals, the puck is removed and the score updated.

Image: goals



Example: create goals
// 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 

// 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

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 & bariers 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 as in the next example:

Example:

// 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

 // listener for begin of collicion
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 halve of playground the paddle moves along the central line.

Example: create mouse joint

// called by Table when touch down occured
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

// called by Table when touch drag occured
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 as a human player but coordinates to setTarget method is calculated by our algorithms. The AI player does only two actions -> defense or attack. To make player more realistic the class implements 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 defense.

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

// 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 desiciton
    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

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

// simple defence 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 using directly setTarget method may cause that the AI player is too fast. 

Example:  move AI player's paddle

 // 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 padle 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 showed in next diagram:

Image: AI player logic



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 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.

License

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

About the Author

PavolSatala

Slovakia Slovakia
Author develops in various programming languages included: C++, Javascript, and PHP. Last year he creates mobile cross platform applications in Moscrif SDK.
Follow on   Twitter

Comments and Discussions

 
Questionhi ! PinmemberMember 1039130917-Nov-13 6:28 
GeneralMy vote of 5 PinmemberHossainCo28-Oct-12 6:07 
GeneralRe: My vote of 5 PinmemberPavolSatala12-Nov-12 5:27 
GeneralMy vote of 5 PinmemberJasmine25019-Aug-12 9:40 
GeneralOne change needed PinmemberTim Corey1-Aug-12 9:36 
GeneralRe: One change needed PinmemberPavolSatala6-Aug-12 2:22 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web02 | 2.8.140421.2 | Last Updated 10 Aug 2012
Article Copyright 2012 by PavolSatala
Everything else Copyright © CodeProject, 1999-2014
Terms of Use
Layout: fixed | fluid