HTML5 Game Development HOTSHOT

5 (1 reviews total)
By Makzan
  • Instant online access to over 7,500+ books and videos
  • Constantly updated with 100+ new titles each month
  • Breadth and depth in over 1,000+ technologies
  1. Building a CSS Quest Game

About this book

This book will show you how to create stunning cross-browser games without the need for Flash or other plugins. Learn about Box2D, DOM elements, the EaselJS framework, and more, all providing a foundation of knowledge to expand your game-creating skills. With in-depth explanations and step-by-step instructions, you will finish this book feeling confident about building great games with HTML. Whether you are familiar with the basics of object-oriented programming concepts, are new to HTML game development, or are familiar with just web design, this project-based book will get you up and running in no time. It will teach and inspire you to create great interactive content on the Web.

Publication date:
July 2014
Publisher
Packt
Pages
366
ISBN
9781849695466

 

Chapter 1. Building a CSS Quest Game

In this project, we are going to build a card quest game using HTML elements and CSS styling. We will also learn how to separate JavaScript logic into modules for clarity. We will build the game from scratch with a basic HTML layout and then represent the composition of the pattern in logic and CSS.

 

Mission briefing


We are going to create a matching game. This game presents a combination of patterns to the player, and some of these patterns overlap each other. Players analyze the patterns and then select them in the correct sequence so that the composition matches the provided one. You may visit the URL http://makzan.net/html5-games/color-quest/ to play the example game in order to have a better understanding of what we will build throughout this project.

The following screenshot shows the final result of this project. The upper part is a timer, the part to the left shows the quest, the right part is the composition of the player, and the lower part is the deck of patterns.

Why is it awesome?

A simple matching game like this is a perfect way to get you warmed up with the HTML5 games' development rhythm. What we will learn in this project is the foundation to build more complicated games later in this book.

This game uses what we are familiar with: HTML element interaction and styling in CSS. What's new, is JavaScript logic. This project will show us how to convert the game rule into code. We will also learn how to separate logic into different parts with different responsibilities.

Your Hotshot objectives

We'll perform the following tasks in this project:

  • Creating the HTML structure

  • Managing the game scenes

  • Representing the quest pattern composition

  • Placing the patterns on the deck

  • Selecting the pattern

  • Comparing players and compositions of the quest

  • Showing different quests

  • Counting down the game

Mission checklist

In this game, we are going to stick to plain JavaScript without using a library. This will help us get started with a basic foundation in JavaScript.

Note that writing JavaScript for games is a little bit different from writing it for a web page. Game logic requires well-structured code to ensure the logic is easy to read and maintained. The following essays provide an in-depth discussion on writing JavaScript in the right way:

 

Creating the HTML structure


In this task, we are going to kick-start the project by creating the file structure, ensuring that the essential files are ready.

Prepare for lift off

We need several things to get started. First, we will create an empty project directory. Then, we will create an index.html file, a folder where we will put the CSS styling files, and a folder to put the JavaScript logic files in.

During the missions, we need several graphics files, including the background and buttons; you can find the graphics from the sample code. Put all the images into a directory named images. The created file structure is shown in the following screenshot:

Engage thrusters

Use the following steps to create the basic game structure:

  1. We will enter the following HTML code in the index.html file. It is a basic HTML structure that includes the CSS file in the head and the script tag at the bottom:

    <!DOCTYPE html>
    <html lang='en'>
    <head>
      <meta charset='utf-8'>
      <title>Color Quest</title>
      <link rel="stylesheet" href="game.css">
    </head>
    <body>
      <!-- game content here -->
      <script src='js/game.js'></script>
    </body>
    </html>
  2. Right after the opening of the <body> tag and before our <script> tag, we add the following HTML code for the game page. The content is divided into four parts: header, game section, the how-to-play section, and footer. The game section is where all the game logic happens. The #element-template is the template of game elements that will be used for cloning later:

    <header>
      <div class="row">
        <h1>Color Quest</h1>
      </div>
    </header>
    <section id="game">
    </section>
    <section class='how-to-play'>
      <div class="row">
        <h2>How to Play?</h2>
        <p>Composite your card to match the given pattern.</p>
      </div>
    </section>
    <footer>
      <div class="row">
        <p>This game is an example for the HTML5 Games Hotshot book. Free for personal and commerical use.</p>
      </div>
    </footer>
    <div id='element-template'>
    </div>
  3. Add the following JavaScript to the game.js file. It acts as the entry point of the game logic:

    (function(){
      // Entry Point
      var init = function() {
      };
    
      init(); // start the game
    })(); // self-executing function.

    Tip

    Downloading the example code

    You can download the example code files for all Packt books you have purchased from your account at http://www.packtpub.com. If you purchased this book elsewhere, you can visit http://www.packtpub.com/support and register to have the files e-mailed directly to you.

