Managing the game scene
In this task, we create four scenes and display different scenes based on the game flow.
Prepare for lift off
The following figure is our planning of the four scenes and the flow, showing on how they should link together:
The following figure shows three scenes of what we will create in this task:
Engage thrusters
We code the management part of the scene via the following steps:
- The scenes are DOM elements, so we will have the following HTML elements defined inside the tag with the
game
ID:<div id="game-scene" class="scene out"> <a href="#" id="gameover-btn">Game Over</a> <a href="#" id="finish-btn">Finish</a> </div> <div id="start-scene" class="scene"> <a href="#" id="start-btn" class="button">Start Game</a> </div> <div id="summary-scene" class="scene out"> <a href="#" id="next-level-button" class="button">Next</a> </div> <div id="gameover-scene" class="scene out"> <a href="#" id="back-to-menu-button" class="button">Back to menu</a> </div>
- Now, we need to import our newly created
scenes.js
file into the HTML file, before thegame.js
file:<script src='js/scenes.js'></script> <script src='js/game.js'></script> </body>
- In the
scene.js
file, we add the following code to define the scene's object and its instances:(function(){ var game = this.colorQuestGame = this.colorQuestGame ||{}; // put common scene logic into 'scene' object. var scene = { node: document.querySelector('.scene'), show: function() { this.node.classList.remove('out'); this.node.classList.add('in'); }, hide: function() { this.node.classList.remove('in'); this.node.classList.add('out'); } }; // scene instances code to go here. )();
- Then, we create an instance of the game scene. Put the following code right after the
scene
object code. The following code creates two temporary links to finish the level and complete the game:var gameScene = game.gameScene = Object.create(scene); gameScene.node = document.getElementById('game-scene'); gameScene.handleInput = function() { document.getElementById('finish-btn').onclick = function(){ game.flow.finishLevel(); }; document.getElementById('gameover-btn').onclick = function(){ game.flow.gameOver(); }; };
- The start scene instance comes after the game scene code. The following code handles the clicking of the start button that links to the game scene:
var startScene = game.startScene = Object.create(scene); startScene.node = document.getElementById('start-scene'); startScene.handleInput = function() { document.getElementById('start-btn').onclick = function(){ game.flow.nextLevel(); }; };
- Then, we have the summary scene. The summary scene has a button that links to the game scene again to show the next level:
var summaryScene = game.summaryScene = Object.create(scene); summaryScene.node = document.getElementById('summary-scene'); summaryScene.handleInput = function() { document.getElementById('next-level-button').onclick = function() { game.flow.nextLevel(); }; };
- At last, we add the game over scene code to the
scenes.js
file. When the game is over, we bring the player back to the menu scene after the back button is clicked:var gameoverScene = game.gameoverScene = Object.create(scene); gameoverScene.node = document.getElementById('gameover-scene'); gameoverScene.handleInput = function() { var scene = this; document.getElementById('back-to-menu-button').onclick = function() { game.flow.startOver(); }; };
- Now, we will define a game flow in the
game.js
file that will help us control how to show and hide the scenes:// Main Game Logic game.flow = { startOver: function() { game.startScene.hide(); game.summaryScene.hide(); game.gameoverScene.hide(); game.gameScene.hide(); game.startScene.show(); }, gameWin: function() { game.gameScene.hide(); game.summaryScene.show(); }, gameOver: function() { game.startScene.show(); game.gameScene.hide(); game.gameoverScene.show(); }, nextLevel: function() { game.startScene.hide(); game.summaryScene.hide(); game.gameScene.show(); }, finishLevel: function() { game.gameScene.hide(); game.summaryScene.show(); }, }
- The
init
function is the entry point of the game. Inside this function, we will register the click input listeners:var init = function() { game.startScene.handleInput(); game.summaryScene.handleInput(); game.gameoverScene.handleInput(); game.gameScene.handleInput(); }
- At last, we need some styling for the scenes to make them work. Put the following CSS rules at the end of the
game.css
file:#game { width: 480px; height: 600px; margin: 0 auto; border: 1px solid #333; text-align: center; position: relative; overflow: hidden; } .scene { background: white; width: 100%; height: 100%; position: absolute; transition: all .4s ease-out; } .scene.out {top: -150%;} .scene.in {top: 0;} .button { width: 145px; height: 39px; display: block; margin: auto; text-indent: 120%; white-space: nowrap; overflow: hidden; background-repeat: no-repeat; } .button:hover { background-position: 0 -39px; } .button:active { background-position: 0 0; } #start-scene {background: url(images/menu_bg.png);} #start-btn { background-image: url(images/start_btn.png); margin-top: 270px; } #game-scene {background: url(images/game_bg.png);} #game-scene.out { opacity: 0; top: 0; transition-delay: .5s; } #summary-scene {background: url(images/summary_bg.png);} next-level-button { background-image: url(images/next_btn.png); margin-top: 370px; } #summary-scene.in { transition-delay: .5s; } #gameover-scene { background: url(images/gameover_bg.png); } #back-to-menu-button { background-image: url(images/restart_btn.png); margin-top: 270px; }
Objective complete – mini debriefing
We have created scenes in this task. Now let's take a closer look at each block of code to see how they work together.
Creating buttons
Each button is of 145 pixels by 39 pixels in size and has two states: a normal and a hover state. Both states are combined in one image and thus the final image is of 78 pixels in height. The bottom part contains the hover state. We switch these states by setting the background's y position to 0
pixel for the normal state and -39
pixel for the hover state, as shown in the following screenshot:
Placing the scene logic and the namespace
We encapsulated the scene's management code in a file named scene.js
. Similar to the game.js
file, we start every logic file with a self-invoked anonymous function. Inside the function, we use the same namespace: colorQuestGame
.
The transition between scenes
We use CSS to control the visibility of the scenes. The .in
and .out
CSS properties that apply to all the scenes have different top values. One is -150%
to ensure it is not visible, and the other is top: 0
; therefore, it is visible in the game element.
Then, we toggle each scene between the .in
and .out
class to control the visibility. In addition, we add transition to these CSS rules so changing the value from 0 to -150 percent and vice-versa becomes a smooth animation.
The scene object inheritance
There are four scenes in this game: the pregame start scene, game scene, game over scene, and level-completed summary scene. Each scene shares certain common logic; this includes showing and hiding themselves. In JavaScript, we can use object inheritance to share common logic. The scene is an object with default show-and-hide behaviors. It also defines a dummy sceneElement
property that points to the DOM element of that scene.
A game scene is another object. However, we do not create it as a normal object. We use the Object.create
method with this scene as the argument. The Object.create
method will chain the argument as the new object's prototype. This is known as a prototype chain.
What if we want to show a different effect for hiding a game scene? It depends on whether your effects are done in CSS or JavaScript. If it is a CSS-only effect, you can just add rules to the #game-scene.out
scene. In the management part of the scene, .in
is to display the scene and .out
is for rules that hide the scene.
For the CSS approach, assume that we want a fade-out effect; we can do so using the following CSS rule:
#game-scene.out { opacity: 0; top: 0; }
Prototype chaining
JavaScript is an object-oriented programming language without the requirement of a class definition. It uses a prototype chain for object inheritance. Each object comes with a special prototype
property. The prototype defines what this object is based on; you can think of it as inheritance in traditional object-oriented languages.
Let's take a look at how JavaScript accesses object properties. When we call Scene.show()
, it takes a look at the property list and finds a property named show
, which is of the type function
.
Imagine now that the show
method is not there. The JavaScript runtime looks it up in the prototype. If it is found inside the prototype, it returns the method. Otherwise, it keeps looking up prototype's prototype until it reaches a generic object.
This is the meaning of prototype chaining. We build an object based on another object by putting the other object into the prototype. The following screenshot shows the startScene
property and the properties in its prototype (scene
):
In order to attach the object to prototype, we use Object.create
. The original object's prototype is attached to the new object. It allows us to directly inherit an object's instance into a new object without going through the traditional abstract class definition.
Another approach to put a different object into the prototype is using the Function
and new
approaches. When we define a function, we can use new
to create an object from the function. For example, observe the following function:
function Scene() {} Scene.prototype.show = function() {} Scene.prototype.hide = function() {}
Now, when we create a scene object instance by using new Scene(),
the instance has the method show
and hide
. If we want to have a GameScene
definition based on the Scene
, we can do that:
function GameScene() {} GameScene.prototype = new Scene();
In this function, we have added an instance of Scene
into GameScene
. Therefore, the GameScene
instance now has all the functionalities of Scene
.
Note
More explanation on the difference between the new instance approach and the Object.create
approach can be found in the following link to a post from the Mozilla Developer Network:
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/create
Classified intel
Besides changing the CSS .in
and .out
classes of the scenes, we can also add extra animation or logic when the scene shows and hides. Instead of the plain scene movement, we can further enhance the transition by defining the game objects in
and out
of the CSS rule. For example, we can make the buttons fade out during the transition, or we can drop the quest element to the bottom to create an illusion of it being unlocked; this can be done using the following code:
// an example of custom hide function gameScene.hide = function() { // invoke the hide function inside the prototype chain. // (aka. super.hide()) Object.getPrototypeOf(this).hide.call(this); /* extra */ // add the class for the out effect var questView = document.getElementById('quest'); questView.classList.add('out'); /* end extra */ }
Since, we have overridden the hide
method from the scene object, we need to call the prototype's hide using the scope of gameScene
. Then, we add our extra logic to add the out
class to the quest DOM element.
We define the dropping effect with CSS transform and transition:
#quest.out { transition: all .8s ease-out; transform: translateY(800px) rotate(50deg); }
The out
object of the game scene is a delayed fading out transition:
#game-scene.out, #summary-scene.in { transition-delay: .5s; }
In addition, we used the transition delay to make sure that the drop animation is displayed before the scene goes out and the next scene goes in.
Tip
Some new properties of CSS are not stable yet. When a vendor adds support to these styles, they add a vendor-prefix to the property. The vendor prefix indicates that the user should use that property with caution.
In this project, we will omit all the vendor-prefixes for clarity when showing the code in the book. In order to make sure that the code works in a browser, we may need to add the following vendor prefixes: -
webkit-
(for Chrome and Safari), -moz
-
(for Mozilla Firefox), -
o-
(for Opera), and -ms
-
(for Internet Explorer).
If you find adding prefixes troublesome, you may use some tools for help. The tool prefix-free (http://leaverou.github.io/prefixfree/) is one that can help. Compile tools will add these prefixes for you, such as CSS preprocess compilers.