Asset loading, Audio and Player Input
The HTML file is essentially the same as the previous lesson, it just loads more Javascript files. FPSMeter and GameLoopManager are the same as before so I'll omit them. Here are the links to the rest of the source files:
- https://ajax.googleapis.com/ajax/libs/jquery/1.6.2/jquery.min.js
- js/JareUtils.js: contains a new function GetRelativePosition() to get the mouse coordinates relative to the canvas.
- js/AudioManager.js: a small audio library written as a Singleton.
- js/InputManager.js: a Singleton to manage keyboard and mouse input.
- js/Lesson.js: the code for this lesson.
Note the presence of jQuery which is loaded from the googleapis site. jQuery is a fantastic library that abstracts many common JavaScript operations and differences between browsers. Read more about it here. If you see a '$' (dollar) sign in my JavaScript code, it probably means it's using some jQuery feature. Learn jQuery and love it.
This lesson shows a really basic usage of audio in an HTML5 game. Let me get this off my chest: HTML5 audio SUCKS. The Audio object API is badly designed, terribly implemented in most browsers, and horribly inconsistent across different browsers and devices. HTML5-only audio is basically unusable for games, and one of the biggest barriers to create truly complex HTML5 games. For this reason, I will leave the Audio code at its most basic, simplistic and (depending on the browser) buggy - a simple and decent audio engine for HTML5 games right now is just not possible.
This is the main JavaScript file, js/Lesson.js:
// ---------------------------------------- // Actual game code goes here. // Global vars fps = null; canvas = null; ctx = null; // ---------------------------------------- // Our 'game' variables var posX = 0; var posY = 0; var velX = 100; var velY = 100; var sizeX = 80; var sizeY = 40; var gravityY = 900; var paused = true;
Just a couple of new variables, one to hold the value of gravity acceleration, and another to track the paused / unpaused state of the game.
function GameTick(elapsed) { fps.update(elapsed); // --- Input InputManager.padUpdate();
Here I just called the InputManager's padUpdate() function. This should be done once per frame. While the InputManager can provide very complete information about the inputs performed by the user, for now I will just use the 'pad button pressed' convenience feature in the manner you see below.
// --- Logic if (InputManager.padPressed & InputManager.PAD.CANCEL) paused = !paused;
The padPressed property contains a map of bits corresponding to the pad buttons that were just pressed this frame. It will not trigger the buttons again until they are released and then pressed again. These button bits are tested using the 'and' & operator and InputManager.PAD.xxx constants (OK, CANCEL, UP, DOWN, LEFT and RIGHT).
if (!paused) { if ((InputManager.padPressed & InputManager.PAD.OK) && velY >= -10) { AudioManager.play("jump"); velY = -1000; } // Movement physics posX += velX*elapsed; posY += (velY + 0.5*gravityY*elapsed)*elapsed; velY += gravityY*elapsed; // Collision detection and response var bouncedX = false, bouncedY = false; if ( (posX <= 0 && velX < 0) || (posX >= canvas.width-sizeX && velX > 0) ) { velX = -velX; bouncedX = true; } if ( (posY <= 0 && velY < 0) || (posY >= canvas.height-sizeY && velY > 0) ) { velY = -velY*0.7; bouncedY = true; } if (bouncedX) AudioManager.play("ping"); if (bouncedY) AudioManager.play("bounce"); }
Logic is slightly more complex now, there is gravity and the ability to kick the box back up when it's falling. You can see the first uses of the AudioManager.play() function to trigger sounds.
Also note how I have added some energy loss to the vertical collisions, which means that the object will gradually reduce its vertical movement until it remains at the bottom... and then slowly drops while annoying everyone with repeated plays of the bounce sound. This happens because every frame the object detects a collision against the ground but doesn't correct its position, which is now slightly below the ground. Over time it slowly sinks due to gravity, detecting the collision and triggering the sound effect every frame but unable to get out. Once we are writing an actual game, we will correct this problem by having a specific state for objects that are standing on solid ground.
// --- Rendering // Clear the screen var grad = ctx.createLinearGradient(0, 0, 0, canvas.height); grad.addColorStop(0, '#06B'); grad.addColorStop(0.9, '#fff'); grad.addColorStop(0.9, '#3C0'); grad.addColorStop(1, '#fff'); ctx.fillStyle = grad; ctx.fillRect(0, 0, canvas.width, canvas.height); // Render objects ctx.strokeRect(posX, posY, sizeX, sizeY); ctx.fillStyle = "red"; ctx.font = "10px sans-serif"; ctx.fillText("Hello World!", posX+10, posY+25); // Paused / Unpaused text ctx.fillStyle = "white"; ctx.font = "22px sans-serif"; ctx.fillText(paused? "Paused" : "Running", 380, 25); }
For rendering I have added a nice gradient to the background (purely to practice canvas operations), and set font details explicitly.
$(document).ready(function () { canvas = document.getElementById("screen"); ctx = canvas.getContext("2d"); fps = new FPSMeter("fpsmeter", document.getElementById("fpscontainer")); InputManager.connect(document, canvas);
Here we are initializing the InputManager. It performs its job by hooking into some events from the document and the canvas elements.
// Async load audio and start gameplay when loaded AudioManager.load({ 'ping' : 'sound/guitar', 'jump' : 'sound/jump', 'bounce' : 'sound/bounce1' }, function() { // All done, go! InputManager.reset(); GameLoopManager.run(GameTick); } ); });
This one is very interesting. In JavaScript, there are a lot of places where it is desirable to perform operations asynchronously. When loading assets like sounds, images or AJAX data, it's practically impossible to do that synchronously. Many asset loading functions will often take a callback function as a parameter, which gets called when the asset has been completely loaded and is ready to be used.
In this case, we provide the AudioManager a list of sound clips to load, and then wait for it to call the callback function to kick off the GameLoopManager and start running the actual game. We also use that opportunity to reset the InputManager, that's good practice so there are not leftover keypresses that happened during the load. We will later extend this pattern to initialize other game subsystems.