Objective complete – mini debriefing

We have created a very basic structure of the game. The following sections explain what we have done.

HTML structure

The header, section, and footer arrangement follows a simple HTML5 content structure. We will put the game elements inside this structure, attached to the game. The following screenshot shows the HTML structure:

Modularizing the logic

We separate the JavaScript logic into modules, and each separated file encapsulates one module. This allows us to tackle each part individually. The first JavaScript file we have is the game.js file. It is in charge of controlling the game flow. There are other different parts in this game, and for these parts, we are going to create different files with the purpose of using them for future tasks.

Variable scope in JavaScript

In JavaScript, we want to have as few global variables as possible. Variables are global when they are attached to the highest scope of the runtime environment. In a web browser, this refers to a variable that is attached to a window scope. A variable that is created with the var keyword lives inside the scope of the function that encloses it.

This is why we put all the logic inside a self-executing anonymous function. This ensures that by default, we will not pollute the global scope with our game logic variables. The following code block shows how to create each module with one global game variable and one local game variable inside a self-executing-anonymous function:

(function(){
  var game  = this.colorQuestGame = this.colorQuestGame || {};
})();

We will intentionally create one global variable named colorQuestGame to act as the namespace. In the later sections, we will put different logic modules into different files under the same global object.

The this.colorQuestGame || {}; declaration means that, by default, it will use the existing colorQuestGame variable if it was already declared previously. Otherwise, it will be a new empty object. We will put this line of declaration into every file.

This scoping feature is also useful when we need to encapsulate logic into private functions. For example, the following code shows how we can create private functions that are only accessible inside a specific scope to help extract the code:

Composition.createFromSequence = function(sequence) {
  // helper functions
  var privateHelperA = function() {}
  var privateHelperB = function() {}
  // end helper functions

  // use your private helper functions here.
}

Inside any method, we can declare local scoped functions to help extract the logic and to make the code more readable.

Classified intel

For performance, we usually place scripts at the end of Document Object Model (DOM) and just before the closing of the </body> tag. This is because script loading may block the loading of DOM and cause the webpage to load slower. For more details, please have a look at the Yahoo performance rule, Put Scripts at the Bottom, at http://developer.yahoo.com/performance/rules.html#js_bottom.

 

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:

  1. 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>
  2. Now, we need to import our newly created scenes.js file into the HTML file, before the game.js file:

      <script src='js/scenes.js'></script>
      <script src='js/game.js'></script>
    </body>
  3. 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.
    )();
  4. 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();
      };
    };
  5. 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();
      };
    };
  6. 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();
      };
    };
  7. 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();
      };
    };
  8. 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();
      },
    }
  9. 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();
    }
  10. 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.

 

Representing the quest composition


In this task, we declare the quest level and then display the level in the quest composition view.

Prepare for lift off

We will need three more JavaScript files in this task, so let's create them. These files are as follows:

  • patch.js: This file is used for adding methods to existing built-in objects to enhance convenience

  • composition.js: We use this file for logic that represents the composition

  • quest.js: These files represent the data of a quest, including the level and quest data manipulating methods

At the end of this task, we should be able to create a composition of patterns according to our level. For example, the following quest composition is composed of four patterns: a circle, the left and right trapezoid, and lines, as shown in the following screenshot:

Engage thrusters

