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

Pairs game

4.97/5 (20 votes)
15 Aug 2012BSD6 min read 89.4K   8.7K  
How to create a pairs game for about 90% of Smartphones?
Image 1   Content

Introduction

Pairs, also known as Concentration, Memory cards, Pexeso etc. is a simple card game, where player must find two identical cards. This game is great for memory training and good to kill some time for kids and adults. Nevertheless, our goal is to show developers how to create an easy game like this with only a few lines of code for many mobile operation systems.

Background

The best option for creating a game for more than one platform is using a cross-platform SDK. This article talks about cross-platform development tools called Moscrif SDK. At present, iOS, Android and Bada devices are supported.

Our applications can be run on many different devices from low-end devices with only 600mHz processor to latest high-end devices like newest iPad, Samsung Galaxy S 3 etc. These devices have different performance capabilities and screen resolutions. The best way to overcome different picture dimensions is using vector graphics; however, this is not always possible.

User interface 

By now, applications are being developed at rapid pace and it usually comes down to offering a user-friendly interface to earn customers’ loyalty. Our game application consists of tool bar with only two buttons (close and new game) and timer. Main part of our application is a grid with twelve cards. The cards are uncovering with a nice 3D rotation effect.

Background and toolbar are filled with bitmap graphics, which is created separately for three resolutions. Simple image on the cards allows us to use vector graphics for them. Vector graphics maintain all details also after resizing. Vector graphics are smaller in size than bitmap.

Image: graphics proposal

Image 2

Development process

To develop this game we used Moscirf’s game framework (Game, Scene, Layer, sprite, GameControl and ImageButton classes).

  • Game class is a base class, which creates environment to run our game. It manages user and system events and contains other scene or sprite objects.
  • Scene is a part of our game which contains several layers or directly Sprite objects. Developer can also manage user and system events in scene.
  • Layer is used to create separate parts of scene like menu, playground etc.
  • Sprites create final objects like pucks, balls, paddles etc. the Sprites objects do not manages user events. They can be added to scene, layer or game objects.
  • GameControl class's instances can be added to scenes, layers or directly to the game object but it also manages user events. In our game Card class is extended from GameControl class.

Start up file

By default, the start up file is main.ms. We put only an instance of Game class into this file, draw a background. It also manages event onKeyPressed. This event is raised when user hits the hardware key.

Example: start up file

C++
include "lib://game2d/game.ms"
include "lib://game2d/layer.ms"
include "lib://game2d/imageButton.ms"
include "app://vectorImage.ms"
 
include "app://resources.ms"
include "app://cardLayer.ms"
include "app://gameScene.ms"
include "app://card.ms"
include "app://menuLayer.ms"
include "app://timerSprite.ms"
 
var game = new Game();
var res = new Resources()
 
// onstart what top do
game.onStart = function(sender) {
    this.gameScene = new GameScene({width : System.width, height : System.height});
    this.push(this.gameScene);
}
 
// clear screen
game.onDraw = function(sender, canvas) {
    canvas.drawBitmapRect(res.images.background, 0, 0, res.images.background.width, res.images.background.height, 0, 0, System.width, System.height);
}
 
