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.