We put patterns into composition using the following steps:

  1. To start with, in HTML, we want to remove the dummy gameover link and the finish link from the game scene. As we are now putting real content in the game scene, these two links may affect the placement of our game elements:

    <div id="game-scene" class="scene out">
      <div id="stage">Stage 1</div>
      <div id='quest' class="quest">
        <div id="quest-composition" class="composition"></div>
      </div>
    </div>
    
    <div id='element-template'>
      <!-- for composition view -->
      <div class="pattern" data-pattern='1'></div>
    </div>
  2. We will import the three files at the end of HTML file. They are patch.js, composition.js, and quest.js:

    <script src='js/patch.js'></script>
    <script src='js/composition.js'></script>
    <script src='js/quest.js'></script>
    <script src='js/scenes.js'></script>
    <script src='js/game.js'></script>
  3. We want to make it easy to remove all the DOM elements from the quest view. This is why we have a patch file. Put the following code in the patch.js file to add the removeAllChildren method to all the DOM nodes:

    // add removeAllChildren to Node object.
    Node.prototype.removeAllChildren = function() {
      while(this.firstChild) {
        this.removeChild(this.firstChild);
      }
    };
  4. Then, we add a basic skeleton to the composition.js file:

    (function(){  
      var game  = this.colorQuestGame = this.colorQuestGame || {};
    
      // composition model definition
      // composition is a deck of pattern put together
      var Composition = game.Composition = (function(){
        function Composition(){
          this.data = [];
        }    
    
        return Composition;
      })();
    })();
  5. In the quest.js file, we represent the level of data in arrays. The number is the pattern. We will discuss how we come up with this special array structure later:

    (function(){
      var game  = this.colorQuestGame = this.colorQuestGame || {};
    
      // level data
      var questLevels = [
        [ [5, 6], [3] ],
        [ [6], [1, 2]],
        [ [5, 6] ],
        [ [3], [1, 2], [4] ],
        [ [1, 2], [3], [4], [5, 6]],
      ];
    
      // quest model definition
      // quest is a kind of composition, the difference is that quest is specifically used as the question for player to give the answer.
      // so it comes with comparing logic.
      var Quest = game.Quest = (function(){
        function Quest(level){
          var questData = questLevels[level];
          this.data = questData;
        }
        Quest.prototype = new game.Composition(); 
        // extends the Quest prototype from Composition.
        return Quest;
      })();
    })();
  6. Since we have removed the dummy gameover and have finished with the link of the game scene, now, from the scenes.js file, we will also remove the onclick event for these two links inside the handleInput method.

  7. We add a new method to the gameScene instance that displays the data in the game scene. This method creates the patterns in the quest area of the game according to the given data:

    gameScene.visualize = function(quest) {
      var questData = quest.data;
      var patternsToShow = [];
      for (var i in questData) {
        for (var j in questData[i]) {
          patternsToShow.push(questData[i][j]);
        }
      }
      // Quest
      // visualize the quest composition view:
      var questCompositionNode = document.getElementById('quest-composition');
    
      // empty the DOM view
    questCompositionNode.removeAllChildren();
    
      // visualize the pattern view:
      for (var i in patternsToShow) {
        var patternNode = document.querySelector('#element-template .pattern').cloneNode(/*clone children=*/true);
        patternNode.setAttribute('data-pattern',   patternsToShow[i]);
        questCompositionNode.appendChild(patternNode);
      }
    };
  8. We need to modify the game flow in the game.js file to show the quest:

    game.flow = {
      currentLevel: 3,
      startLevel: function() {
        game.quest = new game.Quest(this.currentLevel);
        game.gameScene.visualize(game.quest);      
      },
      ...
    }
    
    var init = function() {
      ...
      game.flow.startLevel();
    }
  9. Finally, we will create the view of the patterns and the quest in CSS:

    #stage {  
      position: absolute;
      width: 100%;
      height: 30px;
      line-height: 30px;
    }
    
    /* Template */
    #element-template {display: none;}
    
    /* quest */
    #quest {
      width: 200px;
      height: 200px;  
      position: absolute;
      left: 30px;
      top: 70px;
    }
    
    /* individual pattern */
    .pattern {
      width: 200px;
      height: 200px;
      background-size: contain;
    }
    .pattern[data-pattern='1'] {
      background-image: url(images/pattern1.png);
    }
    /* pattern 2-5 puts here */
    .pattern[data-pattern='6'] {
      background-image: url(images/pattern6.png);
    }
    .composition {
      position: relative;
      height: 200px;
      background: white;
    }
    .composition > .pattern {
      position: absolute;
    }

Objective complete – mini debriefing

We created the visual and data composition of a quest. Let's go in detail on what we have done in this task.

Separating the data and view

While designing games, we usually want to separate the logic of manipulating data and the logic of displaying and drawing elements. In our logic, the composition and quest are data. The scenes are for displaying. That's why we use the gameScene.visualize method to display data into the DOM element once we declare the quest composition data.

We need to dynamically create elements to represent the pattern in the quest DOM node. Sometimes we create HTML directly in JavaScript and append it to the node. A better approach is to have the HTML placed inside, well, HTML. That's why we have the template element for JavaScript to clone it and put it back into the quest node.

Note

Using the data-* attribute

