Complete 3D HTML5 Concentration game made with Babylon.js

Read all posts about "" game

Back talking about Babylon.js after the creation of a Sokoban level and the basics of a Concentration game, here we go with a complete Concentration game.

First, have a look at the game:

You should know the rules, pick two cards with the same color.

If you followed the previous Babylon.js posts, you should already know the concepts behind this game, as the only new feature visible in this game is the callback when an animation ends.

This complete list of things you will be using in the creation of this game is interesting, and you will learn how to:

* Create a scene
* Create an environmental fog
* Create a Box primitive
* Apply a diffuse color to a primitive (that is, paint it the color you want)
* Modify of the size of primitives
* Use texture mapping
* Use different textures on different meshes of the same body
* Create directional lights
* Select bodies with the mouse
* Create animations and keyframes
* Handle animation callbacks

I removed the shadow casting because I will add them in next project, along with particles.

This is the complete source code of game.js, refer to the first example to see how to set up a Babylon.js project.

// storing in an array the 8 colors used to fill game cards
var colors = [
	new BABYLON.Color3(1,0,0),
	new BABYLON.Color3(1,1,0),
	new BABYLON.Color3(1,0,1),
	new BABYLON.Color3(0,1,0),
	new BABYLON.Color3(0,1,1),
	new BABYLON.Color3(0,0,1),
	new BABYLON.Color3(1,1,1),
	new BABYLON.Color3(1,0.5,0)
];

// this is the array we will use to store card values
var gameArray = [0,0,1,1,2,2,3,3,4,4,5,5,6,6,7,7];

// shuffling the array. There are several ways to do it but it's not in the scope
// of this script, so I am using http://jsfromhell.com/array/shuffle 

shuffle = function(v){
    for(var j, x, i = v.length; i; j = parseInt(Math.random() * i), x = v[--i], v[i] = v[j], v[j] = x);
    return v;
};

gameArray = shuffle(gameArray);

// a counter to tell us how many animation have been completed so far
var animationCompleted = 0;
// counter to tell us how many cards we picked so far
var pickedCards=0;
// an array with the picked cards
var pickedArray = [];
// identifying the canvas id
var canvas = document.getElementById("gameCanvas");
// creation of the engine itself
var engine = new BABYLON.Engine(canvas,true);
// attaching a scene to the engine. This is where our game will take place
var scene = new BABYLON.Scene(engine);
// adding a little fog to the scene, to give some kind of "depth" to the scene
scene.fogMode = BABYLON.Scene.FOGMODE_EXP;
// the density is very high, so a low value is recommended
scene.fogDensity = 0.05;
// creation of a camera, the type is "AcrRotate".
// this mean the camera is bound along two arcs, one running from north to south, the other from east to west
// the first argument is the came of the camera instance
// the second argument is the angle along the north-south arc, in radians (3 * Math.PI / 2)
// the 3rd argumentis the angle along the east-west arc, in radians (3*Math.PI/4)
// the 4th argument is the radius of such arcs (20)
// the 5th argument is the camera target (BABYLON.Vector3.Zero()) in this case the origin
// finally, the scene where to attach the camera ("scene")
var camera = new BABYLON.ArcRotateCamera("camera",3 * Math.PI / 2, 11*Math.PI/16, 20, BABYLON.Vector3.Zero(), scene);
// adding touch controls to camera, that's where hand.js come into play
camera.attachControl(canvas, false);
// we need a directional light in order to cast a shadow
var light = new BABYLON.DirectionalLight("light", new BABYLON.Vector3(5,0,20), scene);
light.position = new BABYLON.Vector3(1,1,-10);
 
// this is the table material. We will map an image called "wood.jpg" on it
var tableMaterial = new BABYLON.StandardMaterial("tableMaterial", scene);
tableMaterial.diffuseTexture = new BABYLON.Texture("wood.jpg", scene);

// THE TABLE
var table = BABYLON.Mesh.CreateBox("table", 12, scene);
table.scaling.z = 0.025;
table.material=tableMaterial;

