Complete 3D HTML5 Concentration game made with Babylon.js
Talking about Concentration game, 3D, Game development, HTML5 and Javascript.
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
Never miss an update! Subscribe, and I will bother you by email only when a new game or full source code comes out.