It is often useful to use the data-* attribute to embed extra information when we use DOM elements to represent game objects. Take the card as an instance. We can define data-pattern='3' to indicate that element is a visual of pattern number 3. We can define whatever we like as long as it begins with data-. Then, we can use the getAttribute method to access it and use the setAttribute method to update it. Alternatively, we can use the dataset method to access the data-* attribute.

Visualizing the quest patterns

A pattern is a stack of background-transparent cards. We can represent each card as a DIV and overlay them together in one container. In the composition node, we overlap the pattern by setting the position to absolute, top-left position to 0.

Whenever we use absolute elements, we want to make sure that we have control of the reference point of the top and left properties; this means controlling where the top 0 and left 0 positions are.

Elements that are positioned at the absolute point reference the top-left point in the following way:

  • They find the first parent with a position and set it as absolute or relative

  • They use the body's top-left point if no parents are found with the position's setting

Therefore, what we need to do is set a position that is relative to the container, namely, .composition.

The position styling for the quest and pattern has been defined in the CSS. What we need to do is append the newly created HTML node to the quest node from the gameScene.visualize method. The pattern HTML nodes are created from the template and with the class defined that match the CSS rules.

Quest level

In this game, we require the player to select the pattern in the correct sequence to match the quest. However, some patterns are not overlapped with other patterns. In this case, we will put the two non-overlapped pairs together so that the order of choosing among these patterns will not be treated in the wrong order.

We would like to come up with an approach to compare the player's composition with the quest's composition.

The quest is composited by a sequence of patterns. A straightforward approach is to store the pattern sequence in an array. Then, all we need to do is compare whether the player's sequence is exactly the same as the given one.

Sounds good, but it fails in one case. In our patterns, there are some patterns that don't overlap with the others. Take the following pattern shown in the screenshot as an example:

The trapezoids to the left and right fit together without overlapping. We require the player to match the pattern visually so the sequence of these two selections does not change the effect, as shown in the following screenshot:

However, in the following pattern, the circle does overlap with the triangle:

Therefore, a simple sequence array does not work. Let's improve how we store the patterns in an array. How about using a 2-dimensional array?

The first dimension is the z index of the patterns, which is the sequence that players must match.

In order to represent the same level of relationship between the two trapezoids, we put them into one slot inside our array; thus, the array inside the code becomes the following:

[ [Left Trapezoid, Right Trapezoid], [Triangle] ]

To make the array item type consistent, we can force a single item to be wrapped inside the group too. Let's call each group of non-overlapped pattern layer.

Now, the final data array will be as follows:

[ [Left Trapezoid, Right Trapezoid], [Triangle] ]

Here, each array inside the quest level represents one layer with patterns that are interchangeable. Any overlapping pattern will be another new layer. Moreover, we put all the levels into an array to represent all the levels.

Composition and quest modules

The composition module contains only a data instance variable in the current task. The data has the same format as the multilayered patterns. Quest modules inherit the composition with logic that is related to the quest levels.

In future tasks, we are going to add more logic to the composition and quest modules to help in manipulating the format of a multilayer pattern.

 

Placing the patterns on the deck


In this task, we are going to list the pattern in the deck. Later, we will let the player select patterns from this deck.

Prepare for lift off

We are going to need a new module to handle the display of the composition. Let's create a new empty JavaScript file named composition-view.js.

We need to import the file into the index.html file, as follows:

<script src='js/composition-view.js'></script>

Engage thrusters

Let's work on the pattern with the following steps:

  1. In the game scene of the index.html file, we add two DOM elements, namely, #your-composition and #deck:

    <div id="game-scene" class="scene out">
      ...
      <div id="your-composition"></div>
      <div id="deck" class="deck"></div>
    </div>
  2. In the template element, we add the template for the pattern's slot:

    <div id="element-template">
      <!-- for deck view -->
      <div class="pattern-slot">
        <div class="pattern" data-pattern="1"></div>
      </div>
      ...
    </div>
  3. The following is our composition-view.js file:

    (function(){
      var game  = this.colorQuestGame = this.colorQuestGame || {};
    
      // composition module
      game.compositionView = {
        node: document.getElementById('your-composition'),    
      };
    })();
  4. Before the end of the gameScene.visualize method, we add the visualization logic for the player's composition:

    // randomize the patterns array
    patternsToShow.sort(function(a, b){
      return Math.random() - 0.5;
    });
    
    // empty the current deck view
    var deckNode = document.getElementById('deck');
    deckNode.removeAllChildren();
    
    // add the pattern to the deck view
    for (var i in patternsToShow) {
      var patternSlotNode = document.querySelector('#element-template .pattern-slot').cloneNode(/*clone children=*/true);  
      patternSlotNode.querySelector('.pattern').setAttribute('data-pattern', patternsToShow[i]);
      deckNode.appendChild(patternSlotNode);
    }
  5. From the game.js file, we remove all the selected patterns before starting a new level:

    nextLevel: function() {
      ...
      game.compositionView.node.removeAllChildren();
      this.startLevel();
    },
  6. We need the following CSS style for the composition and patterns:

    /* player's composition and pattern */
    #your-composition {
      position: absolute;
      width: 100px;
      height: 100px;
      right: 65px;
      top: 120px;
      border: 3px solid #999;  
    }
    #your-composition > .pattern {
      width: 100px;
      height: 100px;
      position: absolute;
    }
    
    /* deck and pattern */
      .deck { position: absolute;
      top: 360px;
      left: 20px;
    }
    .pattern-slot {
      width: 100px;
      height: 100px;
      outline: 4px solid #BC7702;
      float: left;
      border-radius: 3px;
      margin: 10px 0 0 10px;
    }
    .deck .pattern {
      width: 100%;
      height: 100%;
    }

