Overview
In this tutorial we create a simple WebGL based picture pairs game, also called memory game. You need Maya with Inka3D version 1.3 or higher and a mouse with three mouse buttons. See completed project
Content creation
This chapter describes the creation of the necessary content, i.e. images and a simple Maya scene.
Images
We use 8 public domain bird images from Uni Hamburg. We crop a square region and resample it to the resolution 256x256. We store them as PNG which is a lossless image format as Inka3D automatically convertes them to JPEG. As we use the Maya image sequence feature we name them image.0.png, image.1.png and so on.Maya scene
The idea is to create a scene containing one card with a front and back side. The front side can show all eight images selectable via the index attribute of an image sequence. We also add a flip over animation that can be played by animating the time from zero to one.
First, create a polygon plane
Plane and camera
Then open the attribute editor (Window -> Attribute Editor) and edit the attributes of polyPlane1:
Now create a camera (Create -> Cameras -> Camera) and set some attributes:
Shading network
Select the plane in the view port, then select lambert1 in the attribute editor which is a shader. Set Diffuse to 1.0. Then click on the checker symbol next to the Color attribute to add a texture:
Create a Blend Colors node:
The blend colors node is used to switch between front and back side of the plane. First add a texture to the front side which is Color 2:
Create a File Texture:
Then set one image of the sequence of 8 images and switch on Use Image Sequence:
If you move the time slider you see the images in the Texture Sample field. As we want the image index to be independent of the time, we create an extra attribute for the image index. Select the plane in the viewport and then in the menu of the Attribute Editor select Attributes -> Add Attributes..., which opens the Add Attribute dialog where you add an attribute called imageIndex:
Click Ok and check in the Attribute Editor if file1 has the extra attribute:
Now edit the expression of Image Number by right-clicking on the value:
Replace time by pPlane1.imageIndex and click on the Edit button:
Now the image changes if you move the slider of the Image Index extra attribute.
To set the back of the plane go back to the blend colors node by selecting the plane in the viewport again, select lambert1 in the attribute editor and click on the arrow symbol next to the Color attribute to follow the connection to the blend colors node:
Add a texture to the back side which is Color 1:
Create a File Texture:
Set the back image:
Then set the placement attributes for the back image:
Now we can look at the shading network that we have created so far by choosing Window -> Rendering Editors -> Hypershade. Right click on the shader lambert1 and select Graph Network. This should look like this:
The last thing to do is to add a Sampler Info node that tells us if the plane is visible from the front or back. On the left side of the Hypershade, select Utilities and the Sampler Info:
Place the Sampler Info node next to the file nodes and press the middle mouse button on it, drag the mouse cursor over the Blend Colors node and release it there. In the popup menu select Other...
This opens the Connection Editor where you now click on flippedNormal on the left side and then blender on the right side to connect the attributes:
Flipover animation
Click on the plane in the viewport and make the attributes of pPlane1 visible in the attribute editor. Rename pPlane1 to card. Then Right-click on the rotateY attribute and select Create New Expression...:
Type in the expression (rotateY = 180 * smoothstep(0, 1, time);) and click Create:
Check if the plane rotates in the viewport when you move the time or press the play button next to the time bar. If it works, the maya scene is done.
Javascript programming
This chapter describes the JavaScript programming part of the game.
Step 1: Export to WebGL
Export the Maya scene using the Inka3D HTML/WebGL Exporter as Memory.html. The file export options should look like this:
This creates Memory.html, Memory.js, Memory.dat and all used textures. Now we can start to edit Memory.html. If you change the Maya scene and export it again, take care not to overwrite Memory.html. Use the Inka3D JavaScript/WebGL Exporter to export only Memory.js, Memory.dat and the textures. Note that you have to set the export options again for Inka3D JavaScript/WebGL Exporter.
If everything works you now see the animated plane turning from the front to the back side.
Step 2: Create 16 instances
Our next step is to create 16 instances for the 16 playing cards. For this we create a global array for the cards:
// array of 16 cards var cards = [];In the waitLoad function after the comment loading finished, we initialize the cards:
// create cards
for (var y = 0; y < 4; ++y)
{
for (var x = 0; x < 4; ++x)
{
// create instance of scene
var scene = Memory.createScene("Memory", group);
// get time
var time = scene.getFloatVector("time", 1);
// place this card
var translate = scene.getFloatVector("card.translate", 3);
translate[0] = 1.2 * (x - 1.5);
translate[1] = 1.2 * (y - 1.5);
// store card in global array
cards[x + 4 * y] = {scene: scene, time: time, imageIndex: imageIndex};
}
}
The two for loops iterate over the 4x4 cards. For each card an instance of scene
Memory is created in the render group. Then the time and image index parameters
are retrieved which are of type Float32Array of length 1. Also the cards are placed
by retrieving the translate attribute (type Float32Array of length 3) and setting
the x and y component which are in translate[0] and translate[1]. Then the info for
the current card is stored in the global cards array for later use.
In the drawScene function we use the time to animate all 16 cards:
// animate cards
for (var i = 0; i < 16; ++i)
{
cards[i].time[0] = time;
}
Now we have 4x4 cards.
Step 3: Shuffle cards
Now we assign each of the 8 images to two cards and shuffle them. For this we get the image index in the creation loops:
// get image index
var imageIndex = scene.getIntVector("card.imageIndex", 1);
The imageIndex variable is of type Int32Array of length 1. An additional function
called initCards assigns and shuffles the images:
function initCards()
{
// initialize cards
for (var i = 0; i < 16; ++i)
{
cards[i].imageIndex[0] = i / 2;
}
// shuffle cards
for (var i = 0; i < 20; ++i)
{
// select two cards
var a = Math.floor(Math.random() * 16);
var b = Math.floor(Math.random() * 16);
// swap two cards
var tmp = cards[a].imageIndex[0];
cards[a].imageIndex[0] = cards[b].imageIndex[0];
cards[b].imageIndex[0] = tmp;
}
}
Now we have shuffled cards with each image occuring exactly twice.
Step 4: Turning cards with mouse click
To turn cards over on mouse click, we get the id of each card shape and also add a side flag that indicates if the card is on front or back side in the card creation loops:
// get id of card for picking
var id = scene.getObjectId("cardShape[0]");
// store card in global array
cards[x + 4 * y] = {
scene: scene,
time: time,
imageIndex: imageIndex,
id: id,
side: false}; // false = back side visible, true = front side visible
Then we add two global variables for the mouse
position in device coordinates, i.e. ranging from -1 to 1:
// mouse position in device coordinates (ranges from -1 to 1) var mouseX = 0; var mouseY = 0;These get set from the mousePosition event handler and used in the mouseDown event handler:
function mousePosition(e)
{
var w = canvas.offsetWidth;
var h = canvas.offsetHeight;
// mouse position in device space (ranges from -1 to 1)
mouseX = (e.pageX + 0.5) / w * 2.0 - 1.0;
mouseY = -(e.pageY + 0.5) / h * 2.0 + 1.0;
return true;
}
function mouseDown(e)
{
// pick card in group using current camera and device space mouse position
var id = group.pick(viewMatrix, projectionMatrix, mouseX, mouseY);
for (var i = 0; i < 16; ++i)
{
var card = cards[i];
if (card.id == id)
{
// turn card to front side (does not influence appearance)
card.side = true;
}
}
}
These event handlers get called by the browser if we install them, which we do at the
end of the waitLoad function to ensure that the cards are already initialized when they
get called the first time:
// install mouse handlers document.onmousemove = mousePosition; document.onmousedown = mouseDown;The mouseDown handler does a pick on the group of 16 cards. The id is compared with all cards to see if one was hit. The one that was clicked on gets turned to the front side. This is indicated by setting card.side to true. This does not change the graphical representation yet. This gets changed by smoothly animating the time parameter of the card from 1 to 0 to let it rotate from the back to the front side. For this we calculate a time delta and subtract it from the time parameter of all cards that are on the front side and clamp it at zero:
// get time
var time = new Date().getTime() / 1000.0;
var timeDelta = time - lastTime;
lastTime = time;
// animate cards
for (var i = 0; i < 16; ++i)
{
var card = cards[i];
if (card.side)
{
// rotate card to front side by animating time of card from 0 to 1
card.time[0] = Math.max(0, card.time[0] - timeDelta);
}
}
Now we can turn all cards to the front side by clicking on them.
Step 5: Adding game rules
The last step is do add some game rules. On each turn you can turn two cards from the back to the front side. If they are equal, they stay up and you get another turn. Otherwise they are reverted to the back side after a short time and you have to try again. For this delay we add a config variable:
// config variables // delay in seconds for two mismatching cards to be turned back var turnDelay = 2;We also need two global variables for the two cards we can turn over and a time that indicates when the cards have to be turned back if they didn't match:
// first and second card turned over in a turn var firstCard = null; var secondCard = null; // time at which the two cards are turned back var turnTime;The mouseDown event handler now gets a bit more complicated:
function mouseDown(e)
{
// no actions allowed as long as two cards are already turned over
if (secondCard == null)
{
// pick card in group using current camera and device space mouse position
var id = group.pick(viewMatrix, projectionMatrix, mouseX, mouseY);
for (var i = 0; i < 16; ++i)
{
var card = cards[i];
if (card.id == id)
{
// check if this card is still on the back side
if (!card.side)
{
// turn card to front side (does not influence appearance)
card.side = true;
if (firstCard == null)
{
// first card: store it
firstCard = card;
}
else
{
// second card: decide if matching or not
if (card.imageIndex[0] == firstCard.imageIndex[0])
{
// good brain: you get a second turn ;-)
firstCard = null;
}
else
{
// oh no: store second card to turn it back again
secondCard = card;
turnTime = new Date().getTime() / 1000.0 + turnDelay;
}
}
}
}
}
}
}
At first we check if the second card has turned over. If yes, no more actions are possible.
If a card has been clicked on that is still on the back side, we now check if it is the
first or the second one. If it is the first one we simply store it, if it is the second
one we check if the images match or not. If they match we clear the first card as two new
cards can be turned over. If it does not match we store it and set the time when the
cards should turn back.
In the drawScene function we check for this timeout and set the cards to back side if
it has been reached:
// check if two mismatching cards are turned over and the time to turn them back has been reached
if (secondCard != null && time > turnTime)
{
firstCard.side = false;
secondCard.side = false;
firstCard = null;
secondCard = null;
}
// animate cards
for (var i = 0; i < 16; ++i)
{
var card = cards[i];
if (card.side)
{
// rotate card to front side by animating time of card from 1 to 0
card.time[0] = Math.max(0, card.time[0] - timeDelta);
}
else
{
// rotate card to back side by animating time of card from 0 to 1
card.time[0] = Math.min(1, card.time[0] + timeDelta);
}
}
Also the time of cards now gets animated that are on the back side that were previously
on the front side.
Now we can play the game!
Step 6: Optimization
Memory.js, the JavaScript file for the exported Maya scene, has a size of 60717 bytes. This can be optimized by reducing the number of attributes that can be controlled from JavaScript. Therefore we export the scene from Maya again using Inka3D JavaScript/WebGL Exporter and only make the attributes of card (and all cameras) available:
Now Memory.js has only 36503 bytes and still works for our purpose. Note: List of Nodes is a comma separated list of Maya node names.