// PLACING THE CARDS: 16 in a 4x4 matrix in this case
var cardsArray = [];
for(i=0;i<16;i++){
	var card = BABYLON.Mesh.CreateBox("card", 2, scene);
	// this is a custom attribute to know whether the card has been picked
	card.picked = false;
	// another custom attribute to determine card index
	card.cardIndex = i;
	// finally, assigning the card the most important color attribute: the value
	card.cardValue = gameArray[i];
	// scaling and placing the card
	card.scaling.z = 0.125;
	card.position = new BABYLON.Vector3((i%4)*2.5-3.75,Math.floor(i/4)*2.5-3.75,-0.25);
	// defining two different meshes, one for the bottom face and one for the rest of the card
	card.subMeshes=[];
	// arguments of Submesh are:
	// 1: the index of the material to use
	// 2: the index of the first vertex
	// 3: the number of verices used
	// 4: index of the first indice to use
	// 5: the number of indices
	// 6: the main mesh 
	card.subMeshes.push(new BABYLON.SubMesh(0, 4, 20, 6, 30, card));
	card.subMeshes.push(new BABYLON.SubMesh(1, 0, 4, 0, 6, card));
	// card material will be made with 2 different materials.
	// The first material is "cardMaterial", a grey color
	var cardMaterial = new BABYLON.StandardMaterial("cardMaterial", scene); 
	cardMaterial.diffuseColor = new BABYLON.Color3(0.5,0.5,0.5);
	// the second material is "cardBackMaterial", a the actual color color
	var cardBackMaterial = new BABYLON.StandardMaterial("cardBackMaterial", scene); 
	cardBackMaterial.diffuseColor = colors[gameArray[i]];
	// with these two colors in mind, let's built a multi material
	var cardMultiMat = new BABYLON.MultiMaterial("cardMulti", scene);
	// here is how we push the materials into a multimaterial
	cardMultiMat.subMaterials.push(cardMaterial);
	cardMultiMat.subMaterials.push(cardBackMaterial);
	// this is the content of our multi material - 0: CardMaterial, 1: CardBackMaterial
	// finally assigning the multi material to the card
	card.material=cardMultiMat;	
	cardsArray[i]=card;
}

// defining the animations

var firstCardMoveAnimation = new BABYLON.Animation(
	"1st card move animation", // name I gave to the animation 
	"position.z", // property I am going to change
	30, // animation speed
	BABYLON.Animation.ANIMATIONTYPE_FLOAT, // animation type
	BABYLON.Animation.ANIMATIONLOOPMODE_CONSTANT // animation loop mode
	// play with BABYLON.Animation.ANIMATIONLOOPMODE_RELATIVE,
	// BABYLON.Animation.ANIMATIONLOOPMODE_CYCLE
	// BABYLON.Animation.ANIMATIONLOOPMODE_CONSTANT
);

// just in case you want to give the 2nd card a different animation.
// not this case

var secondCardMoveAnimation = new BABYLON.Animation(
	"2nd card move animation",
	"position.z",
	30,
	BABYLON.Animation.ANIMATIONTYPE_FLOAT,
	BABYLON.Animation.ANIMATIONLOOPMODE_CONSTANT
);

var firstCardRotateAnimation = new BABYLON.Animation(
	"1st card rotate animation",
	"rotation.y", // this time I rotate the card around y axis
	30,
	BABYLON.Animation.ANIMATIONTYPE_FLOAT,
	BABYLON.Animation.ANIMATIONLOOPMODE_CONSTANT
);

// just in case you want to give the 2nd card a different animation.
// not this case

var secondCardRotateAnimation = new BABYLON.Animation(
	"2nd card rotate animation",
	"rotation.y",
	30,
	BABYLON.Animation.ANIMATIONTYPE_FLOAT,
	BABYLON.Animation.ANIMATIONLOOPMODE_CONSTANT
);

var animationsArray = [firstCardMoveAnimation,secondCardMoveAnimation,firstCardRotateAnimation,secondCardRotateAnimation];

// defining animations keyframes

var moveKeys = [
	{
		frame: 0,
		value: -0.25
	},
	{
		frame: 20,
		value: -2
	}
];

var moveBackKeys = [
	{
		frame: 0,
		value: -2
	},
	{
		frame: 20,
		value: -2
	},
	{
		frame: 40,
		value: -0.25
	}
];
    		
var rotateKeys = [
	{
		frame: 0,
        value: 0
	},
	{
		frame: 20,
        value: 0
	},
	{
		frame: 40,
        value: Math.PI
	}			    
]

