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
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
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()
game.onStart = function(sender) {
this.gameScene = new GameScene({width : System.width, height : System.height});
this.push(this.gameScene);
}
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)
{
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.
this._view3D.applyToCanvas(canvas);
To rotating are used functions rotate(angle), rotateY(angle) and rotateZ(angle).
this._view3D.rotateY(degrees);
By default, canvas is rotating around left top corner of the screen, therefore a card rotates unrealistically.
Image: card rotation
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
function draw(canvas)
{
super.draw(canvas);
canvas.save();
canvas.translate(this.x, this.y);
canvas.scale(0.85, 0.85);
this._view3D.applyToCanvas(canvas);
if (this._side == #front) {
this._drawFront(canvas);
} else {
this._drawBack(canvas);
}
canvas.restore();
}
Image: card rotation
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
function show()
{
this.rotating = true;
var animator = new Animator({
transition: Animator.Transition.easeOut,
duration: 1500, });
animator.addSubject(function(state) { 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:
- Load random image, and remove this loaded image from availableImages
- Set image to first card and remove this card from freeCards
- 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
Example: distribute images onto cards
function _fillImages()
{
res.getCards();
var availableImages = res.images.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++) {
if (freeCards.length == 0)
return;
var randomImageIndex = rand(availableImages.length);
var imageFile = availableImages[randomImageIndex];
availableImages.removeByValue(imageFile);
var randomCardIndex = rand(freeCards.length);
freeCards[randomCardIndex].cardImage = imageFile;
freeCards.removeByValue(freeCards[randomCardIndex]);
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
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
function _turnCards()
{
var timer = null;
if (this._compare()) {
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 {
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
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.