We should have the following screenshot once this task is completed. The deck shows the patterns that a player needs to compose the quest:

Objective complete – mini debriefing

JavaScript comes with a sort method for the array. Normally, we compare the given two array elements and return +1 or -1 to control elements' swapping.

We can randomize an array by randomly returning either +1 or -1:

patternsToShow.sort(function(a, b){
  return Math.random() - 0.5;
});

After we randomize the patterns, we clone each pattern from the template and append them to the deck element.

 

Selecting the pattern


In this task, we allow players to select the pattern from their decks and display the sequence of the selection in the composition view.

Engage thrusters

Perform the following steps to add user interaction to our game:

  1. We allow players to undo their selection, so we need to add an undo button to the index.html file:

    <a href="#" id="undo-button" class="button">Undo</a>
  2. When starting a level in the game.js file, we store the player's selection sequence and register the clicking event by adding the following highlighted code:

    startLevel: function() {
      game.quest = new game.Quest(this.currentLevel);
      game.compositionSeq = [];
      game.composition = new game.Composition();
      game.gameScene.visualize(game.quest);      
      game.gameScene.handleInput();
    },
  3. In the patch.js file, we need to add forEach to the NodeList and HTMLCollection objects using the following code:

    NodeList.prototype.forEach = Array.prototype.forEach;
    HTMLCollection.prototype.forEach = Array.prototype.forEach;
  4. In the composition-view.js file, we need the following methods to display the pattern selection in the composition's DOM element:

    game.compositionView = {
      node: document.getElementById('your-composition'),
      pushPattern: function(patternId) {
        var newChild = document.createElement('div');
        newChild.classList.add('pattern');
        newChild.setAttribute('data-pattern', patternId);
        this.node.appendChild(newChild);
      },
      pullPattern: function() {
        var lastChild = this.node.querySelector('.pattern:last-child');
        if (lastChild) {
          // find the pattern in the deck and make it visible
          var deckNode = document.getElementById('deck');
          var resumePattern = deckNode.querySelector('[data-pattern="' + lastChild.getAttribute('data-pattern') + '"]');
          resumePattern.style.display = 'block';
    
          // remove the current pattern
          this.node.removeChild(lastChild);
        }
      },
      selectPattern: function(pattern) {
        this.pushPattern(pattern);
        game.compositionSeq.push(pattern);
      },
      undo: function() {
        this.pullPattern();
        game.compositionSeq.pop();
      },
    };
  5. Then, we need the mouse event to invoke our selection logic. In the scenes.js file, we add the following clicking event to the gameScene:

    gameScene.handleInput = function() {         
      document.querySelectorAll("#deck .pattern").forEach(function(elm){
        elm.onclick=  function(){
          var pattern = elm.getAttribute('data-pattern');
          elm.style.display = 'none';
          game.compositionView.selectPattern(pattern);
        };
      });
    
      var undoBtn = document.getElementById('undo-button');
      undoBtn.onclick = function(e){
        game.compositionView.undo();
        e.preventDefault();
      };
    };
  6. Let's move to styling. We have a new undo button, so we need the following CSS rules to place it in the right position with the image:

    #undo-button {
      position: absolute;
      right: 70px;
      top: 240px;
      z-index: 999;
      background: url(images/undo_btn.png) no-repeat;
      width: 90px;
      height: 26px;
    }
    #undo-button:hover {background-position: 0 -26px;}
  7. Also, we add mouse-related styling to the pattern's slot:

    .pattern-slot:hover{outline-color: #D68700;}
    .pattern-slot:active {outline-color: #BC7702;}

Objective complete – mini debriefing

The selection is done by the click event on the pattern. Basically, we get the pattern ID from the data- attribute. Once the pattern ID is known, it triggers the following method:

game.compositionView.selectPattern(pattern);

Then, the composition pushes the selection into an array.

Undo the player composition

We trigger the undo logic by listening to the undo button's click event. Then, the undo logic removes the last pattern from the array. At the same time, we find the last pattern element in the composition view and move this element to the pattern deck.

 

Comparing the player and compositions of the quest


In this task, we create logic to make multilayered pattern compositions from player's selection and then compare it with the quest pattern's composition.

Prepare for lift off

We need a way to represent the pattern overlapping and non-overlapping relationships.

Representing a pattern overlapping relationship

We will use the data structure of this section to remember the overlapping relationship. Most patterns will overlap others, so we would need to think the other way round. So, we store those patterns that do not overlap together.

The pattern is a two-dimensional (2D) array. The first dimension contains every pattern. Each pattern is a list of the other patterns that do not overlay with it.

For example, the following A pattern does not overlap with the C and D shapes. We represent it with the ensuing equation:

array['A'] = [ 'C', 'D'];

For a pattern that always overlaps with the others, an empty array will be assigned.

Engage thrusters

In the following steps, we code the logic that allows us to compare two given quest compositions:

  1. In the composition.js file, we have the following class variable to represent the relationship of the overlapping patterns. It indicates the pattern that overlaps with other patterns. The index of the nonOverlappedPattern array is the pattern ID, and the corresponding array value is the list of patterns that do not overlap with that pattern:

    // static variable. available as only one copy among all composition instances.
    Composition.nonOverlappedPattern = [
      [], // pattern 0
      [2], // pattern 1, doesn't overlap with pattern 2.
      [1], // pattern 2, doesn't overlap with pattern 1.
      [], // pattern 3
      [], // pattern 4
      [6], // pattern 5, doesn't overlap with pattern 6.
      [5], // pattern 6, doesn't overlap with pattern 5.
     ];
  2. We create the following new method in the Composition method that can turn a composition back to a one-dimension array:

    Composition.prototype.toSequence = function() {
      var seq = [];
      for (var i=0; i < this.data.length; i++) {
        for (var j=0; j <this.data[i].length; j++ ) {
          seq.push(this.data[i][j]);
        }
      }
      return seq;
    }
  3. Then, we create the createFromSequence method with the following code that turns a sequence back to a composition:

    Composition.createFromSequence = function(sequence) {
      // function to determine if 2 given patterns overlap.
      var allowPatternsInSameLevel = 
        function(patternA, patternB) {
          // iterate the array to see if current pattern overlaps the
          var nonOverlappedPattern = Composition.nonOverlappedPattern[patternA]
          var len = nonOverlappedPattern.length;
          for (var i=0; i<len; i++) {
            if (nonOverlappedPattern[i] === parseInt(patternB)) {
              return true;
            }
          }
          return false;
        };
      // layer is an array that contains existing pattern
      var layerAllowsPattern = function(layer, pattern) {
        for (var i=0, len=layer.length; i<len; i++) {
          if (!allowPatternsInSameLevel(layer[i], pattern)) {
            return false;
          }
        }
        return true;
      };
      // end helper functions
    
      var newComposition = new Composition();
      var layer = [];
      for (var i=0, len=sequence.length; i<len; i++) {
        if (layerAllowsPattern(layer, sequence[i])) { 
          // we are still in same layer.
          layer.push(sequence[i]);
        } else {  
          // two patterns overlapped, 
          // we push the current layer to composition 
          // and use a new layer for the current pattern.
          newComposition.data.push(layer);
          layer = []; // new array instance to prevent browser using the same array and crashes the data.
          layer.push(sequence[i]);
        }
      }
      // for the last layer
      if (layer.length> 0) newComposition.data.push(layer); 
      return newComposition;
    }
  4. We add a new method to the Quest method that can compare two multilayered pattern compositions and check whether they are equal to each other:

    Quest.prototype.isEqualToComposition = function(composition) {
      var a = this.data;
      var b = composition.data;
    
      // sort each level in both array
      for (var i=0, len=a.length; i<len; i++) {
        a[i].sort();
      }
      for (var i=0, len=b.length; i<len; i++) {
        b[i].sort();
      }
      // flatten both compositions into sequence.
      a = this.toSequence();
      b = composition.toSequence();
    
      if (a.length !== b.length) return false;
      for (var i=0, len=a.length; i<len; i++) {
        if (parseInt(a[i]) !== parseInt(b[i])) return false;
      }
      return true;
    }
  5. In the composition-view.js file, we check whether the player's latest selection matches the quest level. Therefore, in both the selectPattern and undo methods, we keep a composition from the sequence and check it with the quest level:

    selectPattern: function(pattern) {
      ...
      game.composition = game.Composition.createFromSequence(game.compositionSeq);
      if (game.quest.isEqualToComposition(game.composition)){
        game.flow.gameWin();
      }
    },
    undo: function() {
      ...
      game.composition =   game.Composition.createFromSequence(game.compositionSeq);
      if (game.quest.isEqualToComposition(game.composition))   
      {
        game.flow.gameWin();
      }
    },

Objective complete – mini debriefing

The comparison requires us to create a multilayered pattern composition from the selection sequence. Then, we compare the player's one with that of the quest level.

Composition is a two-dimensional array that we need to compare between the quest and the player's selection. However, the player's selection is a sequence that does not reflect our layer's structure. This is the reason why we need a conversion between the composition and the sequence. Moreover, we need to keep a copy of the player's sequence because we need that information for the undo feature.

Comparing players and compositions of the quest

In our createFromSequence method, we created two helper functions to manipulate multilayered patterns. The allowPatternsInSameLevel function checks whether the given two patterns can be overlapped together by looking up the non-overlapped pattern table. The layerAllowsPattern function makes use of the previous function and checks whether the given pattern fits all other patterns inside the given layer, which is an array of all non-overlapped patterns put together.

The createFromSequence method makes use of the function to scan the selection array in a sequence and then put each pattern into either the same layer (without overlapping) or a new layer.

Finally, we have the isEqualToComposition method in the quest instance to compare the player's selection composition with the quest level's one. This method sorts all the layers in both the compositions into the same order and then converts the composition back to a one-dimension array using the toSequence method.

This ensures the player's selection can match our defined level of data on non-overlapping patterns, regardless of the order of selection.

Classified intel

We attach the relationship of non-overlapping patterns directly to the composition object because they are the same, regardless of each instance. Attaching it to the composition instead of the instance's prototype prevents this block of data from being copied to each instance.

 

Showing different quests


In this task, we are going to show how to advance to the next level when the player's composition matches that of the quest compositions in the current level.

Engage thrusters

This time, we need quest-level data in the quest.js file to be accessible from another file, so we need to attach the questLevels and questData methods to the game scope rather than the original local scope:

  1. In the quest.js file, we change the questLevels declaration from var questLevels to game.questLevels.

  2. We apply the same to questData:

    // from
    var questData = questLevels[level]; 
    // to
    var questData = game.questLevels[level];
  3. In the scenes.js file, we display the level with the following function:

    gameScene.updateLevelInfo = function(level) {
      document.getElementById('stage').textContent = "Stage "+ level;
    };
  4. At last, we modify the game flow in the game.js file to count the level:

    game.flow = {
      currentLevel: -1,
      maxLevel: game.questLevels.length - 1,
      startOver: function() {
        ...
        this.currentLevel = -1;
      },
      nextLevel: function() {
        this.currentLevel+=1;
        if (this.currentLevel>= this.maxLevel) this.currentLevel = this.maxLevel;
    
        game.gameScene.updateLevelInfo(this.currentLevel+1); 
        // when displaying level, we start from 1 instead of 0, so +1 here.
        ...
      },
      ...
    }

Objective complete – mini debriefing

The level up is done by counting the current level and selecting the level from the questLevels array. The nextLevel method is used to increase the currentLevel counter. Moreover, this method is called once startButton is clicked from the menu. Therefore, it will increase the currentLevel counter at the beginning. This is why we set the initial value of currentLevel to -1. This will ensure that it selects the first level once the game starts that is at index 0 of the level array. We reset currentLevel in the startOver method once the game is over. On the other hand, we display the current level at the top of the game inside the #stage element. Therefore, we also update that wording every time the level is up.

 

Adding a countdown timer to the game


In this task, we are going to give a final touch to the game by adding a countdown timer. With the timer, the players are not only challenged to select the correct sequence but also restricted in terms of time in order to make the game more exciting for the player.

Prepare for lift off

We will need a new file for the timer. Let's create a new empty file and name it timer.js. With the new file, we also need to import it in the index.html file, which is done using the following code:

<script src='js/timer.js'></script>

The timer would be a horizontal bar at the top of the game. Its width decreases when you count down the time.

Engage thrusters

Let's count down the game with the following steps:

  1. In the index.html file, we need to add a div method for the timer. Add the following code to the HTML inside #game:

    <div id="timer"></div>
  2. Let's move to the game.js file to add the timer control to the game flow:

    gameWin: function() {
      ...
      game.timer.stop();
    },
    gameOver: function() {
      ...
      game.timer.stop();
    },
    startLevel: function() {
      ...
      game.timer.restart();
    },
  3. We remove the following code from the init method in order to prevent the timer from starting during the menu scene:

    var init = function() {
      game.flow.startLevel();   <<<<< Delete this line.
    }
  4. The timer is something that can be reused in other games, so it is worth creating a dedicated file named timer.js. Let's put the following timer code into the newly created timer.js file:

    (function(){
      var game = this.colorQuestGame = this.colorQuestGame || {};
    
      game.timer = {
        interval: undefined,
        countFrom: 60, // second
        count: this.countFrom,
        progressView: document.getElementById('timer'),
        restart: function() {
          if (this.interval) {
            clearInterval(this.interval);
          }
          this.count = this.countFrom;
          this.interval = setInterval((this.tick).bind(this), 1000);
        },
        stop: function() {        
          clearInterval(this.interval);       
        },
        tick: function() {
          this.count -= 1;
          if (this.count<= 0) {
            this.count = 0;
            clearInterval(this.interval);
            game.flow.gameOver();
           }        
          // update the view
          var progress = this.count / this.countFrom * 100;    
          this.progressView.style.width = progress + "%";
        }
      }
    })();
  5. Now, it is time for styling. Let's append the following CSS rules to the game.css file:

    #timer {
      position: absolute;
      top: 0;
      left: 0;
      height: 30px;
      width: 100%;
      background: #7ADAF6 url(images/timer_symbol.png) no-  repeat 5px 50%;  
      border-bottom: 1px solid #4F8EA1;
      transition: all .3s ease-out;
    }

Objective complete – mini debriefing

A simple way to count down is to use the setInterval or setTimeout methods. However, for games, we usually want a global timer to act as the world clock. This way, all the elements will listen for the tick of the timer. When the world clock pauses, all game elements stop, when it resumes, all resume. That's why we have a separate timer object.

The timer has its own interval managed. All we need to do is start and stop the timer and it will run the setInterval and clearInterval methods for us.

By default, the tick function checks whether the time is up and also updates the visual timer DOM node with a width that reflects the countdown.

 

Mission accomplished


We created our first playable game. We learned how to separate data manipulation and visual presentation. We also learned how to manage the game scenes and create logic for each state.

There are many ways to create games with web standard. We explored how to create the game with HTML DOM elements, CSS properties, and JavaScript. There are other approaches such as using canvas-drawing APIs and even with some third-party game libraries. In later projects, we will explore these different approaches.

There are some approaches though, such as WebGL, which we will not discuss in this book. The programming of WebGL requires a different kind of programming skills that is not quite related to traditional web designing skills.

In the next project, we will create a card game that uses flipping and CSS animation techniques.

 

Hotshot challenges


There are always improvements in game development. The following sections act as suggestions that can further enhance the game.

Storing the data in local storage

Currently, the game restarts every time a page is refreshed. We can keep progress by saving the completed level locally. Furthermore, we can store the current level that is in progress so a player resumes at the exact point they left the game. We are going to discuss local storage in Project 5, Building an Isometric City Game, when creating the isometric city-building game.

Collecting stars

A level-unlocking game usually works better with stars collection. In each quest level, we can define three levels of achievement. Each achievement rewards the player with one star. For example, completion of a quest can be worth the first star, no-undo is worth the second star, and the final star is awarded only when a player completes the level very fast.

Then, we may have a total count of all the collected stars, and it can be used to unlock new quests or even unlocking new abilities to help player advances.

About the Author

  • Makzan

    Makzan focuses on the fields of web development and game design. He has over 14 years of experience in building digital products. He has worked on real-time multiplayer interaction games, iOS applications, and rich interactive websites.

    He has written three books, on building a Flash virtual world, and creating games with HTML5 and the latest web standards and developed a video course as well. He currently teaches courses in Hong Kong and Macao SAR. He writes tutorials and shares his know-how on makzan.net.

    Browse publications by this author

Latest Reviews

(1 reviews total)
Excellent