The game

The project game we'll build in this chapter is simply called Basic Jelly Wobbling Gravity Game. The goal of the game is to feed our main hero enough jelly that he gets sick and drops to the floor with a severe tummy ache. The main character is controlled through the left and right arrow keys on the keyboard, and in order to eat a jelly, you simply have to direct the hero underneath a falling jelly. Every time you feed the hero a jelly, his health meter decreases slightly. Once enough jelly has been fed, and the health meter reaches zero, the hero gets too sick and faints. If you let a jelly drop on the floor, nothing happens except that the jelly splashes everywhere. This is a Basic Jelly Wobbling Gravity Game. Can you serve Prince George enough jelly until he passes out?

The game

In order to demonstrate a few principles about HTML5 game development, we'll build this game completely with DOM elements. While this approach is normally not the desired approach, you will notice that many games still perform quite nicely on most modern browsers, and on the average desktop or laptop computer today. However, as we'll learn in the chapters to follow, there are a few techniques, tools, and APIs available to us in HTML5 that are far more appropriate for game development.

Also, as is common to this book, most game elements will be kept to a minimum in terms of complexity, so that it can be explained and understood easily. Specifically in this game, we'll only use SVG graphics as a proof of concept instead of diving deeply into the potential and opportunity available to us through the SVG standard. The same goes for drag-and-drop, as there is so much more that can be done with it.

Code structure

The way this code is structured is very straightforward. Every element in the game is absolutely positioned through CSS, and each element is made up of some HTML container styled with a background image or some CSS3 properties that give it a fresh look with rounded corners, drop shadows, and so on. Also, although some people might prefer object oriented programming to functional programming, and better cohesion instead of global variables everywhere, we'll take exactly that approach in this game, and focus on the HTML5 aspects instead of the design of the game. The same goes for the style and quality of the graphics. All that you see in this game was created by myself using a free photo editor program, and it took me no longer than 30 minutes to create all of the graphics you see in the game. This was mostly to show that fun games can be built even if you're on a budget, or don't have a dedicated graphics design team.

Since we're loading all of the SVG entities right inline with our HTML structure, we place them inside a div container that is hidden from the user, then we clone each entity that we need a copy of and use it on the game. We use this technique for all of the jellies and the hero. The hero SVG is left the same as what was exported from the vector editor software. The jelly SVG is slightly modified by removing all of the colors that they were designed with, and replaced with CSS classes. This way we can create different CSS classes that specify different colors, and each new instance of the jelly SVG is assigned a random class. The final result is one single SVG model hidden inside the invisible div container, and each instance of it, using zero extra code, is given a different color to add variety to the game. We could also have randomly assigned a different size and rotation to each jelly instance, but this was left out for an exercise to the reader.

<body>
  <div class="health-bar">
    <span></span>
  </div>

    <h1 id="message"></h1>

    <div id="table"></div>
    <div id="bowl"></div>
    <div id="bowl-top-faux-target"></div>
    <div id="bowl-top" class="dragging-icon bowl-closed"
      draggable="true"
      ondragstart="doOnDragStart(event)"
      ondragend="doOnDragEnd(event)"></div>
    <div id="bowl-top-target"
      ondrop="startGame()"
      ondragover="doOnDrop(event)"
      ondragleave="doOnDragLeave(event)"></div>

    <div class="dom-recs">
      <svg class="hero-svg">
      (…)
      </svg>
      <svg class="jelly-svg">
      (…)
      </svg>
    </div>
</body>

Although we could have used data attributes instead of ID attributes for all of those elements, there would have been no real benefit over using them over the IDs, just as there is really no benefit in using IDs over data-attributes in this situation.

Note how there are two targets where the bowl-top can be dragged onto. Actually, there is really only one, which is the element bowl-top-target. The other element that looks like a target, which was cleverly given an ID of bowl-top-faux-target, is only there for the visual effect. Since a real drop target (an element where a draggable element can be placed at the end of a drag option) is only activated once the mouse pointer moves over it, there wasn't enough room on that table to accomplish the desired effect of showing a small outlined area where bowl-top appears to be dropped.

Finally, there is one global timer used in the game, which controls the frequency at which we call the game loop function, named tick(). Although this is not a chapter on proper game design, I will point out that you should avoid the temptation to create multiple timers for different purposes. Some people out there won't think twice before firing off an event through a unique timer separate from the main game timer. Doing so, especially in HTML5 games, can have negative side effects both in performance and in synchronizing all of the events.

API usage

The three APIs used in this game are audio, SVG, and drag-and-drop. A brief explanation of how each of these APIs were used in the game will follow, where only a general overview of the feature is given. In the next section, however, we'll take a detailed look at what each of these features actually do, and how we can use it in this and other situations. For the complete source code for this game, check out the book's page from Packt Publishing's website.

Web audio

Audio was used both for a never-ending loop used as a background song, as well as individual sound effects that are fired off when a jelly is launched up, bounces off the walls, splatters on the floor, or is eaten by the hungry hero. An old school sound effect is also fired off when the hero finally dies from eating too much jelly.