var rotateBackKeys = [
	{
		frame: 0,
        value: Math.PI
	},
	{
		frame: 20,
        value: 0
	}		    
]   
   
engine.runRenderLoop(function () {
	scene.render();
});

// a simple click listener
window.addEventListener("click", function (evt) {
	// with "scene.pick" we can obtain information about the stuff we picked/clicked 
	var pickResult = scene.pick(evt.clientX, evt.clientY);
	// if we haven't already picked two cards and we are picking a mesh and that mesh is called "card" and it's not picked yet...
	if(pickedCards<2 && pickResult.pickedMesh!=null && pickResult.pickedMesh.name=="card" && !pickResult.pickedMesh.picked){
		// getting card index
		var cardIndex = pickResult.pickedMesh.cardIndex;
		// set "picked" to true as we won't be able to pick it again
		cardsArray[cardIndex].picked = true;
		// storing the picked card in the array
    		pickedArray[pickedCards] = cardIndex;
    		// increase the amount of picked cards
		pickedCards++;    	
    		// adding keyframes to animation
		if(pickedCards==1){
			firstCardMoveAnimation.setKeys(moveKeys);
			firstCardRotateAnimation.setKeys(rotateKeys);
		}
		else{
			secondCardMoveAnimation.setKeys(moveKeys);
			secondCardRotateAnimation.setKeys(rotateKeys);
		}
		
		// adding animations to the card
		cardsArray[cardIndex].animations.push(animationsArray[pickedCards-1]);
    		cardsArray[cardIndex].animations.push(animationsArray[pickedCards+1]);

    		// launching animation, look at the "animCompleted" callback function
		scene.beginAnimation(cardsArray[cardIndex], 0, 40, false, 1, animCompleted);
		
    }
});

function animCompleted(){
	// increasing the number of completed animations
	animationCompleted++;
	// if the number of completed animations is 2, that is the animation of the
	// 2nd card is completed...
	if(animationCompleted==2){
		// reset animationCompleted value
		animationCompleted = 0; 
		// wait some time (a half second) before checking the match
		window.setTimeout(function(){
			if(cardsArray[pickedArray[0]].cardValue==cardsArray[pickedArray[1]].cardValue){
				// CARDS MATCH
				// remove the cards and let the player pick again
				cardsArray[pickedArray[0]].dispose();
				cardsArray[pickedArray[1]].dispose();
				pickedArray = [];
    				pickedCards=0;						
			}
			else{
				// CARDS DO NOT MATCH
				// turning back both cards, basically it's the same concept applied
				// as before when we was showing card colors
				firstCardMoveAnimation.setKeys(moveBackKeys);
				firstCardRotateAnimation.setKeys(rotateBackKeys); 
				secondCardMoveAnimation.setKeys(moveBackKeys);
				secondCardRotateAnimation.setKeys(rotateBackKeys); 
				for(i=0;i<2;i++){
					cardsArray[pickedArray[i]].animations.push(animationsArray[i]);
	    				cardsArray[pickedArray[i]].animations.push(animationsArray[i+2]);
	    				// launching animation, look at the "// launching animation, look at the "animCompleted" callback function" callback function
	    				scene.beginAnimation(cardsArray[pickedArray[i]], 0, 40, false,1,animBackCompleted);	
				}
			}
		},500);
	}
}

function animBackCompleted(){
	// increasing the number of completed animations
	animationCompleted++;
	// if the number of completed animations is 2, that is the animation of the
	// 2nd card is completed...
	if(animationCompleted==2){
		// reset animationCompleted value
		animationCompleted = 0;
		// let the player pick again
		cardsArray[pickedArray[0]].animations=[];
	    	cardsArray[pickedArray[1]].animations=[];
	    	cardsArray[pickedArray[0]].picked=false;
	    	cardsArray[pickedArray[1]].picked=false;
	    	pickedArray = [];
	    	pickedCards=0;
	}	
}

Animation callbacks are used at lines 240 and 275.

Download the source code of the full project

Get the most popular Phaser 3 book

Through 202 pages, 32 source code examples and an Android Studio project you will learn how to build cross platform HTML5 games and create a complete game along the way.

Get the book

