The holidays are over, and now it's time to get back to work. In the last entry, I made a simple collision detector function handle bullet/enemy collision. The first major change to replace propagateEnemies() with a system that generates text as many collidable pieces. During testing, I realized the current detectCollisions() function needs a bit of work. This is the second major change to cover in this post.
As always, if you want to follow along, here is the zipped project folder, there's also a live demo, and now featuring a Github repo! I've also edited the previous posts with this, so the commits will line up properly.Since I want this project to work as a personal website and a playable game, I want to keep things simple for future updates. To that end, I want the website and the game to be two separate views, and have the game read from the content of the website. It looks a little like this:
<section id="game-area">
<div id="enemy-box"></div>
<div class="player"></div>
</section>
<section id="standard-view">
<div class="slide">
<h1 class="slide-header">Slide Title</h1>
<p>Your text here! </p>
</div>
</section>
In the css, #game-area has width and height set to 100%, and the #standard-view is set to display: none. The #standard-view can be styled however you want, but for now, collidable text is only pulled from the <p> tags in the slide, and only one <p> per slide. You can have as many slide divs as you want, and with each slide corresponding to a different page of the website. With all of that in mind, lets talk about the script.
To give it a rough outline, it works in two parts. The first part converts the <p> tags into individual letters and caches it (so slides can be loaded/reloaded later when needed). The second part puts each letter into a jQuery object and appends it to the #game-area.
var textFromPage = []; // Holds all text from the page as letters, separated by slideAuthor's note: There IS an unintended side-effect to this. With each letter being observed separately, 'words' are conceptually ignored, and causes them to wrap around line-breaks funny. The letters make it to the screen just fine, but some more steps will be added later to better adjust final formatting.
var $tiles = []; // Holds all jQuery objects currently on screen
function parseText() {
$('.slide p').each(function() {
var temp = this.innerText.split('');
for (var i = 0; i < temp.length; i++) {
if (!(/\w/.test(temp[i]))) {
temp[i - 1] += temp[i];
temp.splice(i, 1);
i--;
}
}
textFromPage.push(temp);
});
};
function loadLevel(slideNum) {
$tiles = [];
for (i = 0; i < textFromPage[slideNum].length; i++) {
$('#enemy-box').append($('<p>' + textFromPage[slideNum][i] + '</p>').removeClass('hit marked'));
}
$('#enemy-box p').each(function() {
$tiles.push($(this));
});
};
Each letter gets read in and pushed into a temp array. Each temp array gets pushed into textFromPage, such that any letter on the page can be found by textFromPage[slideNumber][letterNumber]. The if statement uses a regexp to look for any whitespaces, and skip them. This effectively lumps the whitespace into the previous character to preserve kearning on the screen, and to prevent creating an 'invisible' tile (a collidable target that can't been seen to be shot at). When a level needs to be loaded, each letter gets a <p> tag in a jQuery object, gets appended to the screen, and then they all get saved to the $tiles array. $tiles comes in handy later for getting object positions, instead of having to iterate through every element for every collision check. There's also a mention of 'hit' and 'marked' classes; those are part of the collision detection, so more on those in a bit.
This is a good time to quickly bring up the css again. To make sure that spacing is handled properly, make sure you add white-space: pre; and display: inline-block; to the #enemy-box p selector. You can also take this time to decide on your own styling: font choice, size, color is all important, but is entirely up to you. The css needed to make this script work is minimal and I only want to touch on the bits that are necessary.
To use these new functions, remove propagateEnemies(), and call these two functions before starting the mainLoop(). In fact, there's enough stuff there now, it can be consolidated to a proper init().
function init() {
window.addEventListener('keydown', handleKeyDown, true);
window.addEventListener('keyup', handleKeyUp, true);
$player = $('.player');
$gameArea = $('#game-area');
$enemybox = $('#enemy-box');
parseText();
loadLevel(0); // Load the first slide by default
}
$(document).ready(function() {
init();
setInterval(mainLoop, 1000 / fps);
});
Now when you load the page, all the text from the first slide is presented as collidable targets. This is the first step. The next step is getting the bullets to stop being laggy, which means better collision detection. The core of the original detectCollisions() is used here, but the rules have been refined and reordered to save time during iteration. A big part of this involves a bullet knowing what it's target is, instead of having to figure out what it's hitting.
When a bullet is created, it looks through the cached objects' positions, determines which ones are in front of it, and chooses the lowest objects on the screen as the targets for that bullet and appends it to the bullet element. You check the position of the bullet against the target position while moving it, and log it as a successful collision. Most of the work is done in a new function fetchTarget(), with a couple small additions to the other bullets functions.
function fetchTarget(bulletPos) {
var hitList = [];
var lowBound = 0;
for (var i = $tiles.length - 1; i >= 0; i--) {
var $this = $tiles[i];
if (!$this.hasClass('marked') &&
(($this.position().left < bulletPos.left + 11 && $this.position().left + $this.width() > bulletPos.left - 5))) {
if ($this.position().top + $this.height() >= lowBound) {
hitList.push($this);
lowBound = $this.position().top + $this.height();
$this.addClass('marked');
} else {
break;
}
}
};
hitList.push(lowBound);
return hitList.reverse();
}
function createBullet() {
var bulletPos = { /* left/top positioning */ };
var bulletTarget = fetchTarget(bulletPos);
$('#game-area').append($('<div/>').addClass('bullet').css(bulletPos).data({ 'target': bulletTarget }));
};
function moveBullets() {
$('.bullet').each(function() {
$(this).animate({ /* animation settings */}).dequeue();
var target = $(this).data('target');
var bulletTop = $(this).position().top;
// Successful bullet-tile collision
if (bulletTop <= target[0]) {
for (var i = 1; i < target.length; i++) {
target[i].addClass('hit');
}
$(this).remove();
}
// Remove bullet when it goes off-screen
if (bulletTop < 25) {
$(this).remove();
}
});
};
The first way to save time is to start from the end of the $tiles list instead of the top. The 'marked' class denotes that particular element has already been targeted by another active bullet can be ignored. You can also skip any element that isn't within range of the bullet (16px total width here). After you've determined everything in range of your shot, you can ignore the elements above (behind) your target. The target elements get marked for collision and pushed to a list with the lowBound. The reverse() call is partly to compensate for scanning the tiles backwards, but it's also easier to call target[0] than target[target.length - 1] when we need to get lowBound.
You'll notice the collided tile element isn't being removed, it gets a 'hit' class. In the css, hit is only set to opacity: 0. The collided tile stays on the screen to preserve spacing, the 'marked' class prevents it from being detected for further collisions, and the 'hit' class keeps it hidden from view.
As an aside, if you wanted to have different weapon styles, this is a good place to start, To make a laser, ignore lowBound and return everything in the left-to-right range. To make a small explosion, return all neighboring elements too. To make a bigger explosion, find return all elements in a defined radius around any target. These are all ideas for later though. In the immediate future, I want to clean up parseText() a bit to recognize words as whole objects, and put loadLevel(n) through some tests. Stay tuned!