game.onKeyPressed = function(sender, keyCode)
{
    // quit game when user click onto back hardwaere key
    if (keyCode == #back)
        this.quit();
}
 
game.run();

GameScene 

The game scene is made of only one scene in our game. This scene has two layers, first which creates all cards and second which creates menu.

Card

Every card is an instance of Card class. This class provides three features:

  • draw a card
  • show a front side of card
  • hide a front side of card

Card turns with 3D effects. Moscrif SDK offers View3D object, which manages object transform in 3D world. View3D only calculates transformations for normal canvas. This means that programmers only need to apply transform to the canvas.

C++
this._view3D.applyToCanvas(canvas);

To rotating are used functions rotate(angle), rotateY(angle) and rotateZ(angle).

C++
this._view3D.rotateY(degrees); 

By default, canvas is rotating around left top corner of the screen, therefore a card rotates unrealistically.

Image: card rotation

Image 3



To achieve a realistic rotation effect, center of rotation has to be moved to the middle. Canvas is also scaled to create gabs between the cards.

Example: draw card

C++
// drawing
function draw(canvas)
{
    super.draw(canvas);
 
    canvas.save();
    canvas.translate(this.x, this.y);
    canvas.scale(0.85, 0.85);
    // apply 3d rotation
    this._view3D.applyToCanvas(canvas);
    if (this._side == #front) {
        this._drawFront(canvas);
    } else {
        this._drawBack(canvas);
    }
    canvas.restore();
}

Image: card rotation

Image 4


Animate cards 

Cards are rotated by two functions show and hide. These functions create animator objects. The animator object creates mathematical background for the animation. The most important animator’s properties are duration and transition. Using this class the animation does not have to have linear behaviour. According to the required transition the animator object calls call-back function specified with addSubject function. The call back function has only one parameter the state, which specifies the current position in the animation.

In our game we apply rotation to View3D in call back function. Because of the view3D rotates from the last position, not from the base position (without rotation) we need to restore view3D first to base position (without rotation) and then rotate it

Example: show card

C++
function show()
{
    this.rotating = true;
    var animator = new Animator({
        transition: Animator.Transition.easeOut,
        duration: 1500,                         // length of animation in miliseconds
    });
    animator.addSubject(function(state) {       // state starts from 1.0 to 0.0
        var self = this super;
        self._view3D.restore();
        self._view3D.save();
        self._view3D.rotateY(state * 180);
        if (state * 180 > 90) {
            self._side = #front;
        }
    });
    animator.onComplete = function() {
        this super.rotating = false;
    };
    animator.play();
}

Card layer 

Card layer creates grid of cards and distributes images onto them.

Cards distribution

Each time game is restarted images on the top side of a card have to be redistributed. Pictures are loaded from resources and then pushed to availableImages array. Then, all cards are pushed to freeCards array. This array contains all cards, where we need to change images. The distribution continues in three steps, until all image are distributed:

  1. Load random image, and remove this loaded image from availableImages
  2. Set image to first card and remove this card from freeCards
  3. Set same image to the second card and remove this card from freeCards

This algorithm allows to distribute images very fast with a minimum number of cycle runs.

Image: distribute images onto cards

Image 5


Example: distribute images onto cards

C++
//fill image grid with random images
function _fillImages()
{
    // load images into resources
    res.getCards();
    // mark all card images as available
    var availableImages = res.images.cards;
    // mark all Cards
    var freeCards = [];
    for (var card in this._cards)
        freeCards.push(card);
    var needed = this._rows * this._columns / 2;
    for (var i=0; i < needed; i++) {
        // no more empty cards?
        if (freeCards.length == 0)
            return;
        // get random vector file
        var randomImageIndex = rand(availableImages.length);
        var imageFile = availableImages[randomImageIndex];
        // remove loaded file from available images
        availableImages.removeByValue(imageFile);
 
        // initialize cell
        var randomCardIndex = rand(freeCards.length);
        // create & write cell view
        freeCards[randomCardIndex].cardImage = imageFile;
        // remove cell from freeCards
        freeCards.removeByValue(freeCards[randomCardIndex]);
 
        // initialize cards's pair (random)
        randomCardIndex = rand(freeCards.length);
        freeCards[randomCardIndex].cardImage = imageFile;
        freeCards.removeByValue(freeCards[randomCardIndex]);
    }
    availableImages = null;
}

Cards comparison 

When two cards are uncovered, the card layer also compares these two cards. If both cards are same, they stay uncovered, otherwise they turn back after a short delay. When two cards are uncovered the _turnCards function is called. It uses function _compare to compare the uncovered cards. It compares their id, because cards with same image have same id.

Example: compare function

C++
function _compare()
{
    if (this._opened[0]._id == this._opened[1]._id)
        return true;
    else
        return false;
}

If both card are same the variable, which carries number of onpened cards are set to zero, what allows user to uncover other cards, and also it is checked if it is not all cards uncvered. If cards are different, the timer which hide both cards starts

Example: compare cards

C++
function _turnCards()
{
    var timer = null;
    if (this._compare()) {
        //let images uncovered, and resume game
        this._opened[0] = 0;
        this._opened[1] = 0;
        this._openedCount = 0;
        this.numberOfOpened += 1;
        this._wav.play();
        if (this.numberOfOpened == this._rows * this._columns / 2)
            game.gameScene.menu.timer.stop();
    } else {
        //after one second rotete card back
        timer = new Timer(10, false);
        timer.onTick = function()
        {
            if (this super._opened[0])
                this super._opened[0].hide();
            if (this super._opened[0])
                this super._opened[1].hide();
            this super._openedCount = 0;
        }
        timer.start(1200);
    }
}

Menu layer 

Menu layer creates simple menu used in our game. It consists of two buttons only. To start new game and quit game with a timer. Buttons are created simply by using ImageButton control. The next example shows how to create a new game button. This button has two different images for normal and pressed state. 

Example: create new game button

C++
var newGame = new ImageButton({
    image           : res.images.newGame,
    imagePressed    : res.images.newGameClicked,
    mode            : #image,
    x               : System.width / 2,
    y               : System.height - this.height / 2,
});
newGame.onClick = function()
{
    game.gameScene.cardLayer.newGame();
}
this.add(newGame);

Summary

Now you know how to create your own Puzzle game, which can be easily adapted to your requirements by only changing the images or number of cards. The best thing is that using Moscrif SDK creates this application only once and can be published for about 90% of Smartphones currently on the market (statistics for the USA from June 2012). Happy coding.

License

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