214 GAME PROTOTYPES EXPLAINED WITH SOURCE CODE
// 1+2=3
// 100 rounds
// 10000000
// 2 Cars
// 2048
// A Blocky Christmas
// A Jumping Block
// A Life of Logic
// Angry Birds
// Angry Birds Space
// Artillery
// Astro-PANIC!
// Avoider
// Back to Square One
// Ball Game
// Ball vs Ball
// Ball: Revamped
// Balloon Invasion
// BallPusher
// Ballz
// Bar Balance
// Bejeweled
// Biggification
// Block it
// Blockage
// Bloons
// Boids
// Bombuzal
// Boom Dots
// Bouncing Ball
// Bouncing Ball 2
// Bouncy Light
// BoxHead
// Breakout
// Bricks
// Bubble Chaos
// Bubbles 2
// Card Game
// Castle Ramble
// Chronotron
// Circle Chain
// Circle Path
// Circle Race
// Circular endless runner
// Cirplosion
// CLOCKS - The Game
// Color Hit
// Color Jump
// ColorFill
// Columns
// Concentration
// Crossy Road
// Crush the Castle
// Cube Jump
// CubesOut
// Dash N Blast
// Dashy Panda
// Deflection
// Diamond Digger Saga
// Don't touch the spikes
// Dots
// Down The Mountain
// Drag and Match
// Draw Game
// Drop Wizard
// DROP'd
// Dudeski
// Dungeon Raid
// Educational Game
// Elasticity
// Endless Runner
// Erase Box
// Eskiv
// Farm Heroes Saga
// Filler
// Flappy Bird
// Fling
// Flipping Legend
// Floaty Light
// Fuse Ballz
// GearTaker
// Gem Sweeper
// Globe
// Goat Rider
// Gold Miner
// Grindstone
// GuessNext
// Helicopter
// Hero Emblems
// Hero Slide
// Hexagonal Tiles
// HookPod
// Hop Hop Hop Underwater
// Horizontal Endless Runner
// Hundreds
// Hungry Hero
// Hurry it's Christmas
// InkTd
// Iromeku
// Jet Set Willy
// Jigsaw Game
// Knife Hit
// Knightfall
// Legends of Runeterra
// Lep's World
// Line Rider
// Lumines
// Magick
// MagOrMin
// Mass Attack
// Math Game
// Maze
// Meeblings
// Memdot
// Metro Siberia Underground
// Mike Dangers
// Mikey Hooks
// Nano War
// Nodes
// o:anquan
// One Button Game
// One Tap RPG
// Ononmin
// Pacco
// Perfect Square!
// Perfectionism
// Phyballs
// Pixel Purge
// PixelField
// Planet Revenge
// Plants Vs Zombies
// Platform
// Platform game
// Plus+Plus
// Pocket Snap
// Poker
// Pool
// Pop the Lock
// Pop to Save
// Poux
// Pudi
// Pumpkin Story
// Puppet Bird
// Pyramids of Ra
// qomp
// Quick Switch
// Racing
// Radical
// Rebuild Chile
// Renju
// Rise Above
// Risky Road
// Roguelike
// Roly Poly
// Run Around
// Rush Hour
// SameGame
// SamePhysics
// Save the Totem
// Security
// Serious Scramblers
// Shrink it
// Sling
// Slingy
// Snowflakes
// Sokoban
// Space Checkers
// Space is Key
// Spellfall
// Spinny Gun
// Splitter
// Spring Ninja
// Sproing
// Stabilize!
// Stack
// Stick Hero
// String Avoider
// Stringy
// Sudoku
// Super Mario Bros
// Surfingers
// Survival Horror
// Talesworth Adventure
// Tetris
// The Impossible Line
// The Moops - Combos of Joy
// The Next Arrow
// Threes
// Tic Tac Toe
// Timberman
// Tiny Wings
// Tipsy Tower
// Toony
// Totem Destroyer
// Tower Defense
// Trick Shot
// Tunnelball
// Turn
// Turnellio
// TwinSpin
// vvvvvv
// Warp Shift
// Way of an Idea
// Whack a Creep
// Wheel of Fortune
// Where's my Water
// Wish Upon a Star
// Word Game
// Wordle
// Worms
// Yanga
// Yeah Bunny
// Zhed
// zNumbers