Great games are not merely games, they are experiences. All aspects of a game, including sound, music, user interface and gameplay, combine to create that experience, so it’s important to apply as much polish as you can to each aspect to create the best experience.
In this chapter we explore the implementation of Snail Bait’s user interface, starting with the simple task of tracking and displaying the game’s score and ending with something a little more complicated: displaying a winning animation when the player wins the game. In between, we discuss how to let players tweet their score, how to warn players when the game runs slowly, and how to show credits at the end of the game.
In this chapter you will learn how to do the following:
• Keep score (Section 17.1 on p. 444)
• Add a lives indicator to the game (Section 17.2 on p. 448)
• Display credits (Section 17.3 on p. 454)
• Tweet player scores (Section 17.4 on p. 461)
• Warn players when the game runs slowly (Section 17.5 on p. 464)
• Implement a winning animation (Section 17.6 on p. 473)
When the runner collides with an asset, meaning a coin, ruby, or sapphire, Snail Bait increases the game’s score, which it displays above the game’s canvas, as shown in Figure 17.1.
Snail Bait keeps track of the score as follows:
• Adds a score
HTML element
• Specifies CSS for the score
element
• Accesses the score
element in JavaScript
• Creates a score
variable and initialize it to zero
• Assigns values to sprites that are assets
• Updates the score
variable and score
element when the runner collides with an asset
First, we add a DIV
HTML element to Snail Bait’s HTML and initialize its value to zero, as shown in Example 17.1.
<!DOCTYPE html>
<html>
...
<body>
...
<div id='snailbait-arena'>
...
<div id='snailbait-score'>
0
</div>
...
</div>
</body>
</html>
The CSS for the score
element is listed in Example 17.2.
#snailbait-score {
font: 46px fantasy;
text-align: center;
color: yellow;
text-shadow: 2px 2px 4px navy;
-webkit-transition: opacity 5s;
-moz-transition: opacity 5s;
-o-transition: opacity 5s;
transition: opacity 5s;
opacity: 0;
display: none;
}
The preceding CSS configures the score
element’s font, specifies a CSS transition, and makes the element initially invisible. That transition takes effect when Snail Bait sets the score
element’s opacity
property to 1.0
after gameplay begins, as discussed in [Missing XREF!].
With the HTML and CSS in place for the score
element, Snail Bait accesses the element in the game’s constructor, as shown in Example 17.3. The constructor creates the score
variable and initializes it to zero.
var SnailBait = function () {
...
this.scoreElement = document.getElementById('snailbait-score');
this.score = 0;
...
};
Snail Bait assigns values to assets in its setSpriteValues()
method. The game invokes that method from initializeSprites(),
as you can see from Example 17.4.
SnailBait.prototype = {
...
setSpriteValues: function() {
var sprite,
COIN_VALUE = 100,
SAPPHIRE_VALUE = 500,
RUBY_VALUE = 1000;
for (var i = 0; i < this.sprites.length; ++i) {
sprite = this.sprites[i];
if (sprite.type === 'coin') {
sprite.value = COIN_VALUE;
}
else if (sprite.type === 'ruby') {
sprite.value = RUBY_VALUE;
}
else if (sprite.type === 'sapphire') {
sprite.value = SAPPHIRE_VALUE;
}
}
},
initializeSprites: function() {
...
this.setSpriteValues();
...
},
...
};
When the runner collides with an asset, the runner’s collide behavior increments the game’s score and updates the score
element, as shown in Example 17.5.
var SnailBait = function () {
...
this.collideBehavior = {
adjustScore: function (sprite) {
if (sprite.value) {
snailBait.score += sprite.value;
snailBait.updateScoreElement();
}
},
processAssetCollision: function (sprite) {
sprite.visible = false; // sprite is the asset
if (sprite.type === 'coin')
snailBait.playSound(snailBait.coinSound);
else
snailBait.playSound(snailBait.chimesSound);
this.adjustScore(sprite);
},
...
};
...
};
SnailBait.prototype = {
...
updateScoreElement: function () {
this.scoreElement.innerHTML = this.score;
},
...
};
When the runner collides with an asset, the collide behavior’s processAssetCollision()
method plays a sound and adjusts the game’s score.
Now that you’ve seen how to keep score, let’s see how to add a lives indicator.
When Snail Bait begins, players have three lives. When a player collides with a bat or a bee or falls through the bottom of the game, the player loses a life. Snail Bait displays the number of remaining lives above and to the left of the game’s canvas, as shown in Figure 17.2.
Adding the lives indicator involves the following steps:
• Add HTML elements for each life indicator and their containing DIV
• Specify CSS for the elements
• Access the elements in JavaScript
• Reveal the elements when the game starts
• Update the elements when the player loses a life
As you can see from Example 17.6, each life indicator is an image. The images, three in all, reside in a DIV
with the identifier lives
.
<!DOCTYPE html>
<html>
...
<body>
...
<div id='snailbait-arena'>
...
<div id='snailbait-lives'>
<img id='snailbait-life-icon-left'
src='images/runner-small.png'/>
<img id='snailbait-life-icon-middle'
src='images/runner-small.png'/>
<img id='snailbait-life-icon-right'
src='images/runner-small.png'/>
</div>
...
</div>
...
</body>
</html>
The three images and the DIV
are shown in Figure 17.3 with white borders around each element.
The CSS for those elements (without the white borders) is shown in Example 17.7.
#snailbait-lives {
position: absolute;
margin-top: 20px;
margin-left: 5px;
-webkit-transition: opacity 5s;
-moz-transition: opacity 5s;
-o-transition: opacity 5s;
transition: opacity 5s;
display: none;
opacity: 0;
}
#snailbait-life-icon-left {
-webkit-transition: opacity 5s;
-moz-transition: opacity 5s;
-o-transition: opacity 5s;
transition: opacity 5s;
}
#snailbait-life-icon-middle {
-webkit-transition: opacity 5s;
-moz-transition: opacity 5s;
-o-transition: opacity 5s;
transition: opacity 5s;
}
#snailbait-life-icon-right {
-webkit-transition: opacity 5s;
-moz-transition: opacity 5s;
-o-transition: opacity 5s;
transition: opacity 5s;
}
All four elements have a CSS transition on the opacity
property. When Snail Bait changes that property’s value, the browser smoothly animates the associated element from the current opacity to the new opacity. Snail Bait sets the opacity for the lives
element when the game starts, fading the elements in, and fades them out temporarily when a player loses a life.
Snail Bait’s constructor function obtains references to the four elements as shown in Example 17.8.
var SnailBait = function () {
...
this.livesElement =
document.getElementById('snailbait-lives');
this.lifeIconLeft =
document.getElementById('snailbait-life-icon-left');
this.lifeIconMiddle =
document.getElementById('snailbait-life-icon-middle');
this.lifeIconRight =
document.getElementById('snailbait-life-icon-right');
...
};
As Snail Bait loads resources, the score element and the lives indicator elements—collectively referred to as the game’s top chrome—fade in, but only to 0.25
opacity to focus the player’s attention on the game’s bottom chrome, which contains the game’s instructions. A short time later, the game fades the top chrome to 1.0
opacity.
Example 17.9 shows the revised listings of Snail Bait’s revealTopChrome()
and revealTopChromeDimmed()
methods, originally listed in Chapter 5, which now manipulate the lives
element instead of the defunct frames/second indicator that has occupied the upper left-corner of the game until now.
SnailBait.prototype = {
...
revealTopChrome: function () {
this.fadeInElements(this.livesElement,
this.scoreElement);
},
revealTopChromeDimmed: function () {
var DIM = 0.25;
this.scoreElement.style.display = 'block';
this.livesElement.style.display = 'block';
setTimeout( function () {
snailBait.scoreElement.style.opacity = DIM;
snailBait.livesElement.style.opacity = DIM;
}, this.SHORT_DELAY); // 50 ms
},
...
};
The revealTopChrome()
method invokes Snail Bait’s fadeInElements()
method, which takes a variable-length list of elements. The fadeInElements()
method fades elements in by setting their opacity to 1.0;
for the fade-in animation, the method relies on CSS transitions attached to the elements.
The revealTopChromeDimmed()
method sets the opacity for the score
and lives
elements to 0.25
instead of 1.0,
so instead of invoking fadeInElements(),
which makes elements fully opaque, revealTopChromeDimmed()
fades elements in by hand. Like revealTopChrome(), revealTopChromeDimmed()
relies on CSS transitions attached to the elements it manipulates.
When the player loses a life, Snail Bait’s loseLife()
method decrements the lives
property and updates the lives
element, as shown in Example 17.10.
SnailBait.prototype = {
...
loseLife: function () {
...
this.lives--;
this.updateLivesElement();
...
}
...
};
The updateLivesElement()
method is listed in Example 17.11.
SnailBait.prototype = {
...
updateLivesElement: function () {
if (this.lives === 3) {
this.lifeIconLeft.style.opacity = snailBait.OPAQUE;
this.lifeIconMiddle.style.opacity = snailBait.OPAQUE;
this.lifeIconRight.style.opacity = snailBait.OPAQUE;
}
else if (this.lives === 2) {
this.lifeIconLeft.style.opacity = snailBait.OPAQUE;
this.lifeIconMiddle.style.opacity = snailBait.OPAQUE;
this.lifeIconRight.style.opacity = snailBait.TRANSPARENT;
}
else if (this.lives === 1) {
this.lifeIconLeft.style.opacity = snailBait.OPAQUE;
this.lifeIconMiddle.style.opacity = snailBait.TRANSPARENT;
this.lifeIconRight.style.opacity = snailBait.TRANSPARENT;
}
else if (this.lives === 0) {
this.lifeIconLeft.style.opacity = snailBait.TRANSPARENT;
this.lifeIconMiddle.style.opacity = snailBait.TRANSPARENT;
this.lifeIconRight.style.opacity = snailBait.TRANSPARENT;
}
},
...
};
The updateLivesElement()
method sets the opacity for each life indicator according to the number of lives remaining, which triggers a smooth animation from the previous opacity by virtue of the elements’ CSS transition. Snail Bait’s OPAQUE
and TRANSPARENT
constants are 1.0
and 0
, respectively.
Now that you’ve seen how Snail Bait implements the lives indicator, let’s see how it displays credits.
The user interface effects in this chapter use Snail Bait’s fadeInElements()
and fadeOutElements()
methods. Those methods, which in turn rely on CSS transitions attached to the elements they manipulate, are discussed in Chapter 5.
Snail Bait displays credits at the end of the game, as shown in Figure 1.4.
The game implements credits with the following steps:
• Add HTML elements for the credits
• Specify CSS for the credit HTML elements
• Access credits HTML elements in JavaScript
• Implement hideCredits()
and revealCredits()
methods
• Modify gameOver()
to reveal credits
• Modify restartGame()
to hide credits
• Add a click event handler for the credit’s Play again
link that calls restartGame()
As it does for keeping score and displaying life indicators, Snail Bait implements credits by specifying HTML elements and their corresponding CSS and subsequently accessing those elements in the game’s constructor. The HTML for the credits is listed in Example 17.12.
<!DOCTYPE html>
<html>
...
<body>
...
<div id='snailbait-arena'>
<!-- Credits...............................................-->
<div id='snailbait-credits' class='snailbait-credits'>
<div class='snailbait-heading'>Credits</div>
<hr></hr>
<div class='snailbait-credit'>
<div id='snailbait-art-title'
class='snailbait-title'>
~ Art ~
</div>
<div class='snailbait-attribution'><b>
<a href='http://lea.verou.me/css3patterns'>
CSS3 Patterns Gallery
</a>:
</b> Background pattern in CSS by Anna Kassner
</div>
<div class='snailbait-attribution'><b>
<a href='http://bit.ly/kNzDVc'>
Replica Island
</a>:
</b> All graphics except the runner and coins
</div>
<div class='snailbait-attribution'>
<b>MJKRZAK People's Sprites:</b>
Runner spritesheet (Link no longer available)
</div>
<div class='snailbait-attribution'><b>
<a href='http://bit.ly/QvGRVR'>
LoversHorizon at deviantART
</a>:
</b> Coins
</div>
</div>
<div class='snailbait-credit'>
<div id='snailbait-sound-and-music-title'
class='snailbait-title'>
~ Sound and Music ~
</div>
<div class='snailbait-attribution'>
<a href='http://bit.ly/LFtwr8'>
Pete Marquardt:
</a>
Soundtrack from soundclick.com
</div>
<div class='snailbait-attribution'>
<a href='http://bit.ly/kNzDVc'>
Replica Island:
</a>
Sound effects
</div>
</div>
<p>
<!-- Tweet my score link omitted -->
<a id='snailbait-play-again-link'
href='#'>Play again</a>
</p>
</div> <!-- end of credit -->
...
</div> <!-- end of arena -->
</body>
</html>
The preceding HTML creates a hierarchy of DIVs where an enclosing credits
DIV contains credit
DIVs. The credit
DIVs in turn contain attribution
DIVs. At the end of the credits
DIV, a paragraph
element contains the Tweet my score and Play again links. The HTML for the Tweet my score link is omitted from Example 17.12; instead, it is discussed in Section 17.4, “Tweet Player Scores,” on p. 461.
The CSS for the credits elements is listed in Example 17.13.
.snailbait-credits {
position: absolute;
margin-left: 50px;
margin-top: 10px;
padding-bottom: 30px;
font: 20px fantasy;
text-align: center;
width: 650px;
height: 23em;
background: rgba(255,255,230,0.75);
border: thin solid blue;
padding-top: 10px;
padding-left: 40px;
padding-right: 40px;
-webkit-transition: opacity 2s;
-moz-transition: opacity 2s;
-o-transition: opacity 2s;
transition: opacity 2s;
-webkit-box-shadow: rgba(0,0,0,0.5) 8px 8px 16px;
-moz-box-shadow: rgba(0,0,0,0.5) 8px 8px 16px;
-o-box-shadow: rgba(0,0,0,0.5) 8px 8px 16px;
box-shadow: rgba(0,0,0,0.5) 8px 8px 16px;
border-radius: 10px;
opacity: 0;
display: none;
z-index: 1;
}
#snailbait-credits a:hover {
color: blue;
text-shadow: 1px 1px 1px rgba(0,0,200,0.5);
}
#snailbait-credits p {
margin-top: 20px;
margin-bottom: 10px;
font: 24px fantasy;
text-shadow: 1px 1px 1px rgba(0,0,0,0.8);
color: blue;
text-shadow: 1px 1px 1px rgba(255,255,255,0.6);
}
#snailbait-credits .attribution {
font: 18px fantasy;
color: blue;
text-shadow: 1px 1px 1px rgba(255,255,255,0.6);
}
#snailbait-credits .title {
margin-bottom: 10px;
font-size: 22px;
color: blue;
text-shadow: 1px 1px 1px rgba(255,255,255,0.6);
}
#snailbait-new-game-link {
margin-top: 10px;
float: right;
margin-right: 20px;
font-size: 0.9em;
}
#snailbait-art-title {
margin-top: 20px;
}
#snailbait-sound-and-music-title {
margin-top: 20px;
}
#snailbait-credits .snailbait-heading {
margin-bottom: 10px;
font-size: 35px;
font-family: fantasy;
color: blue;
text-shadow: 2px 2px 2px rgba(255,255,255,0.8);
}
.snailbait-tweet-link {
margin-top: 10px;
margin-left: 20px;
float: left;
font-size: 0.9em;
}
#snailbait-play-again-link {
margin-top: 10px;
float: right;
margin-right: 20px;
font-size: 0.9em;
}
As we’ve done previously with other DIVs that the game fades in and out, the credits
DIV is initially invisible. A CSS transition smoothly animates the DIV’s opacity when the game sets that property. Additionally, the border-radius
and box-shadow
properties give the credits
element rounded corners and a drop shadow.
Besides the two links, Snail Bait does not manipulate any of the elements inside the credits
element because those elements merely display static information. In its constructor function, Snail Bait obtains references to the credits
element and the Play again link, as shown in Example 17.14.
var SnailBait = function () {
...
// Credits...........................................................
this.creditsElement =
document.getElementById('snailbait-credits');
this.newGameLink =
document.getElementById('snailbait-play-again-link');
...
};
Snail Bait’s revealCredits()
and hideCredits()
methods, listed in Example 17.15 use the game’s fadeInElements()
and fadeOutElements()
methods, respectively.
SnailBait.prototype = {
...
revealCredits: function () {
this.fadeInElements(this.creditsElement);
...
},
hideCredits: function () {
var FADE_DURATION = 1000;
this.fadeOutElements(this.creditsElement, FADE_DURATION);
},
...
};
The gameOver()
method, listed in Example 17.16, reveals the game’s credits, drastically slows the rate at which the background scrolls, and sets the game’s playing
property to false,
which causes the game to disregard the player input.
SnailBait.prototype = {
...
gameOver: function () {
this.revealCredits();
this.bgVelocity = this.BACKGROUND_VELOCITY / 20;
this.playing = false;
...
},
...
};
When the game restarts, it hides credits, as shown in Example 17.17.
SnailBait.prototype = {
...
restartGame: function () {
this.hideCredits();
...
},
...
};
The game restarts when the player clicks the Play again link, as shown in Example 17.18.
snailBait.newGameLink.onclick = function (e) {
snailBait.restartGame();
};
The preceding event handler restarts the game when the player clicks the Play again link. Next, let’s see how to implement the functionality behind the Tweet my score link.
The credits and running slowly warning both have a z-index
of one. That places those elements above the game’s canvas, which by default, has a z-index
of zero.
Twitter’s Web Intents are the simplest way to let players tweet about your game. Snail Bait lets players tweet their scores by clicking on the Tweet my score link in the game’s credits, as shown in Section 17.3, “Display Credits,” on p. 454.
Figure 17.5 shows Twitter’s documentation for Web Intents.
Using Web Intents is simple. Create a link that points to the Web Intents URL, with parameters for things such as the tweet’s text. As you can see from Figure 17.6, when players click the link, the Twitter intent opens a webpage that makes it easy for them to sign into their Twitter account and tweet (the sign in is not necessary if the player is already logged into Twitter).
Snail Bait implements the Tweet my score link with the following steps.
• Add a link to the game’s HTML.
• Access the link in JavaScript.
• Set the link’s href
property to a twitter intent URL.
Example 17.19 shows the Tweet my score link in the game’s HTML. The link’s target
property is _blank
, so the browser opens the linked page in a new window or tab, as you can see in Figure 17.6.
<!DOCTYPE html>
<html>
...
<body>
...
<div id='snailbait-arena'>
...
<div id='snailbait-credits'>
...
<p>
<a id='snailbait-tweet' href=""
hashtags='#html5' target='_blank'
class='snailbait-tweet-link'>
Tweet my score</a>
...
</p>
</div> <!-- end of credits -->
</div>
...
</body>
</html>
Snail Bait’s constructor accesses the link and declares two string constants, as shown in Example 17.20.
var SnailBait = function () {
...
this.tweetElement = document.getElementById('snailbait-tweet');
this.TWEET_PREAMBLE = 'https://twitter.com/intent/tweet?text= +
'I scored ';
this.TWEET_PROLOGUE = ' playing this HTML5 Canvas platformer: ' +
'http://bit.ly/NDV761 &hashtags=html5';
...
};
Example 17.21 lists a revised version of the revealCredits()
method that constructs the link’s hypertext reference, which Snail Bait accesses with the href
property.
SnailBait.prototype = {
...
revealCredits: function () {
this.fadeInElements(this.creditsElement);
this.tweetElement.href = this.TWEET_PREAMBLE + this.score +
this.TWEET_PROLOGUE;
},
...
};
From Snail Bait’s perspective, that’s all the code that’s necessary to let players tweet their scores. Snail Bait’s end of the bargain is merely to provide a properly configured link that points to the Twitter intent. The intent takes care of the rest.
Web applications are typically implemented as a collection of services, ranging from simple services such as saving a document to more sophisticated services such as tweeting a game’s score. Web intents let web applications hand off services to third parties to implement the intent.
In this section Snail Bait handed off the service of tweeting the game’s score to a Twitter web intent, which is orders of magnitude easier than implementing the same functionality on your own.
HTML5 game developers are fortunate because their games run on virtually any computer platform with a browser, including, with a little extra work, mobile devices. HTML5 game developers are unfortunate, however, because their games, which run in an unpredictable environment, can easily be brought to their knees by all sorts of other applications, from backup software running in the background to YouTube videos running in another window.
No matter how fast your HTML5 game runs, there will be times when a player’s computer won’t be able to give your game the time it needs and the game will run too slowly to be playable. When that happens, you must warn the user that the game is running too slowly. Figure 17.7 shows Snail Bait’s running slowly warning.
Snail Bait’s running slowly warning includes the game’s current frame rate (which the game constantly updates while the running slowly warning is displayed) and an option to dismiss the warning for good. The warning also explains why HTML5 games sometimes run slowly and what the user can do about it.
Snail Bait implements the running slowly warning with the following steps:
• Add HTML elements for the running slowly warning
• Specify CSS for the running slowly warning elements
• Access the HTML elements in JavaScript
• Implement a revealRunningSlowlyWarning()
method
Example 17.22 shows the HTML for Snail Bait’s running slowly warning.
<!DOCTYPE html>
<html>
...
<body>
...
<div id='snailbait-arena'>
...
<div id='snailbait-running-slowly'>
<h1>Snail Bait is running slowly</h1>
<hr>
<p id='snailbait-slowly-warning'></p>
<p>
A wide range of applications, from video players to
backup software, can slow this game down. For best
results, hide all other windows on your desktop and
close graphics intensive apps when you play HTML5 games.
</p>
<p>
You should also upgrade your browser to the latest
version to make sure that it has hardware-accelerated
HTML5 Canvas. Any version of Chrome starting with
version 18, for example, has hardware-accelerated
Canvas. Here is a link to the
<a href='http://www.google.com/chrome/'>
latest version of Chrome
</a>.
</p>
<a id='snailbait-slowly-okay' href='#'>
Okay
</a>
<a id='snailbait-slowly-dont-show' href='#'>
Do not show this warning again
</a>
</div> <!-- End of running-slowly -->
...
</div> <!-- End of snailbait-arena -->
...
</body>
</html>
The interesting part of the preceding HTML is the empty DIV with the identifier snailbait-slowly-warning
. Snail Bait fills in the contents of that DIV on the fly because the warning contains the game’s current frame rate.
The CSS for the running slowly warning is listed in Example 17.23.
#snailbait-running-slowly {
position: absolute;
margin-left: 82px;
margin-top: 75px;
width: 600px;
background: rgba(255,255,255,0.85);
padding: 0px 20px 20px 20px;
color: navy;
text-shadow: 1px 1px 1px rgba(255,255,255,0.5);
-webkit-transition: opacity 1s;
-moz-transition: opacity 1s;
-o-transition: opacity 1s;
transition: opacity 1s;
border-radius: 10px 10px 10px;
-webkit-box-shadow: rgba(0,0,0,0.5) 4px 4px 8px;
-moz-box-shadow: rgba(0,0,0,0.5) 4px 4px 8px;
-o-box-shadow: rgba(0,0,0,0.5) 4px 4px 8px;
box-shadow: rgba(0,0,0,0.5) 4px 4px 8px;
opacity: 0;
display: none;
z-index: 1;
}
#snailbait-running-slowly h1 {
padding-top: 0;
text-align: center;
color: rgb(50,50,250);
}
#snailbait-running-slowly p {
color: navy;
font-size: 1.05em;
}
#snailbait-slowly-okay {
margin-top: 20px;
float: left;
margin-left: 50px;
font-size: 1.2em;
}
#snailbait-slowly-okay:hover {
color: blue;
}
#snailbait-slowly-dont-show {
margin-top: 20px;
float: right;
margin-right: 50px;
font-size: 1.2em;
}
#snailbait-slowly-dont-show:hover {
color: blue;
}
Once again, we combine initial invisibility with CSS transitions to fade the running slowly warning in and out as needed by modifying its opacity with Snail Bait’s fadeInElements()
and fadeOutElements()
. Example 17.24 shows how Snail Bait’s constructor accesses the running slowly warning HTML elements.
var SnailBait = function () {
...
// Running slowly warning............................................
this.runningSlowlyElement =
document.getElementById('snailbait-running-slowly');
this.slowlyOkayElement =
document.getElementById('snailbait-slowly-okay');
this.slowlyDontShowElement =
document.getElementById('snailbait-slowly-dont-show');
this.slowlyWarningElement =
document.getElementById('snailbait-slowly-warning');
this.lastSlowWarningTime = 0;
this.showSlowWarning = false;
this.runningSlowlyThreshold = this.DEFAULT_RUNNING_SLOWLY_THRESHOLD;
...
};
Snail Bait obtains references to some of the elements in the running slowly warning and creates three variables: showSlowWarning
, runningSlowlyThreshold
, and lastSlowWarningTime
. Snail Bait uses showSlowWarning
to control whether the game shows the running slowly warning and the runningSlowlyThreshold
defines slowest acceptable frame rate. Snail Bait uses lastSlowWarningTime
to determine when to next check the game’s frame rate.
Snail Bait’s revealRunningSlowlyWarning()
method is listed in Example 17.25.
SnailBait.prototype = {
...
revealRunningSlowlyWarning: function (now, averageSpeed) {
this.slowlyWarningElement.innerHTML =
"Snail Bait is running at <i><b>" +
averageSpeed.toFixed(0) + "</i></b>" +
" frames/second (fps), but it needs more than " +
this.runningSlowlyThreshold +
" fps to work correctly."
this.fadeInElements(this.runningSlowlyElement);
this.lastSlowWarningTime = now;
},
...
};
The revealRunningSlowlyWarning()
method constructs the variable part of the warning with the game’s current frame rate and fades in the running slowly element. The method also updates the lastSlowWarningTime
, which records the last time the warning was revealed.
When the frame rate drops below the game’s running slowly threshold—represented by the runningSlowlyThreshold
variable declared in Example 17.26—Snail Bait invokes the revealRunningSlowlyWarning()
method discussed above. Snail Bait’s constructor also declares an array of 10 speed samples and an index into that array.
var SnailBait = function () {
...
// Running slowly warning............................................
this.FPS_SLOW_CHECK_INTERVAL = 2000; // Check every 2 seconds
this.DEFAULT_RUNNING_SLOWLY_THRESHOLD = 40; // fps
this.MAX_RUNNING_SLOWLY_THRESHOLD = 60; // fps
this.RUNNING_SLOWLY_FADE_DURATION = 2000; // seconds
this.speedSamples = [60,60,60,60,60,60,60,60,60,60];
this.speedSamplesIndex = 0;
this.NUM_SPEED_SAMPLES = this.speedSamples.length;
...
};
To monitor frame rate, we revise Snail Bait’s animate()
method as shown in Example 17.27.
SnailBait.prototype = {
...
animate: function (now) {
// Replace the time passed to this method by the browser
// with the time from Snail Bait's time system
now = snailBait.timeSystem.calculateGameTime();
...
if (snailBait.paused) {
...
}
else { // not paused
snailBait.fps = snailBait.calculateFps(now);
if (snailBait.windowHasFocus &&
snailBait.playing &&
snailBait.showSlowWarning &&
now - snailBait.lastSlowWarningTime >
snailBait.FPS_SLOW_CHECK_INTERVAL) {
snailBait.checkFps(now);
}
...
}
},
...
};
Snail Bait’s animate()
method checks the frame rate when play is underway and two seconds have elapsed since the last check. It checks the frame rate with the checkFps()
method, listed in Example 17.28.
SnailBait.prototype = {
...
checkFps: function (now) {
var averageSpeed;
this.updateSpeedSamples(snailBait.fps);
averageSpeed = this.calculateAverageSpeed();
if (averageSpeed < this.runningSlowlyThreshold) {
this.revealRunningSlowlyWarning(now, averageSpeed);
}
},
...
};
The checkFps()
method updates the game’s speed samples and subsequently calculates an average speed from those samples. If the average speed is less than the running slowly threshold, checkFps()
reveals the running slowly warning.
The helper methods used by checkFps()
are listed in Example 17.29.
SnailBait.prototype = {
...
calculateAverageSpeed: function () {
var i,
total = 0;
for (i=0; i < this.NUM_SPEED_SAMPLES; i++) {
total += this.speedSamples[i];
}
return total/this.NUM_SPEED_SAMPLES;
},
updateSpeedSamples: function (fps) {
this.speedSamples[this.speedSamplesIndex] = fps;
this.advanceSpeedSamplesIndex();
},
advanceSpeedSamplesIndex: function () {
if (this.speedSamplesIndex !== this.NUM_SPEED_SAMPLES-1) {
this.speedSamplesIndex++;
}
else {
this.speedSamplesIndex = 0;
}
},
...
};
When a player clicks the Do not show this warning again button, the browser invokes the element’s click
event handler, which is listed in Example 17.30.
snailBait.slowlyDontShowElement.addEventListener(
'click',
function (e) {
snailBait.fadeOutElements(snailBait.runningSlowlyElement,
snailBait.RUNNING_SLOWLY_FADE_DURATION);
snailBait.showSlowWarning = false;
...
}
);
The preceding event handler fades the running slowly warning from view for a duration of two seconds. It then sets the showSlowWarning
variable to false
so that Snail Bait no longer monitors frame rate. Finally, the event handler puts the game in play.
The onclick
event handler for the running slowly warning’s Okay button is listed in Example 17.31.
SnailBait.prototype = {
...
resetSpeedSamples: function () {
snailBait.speedSamples = [60,60,60,60,60,60,60,60,60,60];
},
...
},
snailBait.slowlyOkayElement.addEventListener(
'click',
function (e) {
snailBait.fadeOutElements(snailBait.runningSlowlyElement,
snailBait.RUNNING_SLOWLY_FADE_DURATION);
snailBait.resetSpeedSamples();
}
);
The Okay button’s event handler fades out the running slowly warning and the game’s speed samples array, discarding the frame rates the game recorded before the warning was revealed.
If a player lands on the gold button at the end of the game, as shown in Figure 17.8, Snail Bait displays the winning animation shown in Figure 17.9.
The winning animation fades the game’s canvas, replaces the score with Winner! and shows the animated GIF the game displays when it loads. The game’s revealWinningAnimation()
method reveals the animation. That method is invoked by a behavior attached to the gold button.
Snail Bait implements the winning animation with the following steps:
• Implement a behavior for the gold button that invokes revealWinningAnimation()
• Detonate the button in the runner’s fall behavior when the runner falls on the button
• Implement a revealWinningAnimation()
method that fades in the winning animation; after displaying the animation for five seconds, end the game
Snail Bait has two buttons. The first is blue and the second is gold, as you can see from Example 17.32.
SnailBait.prototype = {
...
createButtonSprites: function () {
var button;
for (var i = 0; i < this.buttonData.length; ++i) {
if (i !== this.buttonData.length - 1) {
button = new Sprite('button',
new SpriteSheetArtist(this.spritesheet,
this.blueButtonCells),
[ this.paceBehavior,
this.blueButtonDetonateBehavior ]);
}
else {
button = new Sprite('button',
new SpriteSheetArtist(this.spritesheet,
this.goldButtonCells),
[ this.paceBehavior,
this.goldButtonDetonateBehavior ]);
}
...
}
},
...
};
Both buttons pace back and forth on their platforms. Recall from Chapter 7 that the blue button’s detonate behavior blows up two bees earlier in the level to clear a path for the runner. The gold button’s detonate behavior reveals the winning animation.
As required by all behaviors, the gold button’s detonate behavior implements an execute()
method, as you can see in Example 17.33. Snail Bait invokes that execute()
method for every animation frame in which the gold button is visible.
var SnailBait = function () {
...
this.goldButtonDetonateBehavior = {
execute: function(sprite, now, fps, lastAnimationFrameTime) {
if ( ! sprite.detonating) { // trigger
return;
}
snailBait.revealWinningAnimation();
sprite.detonating = false;
}
};
...
};
The execute()
method first checks to see if the sprite—which is always the gold button because the behavior is attached to it—is detonating; if not, there’s nothing to do, and the method returns. If the button is detonating, the execute()
method reveals the winning animation and resets the button’s detonating
property to false
.
The gold button’s detonating
property, which is the trigger for the gold button’s detonate behavior, is false
until the runner lands on the gold button. In that case, the runner’s fall behavior sets the detonating
property to true
, as you can see in Example 17.34.
SnailBait.prototype = {
...
this.fallBehavior = {
...
processCollision: function (sprite, otherSprite) {
if (sprite.jumping && 'platform' === otherSprite.type) {
this.processPlatformCollisionDuringJump(sprite, otherSprite);
}
else if ('button' === otherSprite.type) {
if (sprite.jumping && sprite.descendTimer.isRunning() ||
sprite.falling) {
otherSprite.detonating = true;
}
}
},
...
};
When the runner’s fall behavior sets the gold button’s detonating property to true
, the gold button’s detonate behavior invokes Snail Bait’s revealWinningAnimation()
method, as you saw in Example 17.33. That method is listed in Example 17.35.
SnailBait.prototype = {
...
revealWinningAnimation: function () {
var WINNING_ANIMATION_FADE_TIME = 5000,
SEMI_TRANSPARENT = 0.25;
this.bgVelocity = 0;
this.playing = false;
this.loadingTitleElement.style.display = 'none';
this.fadeInElements(this.runnerAnimatedGIFElement,
this.loadingElement);
this.scoreElement.innerHTML = 'Winner!';
this.canvas.style.opacity = SEMI_TRANSPARENT;
setTimeout( function () {
snailBait.runnerAnimatedGIFElement.style.display = 'none';
snailBait.fadeInElements(snailBait.canvas);
snailBait.gameOver();
}, WINNING_ANIMATION_FADE_TIME);
},
...
};
The revealWinningAnimation()
method fades in the loading screen elements, without the Loading... text that the game displays when it loads resources. As the animated GIF fades in, the canvas fades out to an opacity of 0.25.
After five seconds, revealWinningAnimation()
hides the animated GIF, fades the canvas in to fully opaque, and ends the game.
In this chapter you saw how to implement most of Snail Bait’s user interface. Not all game programming involves programming gameplay, and as a result, game developers spend a significant amount of time implementing features that don’t affect gameplay.
At this point we’ve implemented all Snail Bait’s gameplay and most of its user interface. The remaining chapters show you how to implement a developer backdoor, how to store high scores and in-game metrics on the server, and how to deploy your games.
1. Activate Snail Bait’s developer backdoor by pressing CTRL-d. Experiment with the running slowly threshold to see how fast the game runs. Hint: If you set the threshold at 60 fps, the warning will most likely show constantly. If you set it to 10 fps, you should hardly ever see the warning. Somewhere in between lies the game’s frame rate.
2. Move the lives indicator from the left side of the game to the right.
3. Change the opacity of the score
element during gameplay from fully opaque to an opacity of 0.5. When the game updates the score
element, temporarily change the element’s opacity to fully opaque for 0.5 seconds to make the score
element flash whenever the player captures an asset.
52.15.47.218