[Tutorial] Mini Game 03 -- Game States, UI Scores, and Persistent Storage
FeaturedHello everyone!
This is the third chapter in our tutorial series of a mouth catching mini game. Check out the previous chapters here:
[Tutorial] Mini Game 01 -- Infinite Instantiation, Timer, and Randomization
[Tutorial] Mini Game 02 -- Duplicate & Recycle Textures and Materials
Today we will continue working on the same mini game and make it more polished by adding UI elements to it.
In this chapter we will set up:
- A start button -- game does not start until user taps the button
- A game over state -- a state of failure with a game-over screen
- A counter -- counting how many cookies have been caught
- A top score -- using persistent storage to keep track of the highest score ever made
Adding a Start button
A start button is essential in a lot of mini games. Your users may find it overwhelming if they aren’t given some time to prepare before the game officially starts.
To create a start button. We’ll create another Full Frame Region and assign its Screen Region as Safe Render. We’ll also create a screen image in it and give it the button image asset provided in the zip below.
We’ll also create a ScreenText object and position it inside the Button object, assign a font to it and refill the text field as ‘start’.
We want our game to start (aka cookies start to drop) when this button has been pressed. The easiest way to do so is by sending a global trigger with Behavior script, then in our ObjectSpawner script we’ll write a function that turns on a boolean which will allow cookie dropping to happen.
Let’s create a behavior script by pressing the ‘+’ button in the Objects panel and type Behavior.
A new object will be created in the scene with the behavior script attached. Let's move this object to top of our Objects list.
It is good practice to keep Behavior object at top of the screen. Scripts in Lens Studio gets executed in an order from top to bottom. If we are trying to call a function in Behavior script in our own script, and our script gets executed before Behavior does, we might get a 'missing reference' error.
We'll create a custom string for our trigger, remember we'll add a response of this trigger in our scripts later, so we should use something distinctive and easy-to-remember, such as 'START_GAME'.
Now if we open up Behavior script, at the top there are few lines of commented code that shows the syntax for working with Behavior script in any custom scripts.
In order to add a custom response to triggers sent out by Behavior script, we will call this function:
global.behaviorSystem.addCustomTriggerResponse(triggerName, callback)
We'll add this to our ObjectSpawner script, and also add a startGame function to be called on this trigger. In our startGame function we'll enable a startgame boolean variable, and we'll add that boolean in our Update event so that nothing will happen unless startgame boolean variable is set to true.
global.behaviorSystem.addCustomTriggerResponse("START_GAME", startGame);
var startgame = false;
function startGame(){
print("start")
startgame = true;
}
...
script.createEvent("UpdateEvent").bind(function(){
if(startgame){
...
}
});
If you refresh the lens, nothing should happen unless you press the start button. You should see cookie dropping and 'start' being printed in the console when start button is pressed, that means our script is working!
One last thing is that we'll disable the start button after it is being clicked. We can create a reference of the button in our script, and simply disable it in our startGame function.
//@input SceneObject startButton
function startGame(){
...
script.startButton.enabled = false;
}
Now our button will disappear when we tap it, and our game will start!
Adding a Score Counter
Now this still doesn't quite feel like a game without keeping track of a score. We'll add that right now.
We'll create 2 screen texts, one we'll put 'Score:' as its text field, another just '0'. And we'll give them a parent with just a ScreenTransform property.
A good way to arrange UI element is to change the Pin to Edge and Fix Size settings. In this case we want the score counter to be pinned to the left (or right) side of the screen, and we want its size to be fixed. So we'll set its Pin to Edge and Fix Size as such:
For its children score , we'll set Pin to Edge for all sides and leave all paddings to 0. For scoreNumber, we'll give it some padding on the left and negative padding on the right, we'll also set its Horizontal Alignment to left. In this way when number increases it would flow towards right which is the natural direction.
For more information about ScreenTransform, check out this page!
We'll also create a currentScore variable, we'll increase this number when a cookie is caught. Since our cookie caught action is happening in the ObjectPrefab script, we'll create an api reference to a function, we can call this function OnHit().
Inside the OnHit function, we'll also update the text field of scoreNumber when we update currentScore, so we'll need to create a reference for scoreNumber as well. Therefore, we'll add this to our script:
//@input Component.Text currentScoreNumber
var currentScore = 0;
function OnHit(){
if(startgame){
currentScore ++;
script.currentScoreNumber.text = currentScore.toString();
}
}
script.api.OnHit = OnHit;
Then we'll link scoreNumber object into the currentScoreNumber slot in the Inspector.
Finally we'll call the OnHit function in ObjectPrefab script. We'll do so under the if statement where objects are hit:
if((distanceFromMouth < script.objectSpawner.api.getThreshold()) && !isHit){
print("HIT!")
script.objectSpawner.api.OnHit();
script.textureDuplicationHelper.api.playHitAnimation(script.getSceneObject());
audioComponent.play(1);
isHit = true;
}
Now we should have it! We'll see the score being updated when a cookie is hit!
Adding a Game Over State
The mini game would not make that much sense if there isn't a game-over state! There are a few ways we can set up a game-over state. We can make it timer based where we give users an amount of time to gather cookies, or make it 'fail amount' based, meaning the game will be over when certain amount of cookies are failed to be caught.
In this case, we won't use the timer based mechanism because we have used a random spawning system at the beginning, so every time the amount of cookie spawned might not be exactly the same. But we can totally implement an amount based fail system.
Remember when we deleted the object in ObjectPrefab when it falls beyond the screen? We'll just add another function in ObjectSpawner and call it whenever a cookie gets deleted in ObjectPrefab. It's very much similar logic with currentScore. We can create a variable called missedScore and a function called OnMissed().
var missedScore = 0;
function OnMissed(){
if(startgame)
missedScore ++;
}
script.api.OnMissed = OnMissed;
Then in ObjectPrefab, we'll call this under the if statement that destroys the object (it's important to call the function before the object destroys itself):
if(currentpos.y < -1.5){
script.objectSpawner.api.OnMissed();
script.getSceneObject().destroy();
}
We can set a missedScoreMax variable to be 5, meaning game will be over if more than 5 cookies are missed. We can expose this variable so we can change it later in Inspector. We created the startgame boolean that gets turned on when game has started earlier, now we can turn it off when game is over, and we'll do this under the condition where startgame is true, so it'll only be called once.
We'll add these to our script:
//@input float missedScoreMax
var missedScoreMax = script.missedScoreMax;
var missedScore = 0;
script.createEvent("UpdateEvent").bind(function(){
if(startgame){
...
if(missedScore >= missedScoreMax){
print("game is over!");
startgame = false;
}
...
}
});
Try running the game now, you should see 'game is over!' being printed after 5 cookies are missed!
We should also add a game-over screen now, let's do so by creating another object and put a ScreenText saying 'GAME OVER!' in it.
We'll add this to our ObjectSpawner script:
//@input SceneObject gameOverScreen
script.gameOverScreen.enabled = false;
script.createEvent("UpdateEvent").bind(function(){
if(startgame){
...
if(missedScore >= missedScoreMax){
script.gameOverScreen.enabled = true;
startgame = false;
}
...
}
});
Now we'll see this giant red 'GAME OVER!' text when game is over (in case anyone misunderstands...)
Now there is no visual indication of the failing system we're using here, so users have no way to find out why game is over. What we can do is add another text on screen showing missed cookie amount.
We'll do so by duplicating our score number, and change its text to 'missed:'. We'll link it to our script and update it from the missedScore variable the exact same way we did for score number with the currentScore variable.
//@input Component.Text missedScoreNumber
function OnMissed(){
if(startgame){
missedScore ++;
script.missedScoreNumber.text = missedScore.toString();
}
}
Feel free to change your text color accordingly, and now our game looks like this (^_^)
Adding Top Score with Persistent Storage
Finally we'll set up a Top Score with persistent storage. Click here to check out more about persistent storage in Lens Studio!
We'll first set up a Top Score object same way we did for current score and missed score. We'll have it on the top (since it's top score).
In our script we'll also create a variable called topScore, a reference to the top score number topScoreNumber.
Persistent storage works in pairs of key and value. Each value is stored with a specific key to identify this value. In this way we can store many different values for a complicated lens, and assign different key for each.
When lens is turned on we want to 'fetch' value from stored value for topScore. So this is what we'll put on top of the ObjectSpawner script:
//@input Component.Text topScoreNumber
//create reference to global persistent storage system
var store = global.persistentStorageSystem.store;
//create a key to identify the variable to be saved
var scoreKey = "topScore";
//get from saved variable every time lens starts
topScore = store.getInt(scoreKey);
script.topScoreNumber.text = topScore.toString();
Then when we update score values, we'll add an if statement checking if the current score value is larger than our top score, if so then we will update the top score value:
function OnHit(){
if(startgame){
currentScore ++;
script.currentScoreNumber.text = currentScore.toString();
if(currentScore > topScore){
//update top score when current score exceeds top score
topScore = currentScore;
//store top score in persistent storage
store.putInt(scoreKey, topScore);
//update UI
script.topScoreNumber.text = topScore.toString();
}
}
}
Don't forget to link the topScoreNumber object into the script, and now our top score should work like a dream!
Whenever game starts it always shows highest score ever achieved:
When current score is higher it will update its value:
That's it for today's tutorial!
To summarize, today we've added:
- A start button -- game start mechanism
- A game-over screen -- game end mechanism
- A current score counter
- A failed/missed score counter
- A top score counter with persistent storage
We added these elements so that our lens would be more user friendly.
For example, a start button can give our users more time to prepare and get ready before the game starts, it's recommended to have it in any mini games.
We also added a few counter UI elements so that player's progress and outcomes can be tracked in real time.
We've added top score with persistent storage so players can compare their score with the highest. It's a great way to motivate players to strike better scores, and come back to play more.
------
Download finished project zip here.
Download assets here.
This is an awesome resource and a great tutorial!
I have a question, is there a way to restart the lens/game without having to go to another lens in the carousel and then returning to the cookie game lens?
Ideally I would like to have the game reset with a clickable button or really any trigger would work.
Thanks again for this awesome lens and I look forward to hopefully solving this minor issue with your help!
dam this is an easy approach than what i did . So i got a question in my game what i did was i used multiple update function and it was causing some lag issue after 30 second in the game play or even in the lens studio . Is there a optimised way to use update function .
https://support.lensstudio.snapchat.com/hc/en-us/community/posts/360064589991 sadly my game was a 3d based so i used collison detection for it . so that function also has update .
Hi, Kavin Kumar!
The way to optimize would be to keep track of all objects in one script and update the list of object rather than each o them having its own script.
Maybe try to use distance check instead of collision detection? Assume that all objects are spheres with some radius.
Also make sure to delete SceneObjects when they are out of the screen.
I tried your lens and it shouldn't get so slow! Feel free to share details, i believe it can be optimized!
Best
Olha
hi Olha
so i have 6 different 3d models and all the models are being recycled . during the time of the game i didn't instantiate the prefab . What i did was i was moving the each object with different components for movement . Even tho the script is same but i used separate script for each model in order to give them time between each fall . And upon that i have separate script for rotation of the model to look like falling . but rest of the things are similar to the one above .
thanks
kavin
Hi Colin Tucker
Having a restart setting is a great suggestion! We can safely add a restart button to the game once 'Game Over' screen is shown. I will have that added to the next chapter of this tutorial series.
Thanks for the suggestion! Great idea ^_^
Best,
Nico