[Tutorial] Mini Game 02 -- Duplicate & Recycle Textures and Materials
FeaturedHi everyone!
This is the second chapter of our tutorial series of a mouth catching mini game. Check out the previous chapter here:
[Tutorial] Mini Game 01 -- Infinite Instantiation, Timer, and Randomization
Today we'll get to the interactive part of our mini game. We want to essentially get distance between our mouth and each cookie, and if that distance is smaller than a specific value, we ‘eat’ the cookie by playing the cookie cracking animation on it.
Get Mouth Position
To get the position of our mouth, we’ll first create a Face Image object to get the mouth position.
We’ll have this image pinned to Mouth Center.
Then we’ll create a mouthPositionObject reference in ObjectPrefab script and link this image to it.
//@input SceneObject mouthPositionObject
We can safely turn off the Image component on our mouth image as we don’t need it.
Pro-tip: if you want other part of the face to 'catch' your object, feel free to attach this object to any other part of the face!
To get the mouth position, we’ll have to convert the 3D world position of mouth object into a 2D normalized screen position. To do so we need a reference of the camera, so we’ll create a camera component and link the mouth camera parent to it.
//@input Component.Camera camera
In the getMouthPosition() function, we’ll first get the world position of the mouth, then convert it with the worldSpaceToScreenSpace function. This will return a screen position between 0 and 1, however, the screen position scale we were using for all cookies are between -1 and 1, so we’ll need to offset our final value as well, so this is what we’ll add to our script:
function getMouthPosition(){
var mouthWorldPos = script.mouthPositionObject.getTransform().getWorldPosition();
var mouthPos = script.camera.worldSpaceToScreenSpace(mouthWorldPos);
mouthPos = new vec2(mouthPos.x*2-1, 1-mouthPos.y*2);
return mouthPos;
}
script.api.getMouthPosition = getMouthPosition;
Get Distance
We'll also create a getDistance() function to get distance between 2 points, this function can be reused whenever we try to get distance between any 2 positions.
function getDistance(pos1, pos2){
//get x y distance (screen space) between 2 points
var xDistance = Math.abs(pos1.x - pos2.x);
var yDistance = Math.abs(pos1.y - pos2.y);
//multiplies aspect ratio to y
yDistance /= aspectRatio;
//get diagonal distance
return Math.sqrt(xDistance*xDistance + yDistance*yDistance);
}
We’ll then create a threshold and a variable to track distance between each prefab and the mouth position:
var threshold = 0.1;
var distanceFromMouth = 0;
Check Distance between mouth and Target Object
In the update function, we’ll update the distanceFromMouth variable, and use an if statement with the threshold, we can have it print a message for now:
//get current position
var currentpos = screenTransform.anchors.getCenter();
currentpos.y -= fallingSpeed * getDeltaTime();
screenTransform.anchors.setCenter(currentpos);
distanceFromMouth = getDistance(currentpos, getMouthPosition());
if(distanceFromMouth < threshold){
print("HIT!")
}
Now if we run our project we should see the HIT being printed, woohoo~!
We can later make the threshold an input in ObjectSpawner, so you can adjust it while testing. Here’s what we’ll add in ObjectSpawner:
//@input float threshold {"widget":"slider","min":0, "max":0.5, "step":0.01}
function getThreshold(){
return script.threshold;
}
script.api.getThreshold = getThreshold;
Then in ObjectPrefab, instead of creating a threshold we’ll just call getThreshold():
if((distanceFromMouth < script.objectSpawner.api.getThreshold()) && !isHit){
...
}
Adding mouth open mechanism
We might also want user to only 'catch' an item when their mouth is opened. So we can create a boolean and a mouthOpen event:
var isMouthOpened = false;
script.createEvent("MouthOpenedEvent").bind(function(){
isMouthOpened = true;
});
script.createEvent("MouthClosedEvent").bind(function(){
isMouthOpened = false;
});
......
if(isMouthOpened && (distanceFromMouth < threshold)){
print("HIT!")
}
Making it compatible across all screen aspect ratios
We used a genera distance check for our 2D objects, however, because we are using normalized positions, if one side of the screen is longer than the other (for mobile devices, y would be larger than x), the influence of that side be ‘stronger’ than the other.
For example, in this graph, both distance between the 2 sphere along x and y would return 0.1, but the actual distance is a lot more different, because we are using normalized positions.
Right now if we play this game in Horizontal mode, we’ll notice it’s easier to hit cookies even when it's kind of far from the mouth along the x axis, and vice versa.
So in order for the detection to be precise across all screen proportions, we’ll multiply aspect ratio with our distance check.
To keep our script organized, we’ll create a function getDistance() with input of any 2 positions, and also get the camera aspect ratio before that.
var aspectRatio = script.camera.aspect;
function getDistance(pos1, pos2){
//get x y distance between 2 points
var xDistance = Math.abs(pos1.x - pos2.x);
var yDistance = Math.abs(pos1.y - pos2.y);
//multiplies aspect ratio to y
yDistance /= aspectRatio;
//get diagonal distance
return Math.sqrt(xDistance*xDistance + yDistance*yDistance);
}
Then in our update, we just replace
distanceFromMouth = currentpos.distance(getMouthPosition());
With
distanceFromMouth = getDistance(currentpos, getMouthPosition());
This should be it, now our mini game would also work on Snap Camera! (wuuut!)
Play cookie ‘crack’ animation when it hits mouth
Now the cookie needs some visual feedback when it’s being ‘caught’. We’ll try to switch our texture to the animated ‘cookie-broken’ texture under this if statement.
We’ll create a texture reference:
//@input Asset.Texture cookieBrokenTexture
Then we’ll link the cookie-broken texture (you’ll find it in the zip file provided in the last post) to the prefab. Under our if statement we’ll put:
script.getSceneObject().getComponent("Component.Image").getMaterial(0).mainPass.baseTex = script.cookieBrokenTexture;
Now our script would work, but this would happen:
This is because lens studio shares the same instance of materials and textures across the project. So in order to get different animation states on the same texture, we’ll need to duplicate the texture and materials, and loop them through our objects.
Duplicating and Recycling Materials and Textures
So in order to let our animation play differently at different times, we'll have to duplicate the materials and textures. We can duplicate the material in script, however, we'll have to duplicate the texture file in the resources folder.
To keep our project organized, we’ll create a dedicated script for duplicating textures and materials. We can put it on an empty scene object and call it TextureDuplicationHelper.js
Now in our script, we’ll create an array of texture inputs, and a material.
//@input Asset.Texture[] textures
//@input Asset.Material material
Then in our Resources folder, we’ll create an Unlit material, and duplicate our cookie-broken textures. 10 should be enough since there’s probably not gonna be more than 10 cookie broken at the same time on screen----unless your user is a cookie ninja, feel free to increase the amount in that case!
Then we’ll input these textures and material into our script in scene.
In our script we’ll create a function called playHitAnimation(), in this function we will:
- Clone a material from our material variable
- Assign this material to the cookie object
- Assign a texture to the material according to its index
- Increase the index, reset index if it’s smaller than total texture amount
We also have to create a textureIndex variable to keep track of the current index of our duplicated texture:
var textureIndex = 0;
function playHitAnimation(_object){
//Clone a material from our material variable
var newmaterial = script.material.clone();
//Assign this material to designated object '_object'
_object.getComponent("Component.Image").clearMaterials();
_object.getComponent("Component.Image").addMaterial(newmaterial);
//Assign a texture to the material according to its index
newmaterial.mainPass.baseTex = script.textures[textureIndex];
//play the texture animation from start
newmaterial.mainPass.baseTex.control.play(1,0);
//Increase the index, reset index if it’s smaller than total texture amount
if(textureIndex < script.textures.length - 1){
textureIndex ++;
}else{
textureIndex = 0;
}
}
//Make the function accessible for other scripts
script.api.playHitAnimation = playHitAnimation;
Then, as we did before, we’ll create a script reference in ObjectPrefab script, drag our ObjectPrefab into the scene and assign TextureDuplicationHelper to it, then apply the prefab.
We’ll also add a boolean isHit in the ObjectPrefab script, and use that so we only call the playHitAnimation function once. This is what we’ll add to ObjectPrefab:
//@input Component.ScriptComponent textureDuplicationHelper
…
if((distanceFromMouth < threshold) && !isHit){
script.textureDuplicationHelper.api.playHitAnimation(script.getSceneObject());
isHit = true;
}
Pro-tip: A good example of duplicating instances of textures is the Spectacles Depth template. Feel free to check out the AnimatedTextureMaterialDuplicationHelper.js script inside that template to learn more about this method!
We should also destroy the object after the cookie-broken texture is done playing so it doesn’t show up if we have a lot of broken textures being played. We can do so if isHit boolean is true, meaning after a cookie has been hit, we detect if its animated texture has stopped playing, then destroy the object once it has. We’ll add this after our last if statement in ObjectPrefab:
if(isHit){
var animatedTex = script.getSceneObject().getComponent("Component.Image").getMaterial(0).mainPass.baseTex;
if(animatedTex.control.isFinished()){
print("DESTROY")
script.getSceneObject().destroy();
}
}
Since we can’t visualize if an object has been deleted given that the last frame of our animated texture is an empty image, we would know it’s working if we see our message being logged.
We’re almost done for today’s tutorial, one more thing that’s cool to add is some ‘crack’ sound when cookie is being caught, just so we can make the project more ‘delicious looking’. We can go ahead and find a crack sound from sites like Freesound.org, or record our own ;) I’ve attached a sample sound in the zip below.
We’ll create an audio component on the ObjectPrefab, and link our sound to it.
Then in script, we just add this one line under our if statement when cookie is being hit:
script.getSceneObject().getComponent("Component.AudioComponent").play(1);
Now our game has sounds! Yayy!
To summarize today’s tutorial, we’ve covered:
- Getting mouth position in script
- Comparing distance from mouth position to each cookie prefab
- Duplicating materials and animated textures to avoid playing at the same instance
- Adding sound to when cookie is caught
In our next post we’ll be creating UI elements such as counter and start buttons, as well as a local storage system with persistent storage. Stay tuned!
------
Download finished project zip here.
Download the cookie crack sound here.
hi Nico S
Thank you for the effort to explain the game
i cant wait to see you next tutorial.
i made a puzzle i would love to hear from you how i did