The way that each audio entity is managed in the game is through a simple encapsulation that holds references to individual sound files, and exposes an interface allowing us to play a file, fade sound files in and out, as well as add new sound files to the list of audios managed by this class. The code for the same is as follows:

// ** By assigning an anonymous function to a variable, JavaScript
// allows us to later call the variable's referenced function with
// the keyword 'new'. This style co function creation essentially 
// makes the function behave like a constructor, which allows us to
// simulate classes in JavaScript
var SoundFx = function() {
  // Every sound entity will be stored here for future use
  var sounds = {};

  // ------------------------------------------------------------
  // Register a new sound entity with some basic configurations
  // ------------------------------------------------------------
  function addSound(name, file, loop, autoplay) {

    // Don't create two entities with the same name
    if (sounds[name] instanceof Audio)
      return false;

      // Behold, the new HTML5 Audio element!
      sounds[name] = new Audio();
      sounds[name].src = file;
      sounds[name].controls = false;
      sounds[name].loop = loop;
      sounds[name].autoplay = autoplay;
    }

    // -----------------------------------------------------------
    // Play a file from the beginning, even if it's already playing
    // -----------------------------------------------------------
  function play(name) {
    sounds[name].currentTime = 0;
    sounds[name].play();
  }

    // -----------------------------------------------------------
    // Gradually adjust the volume, either up or down
    // -----------------------------------------------------------
  function fade(name, fadeTo, speed, inOut) {
    if (fadeTo > 1.0)
      return fadeOut(name, 1.0, speed, inOut);

    if (fadeTo < 0.000)
      return fadeOut(name, 0.0, speed, inOut);

      var newVolume = parseFloat(sounds[name].volume + 0.01 * inOut);

    if (newVolume < parseFloat(0.0))
      newVolume = parseFloat(0.0);

      sounds[name].volume = newVolume;

    if (sounds[name].volume > fadeTo)
      setTimeout(function(){ fadeOut(name, fadeTo, speed, inOut); }, speed);
    else
      sounds[name].volume = parseFloat(fadeTo);

      return sounds[name].volume;
  }

    // -----------------------------------------------------------
    // A wrapper function for fade()
    // ------------------------------------------------------------
    function fadeOut(name, fadeTo, speed) {
      fade(name, fadeTo, speed, -1);
    }

    // -----------------------------------------------------------
    // A wrapper function for fade()
    // -----------------------------------------------------------
    function fadeIn(name, fadeTo, speed) {
      fade(name, fadeTo, speed, 1);
    }


    // -----------------------------------------------------------
    // The public interface through which the client can use the class
    // -----------------------------------------------------------
    return {
      add: addSound,
      play: play,
      fadeOut: fadeOut,
      fadeIn: fadeIn
    };
};

Next, we instantiate a global object of this custom SoundFx type, where every sound clip used in the game is stored. This way, if we want to play any type of sound, we simply call the play method on this global reference. Take a look at the following code:

// Hold every sound effect in the same object for easy access
var sounds = new SoundFx();

// Sound.add() Parameters:
// string: hash key
// string: file url
// bool: loop this sound on play?
// bool: play this sound automatically as soon as it's loaded?
sounds.add("background", "sound/techno-loop-2.mp3", true,  true);
sounds.add("game-over",  "sound/game-over.mp3",     false, false);
sounds.add("splash",     "sound/slurp.mp3",         false, false);
sounds.add("boing",      "sound/boing.mp3",         false, false);
sounds.add("hit",        "sound/swallow.mp3",       false, false);
sounds.add("bounce",     "sound/bounce.mp3",        false, false);

Scalable Vector Graphics (SVG)

As mentioned previously, the way SVG is used in the game is limited simply because the SVG spec is so robust and can get fairly complex. As you'll see in the in-depth description of the SVG API, there were a lot of things that we could have done to each individual primitive shape drawn through SVG (such as natively animating the hero's facial expressions, or making each jelly jiggle or rotate, and so on).

The way we switch the sprite that represents a jelly into a splashed out jelly, when a jelly hits the floor is pretty clever. When we draw the jelly vectors using the vector editor software, we create two separate images, each representing a different state of the jelly. Both images are stacked on top of each other, so that they line up properly. Then, inside the HTML code, we assign a CSS class to each of these images. These classes are called jelly-block and splash, representing a jelly in its natural state, and a jelly splashed on the floor. In both of these classes, one of the vectors is hidden and the other is not. Depending on the state of each jelly element, these two classes are toggled back and forth. This is all done by simply assigning one of the two classes jelly-svg-on and jelly-svg-off to the parent svg element holding these two vector groups, as shown in the following code:

.jelly-svg-off g.jelly-block, .jelly-svg-on g.splash {
    display: none;
}

.jelly-svg-off g.splash, .jelly-svg-on g.jelly-block {
    display: block;
}

The way the preceding styles are driven is simple. By default, every jelly element is given a CSS class of jelly-svg-on, meaning that the jelly is not splashed. Then, when a jelly is calculated to have hit the floor, we remove that class, and add the CSS class of jelly-svg-off, as seen in the following code snippet:

// Iterate through each jelly and check its state
for (var i in jellies) {

  // Don't do anything to this jelly entity if it's outside the screen,
  // was eaten, or smashed on the floor
  if (!jellies[i].isInPlay())
    continue;

    // Determine if a jelly has already hit the floor
    stillFalling = jellies[i].getY() + jellies[i].getHeight() * 2.5 < document.body.offsetHeight;

    // If it hasn't hit the floor, let gravity move it down
    if (stillFalling) {
      jellies[i].move();
    } else {

    // Stop the jelly from falling
    jellies[i].setY(document.body.offsetHeight - jellies[i].getHeight() - 75);

      // Swap the vectors
      jellies[i].swapClass("jelly-svg-on", "jelly-svg-off");
      jellies[i].setInPlay(false);

      // Play the corresponding sound to this action
      sounds.play("splash");
    }
}

Drag-and-drop

Similar to the way SVG was used in the game, drag-and-drop made its way into the final product taking a backseat to web audio. Yet, the role that drag-and-drop plays in the game is arguably the most important one, it starts the game. Instead of having the game start playing right away when the page first loads, or instead of having the user press a button or hit a key to start game play, the player needs to drag the lid away from the bowl where all the jellies are stored, and place it next to the bowl on the table where it sits.

The way drag-and-drop works in HTML5 is simple and intuitive. We register at least one object to be a draggable object (the one you drag around), and at least one other object to be a drop target (the object where the draggable can be dropped into). Then we register callback functions for whatever events we want that apply to the dragging and dropping behavior.

In the game, we only listen for five events, two on the draggable element, and three on the drop target element. First, we listen for when the draggable is first dragged by the user (on drag start), which we respond to by making the bowl lid image invisible and placing a copy of the lid behind the mouse pointer, so that it appears that the user is truly dragging that lid.

Next, we listened for the event that is triggered when the user finally releases the mouse button, indicating the end of the dragging action (on drag end). At this point, we simply restore the bowl lid back to where it was originally, on top of the bowl. This event is fired whenever the dragging action is finished, and the drop was not done inside a valid drop target (the user didn't drop the lid where it was expected), which essentially restarts the process.

The three events that we listen for on the drop target are the onDragLeave, onDragOver, and onDrop. Whenever a draggable is dropped inside a drop target, the target's onDrop event is fired. In this case, all we do is call the startGame() function, which sets the game in motion. As part of the set up for this startGame function, we move the bowl lid element into the exact pixel position where it was dropped, and remove the draggable attribute, so that the user can no longer drag that element.

The functions onDragOver and onDragLeave are triggered whenever the mouse pointer is moved on top of, and hovered out of the target object, respectively. In our case, all we do in each of those functions is toggle the visibility of the bowl lid and the image that shows behind the cursor while the dragging is happening. This can be seen in the following code:

// ------------------------------------------------------------
// Fired when draggable starts being dragged (onDragStart)
// ------------------------------------------------------------
function doOnDragStart(event) {
  if (bowlTop.isReady) {
    event.target.style.opacity = 0.0;
    event.dataTransfer.setDragImage(bowlTop, 100, 60);
  }
}

// ------------------------------------------------------------
// Fired when draggable is released outside a target (onDragEnd)
// ------------------------------------------------------------
function doOnDragEnd(event) {
  event.target.style.opacity = 1.0;
  document.querySelector("#bowl-top-faux-target").style.opacity = 0.0;
}

// ------------------------------------------------------------
// Fired when draggable enters target (onDragOver)
// ------------------------------------------------------------
function doOnDragOver(event) {
  event.preventDefault();
  document.querySelector("#bowl-top-faux-target").style.opacity = 1.0;
}

// ------------------------------------------------------------
// Fired when draggable is hovered away from a target (onDragLeave)
// ------------------------------------------------------------
function doOnDragLeave(event) {
  document.querySelector("#bowl-top-faux-target").style.opacity = 0.0;
}

// ------------------------------------------------------------
// Fired when draggable is dropped inside a target (onDrop)
// ------------------------------------------------------------
function startGame() {

  // Keep the game from starting more than once
  if (!isPlaying) {

    // Register input handlers
    document.body.addEventListener("keyup", doOnKeyUp);
    document.body.addEventListener("keydown", doOnKeyDown);

    // Reposition the bowl lid
    var bowlTop = document.querySelector("#bowl-top");
    bowlTop.classList.remove("bowl-closed");
    bowlTop.style.left = (event.screenX - bowlTop.offsetWidth + 65) + "px";
    bowlTop.style.top = (event.screenY - bowlTop.offsetHeight + 65 * 0) + "px";

    // Disable dragging on the lid by removing the HTML5 draggable attribute
    bowlTop.removeAttribute("draggable");
    bowlTop.classList.remove("dragging-icon");

    newJelly();
      isPlaying = true;

      // Start out the main game loop
      gameTimer = setInterval(tick, 15);
    }
};
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.15.22.160