Breakout: An RSS Reader...
Last updated: Jun 10, 2020
THE RESULT (Requires javascript)
When I started this project, my intention was to learn more about Javascript and the HTML5 canvas. I knew I wanted something interactive, and that a game would be an excellent place to begin. What started as a straight-forward tutorial project quickly began to teach me a valuable lesson about scope creep, among other things. All of those what-ifs can add weeks—if not months—to a project. That being said, I learned so much as I went, researching almost everything that finally made it into this creation.
Some things I learned, some challenges I faced:
Angle Calculations:
Right off the bat, the MDN breakout tutorial has the ball moving 2 pixels on each axis at each frame. Following a tutorial only goes so far. When the ball collides with a wall, the game simply negates the number, and the ball goes the same speed and angle in the other direction.
if(wall.collision)
ball.direction.x = -ball.direction.x;
if(top or paddle collision)
ball.direction.y = -ball.direction.y;
That’s it. I wanted a bit more of an authentic breakout experience, so I modified the paddle collision: when the ball collides with the paddle, the game grabs the distance from the ball’s centre to the centre of the paddle, and calculates a new angle based on proximity. This way, the centre of the paddle sends the ball straight up on the y-axis, while either side of paddle centre sends the ball more horizontally the closer to the edge it is, up to about 75°. The result is a rudimentary aiming system!
//called when collision between ball and paddle is detected
calculateNewAngle = function(paddle){
//get centre of paddle
var paddleCenter = paddle.position.x + (paddle.w / 2);
//get ball distance from centre of paddle
var distanceFromCenter = paddleCenter - ball.position.x;
//normalize the distance from centre relative to paddle space (needed to calculate new angle)
var normalizeIntersect = distanceFromCenter / (paddle.w / 2);
//multiply normalized distance by 5 x PI / a dampener (reduces severity of angle)
var newBounceAngle = normalizeIntersect * (5 * Math.PI / 17);
//set new directions using sin/cos and reverse x-axis
this.direction.x = Math.sin(newBounceAngle);
this.direction.y = -Math.cos(newBounceAngle);
this.direction.x = -this.direction.x;
}
RSS:
Somewhere along the line I thought it’d be interesting to have some event triggered when the ball collided with the paddle. This turned out to be a bit harder than I thought, strictly because I used RSS and RSS is old. Being an old technology, most of the popular RSS APIs are deprecated. When the game loads up, it queries the XML feeds of a few different online news sources and loads the headlines into an array of objects, where each object holds the title of the article and a link to its source. On collision the game displays one clickable headline randomly, then removes it from the array to avoid duplicates. Eventually I added the feature to go into the menu and access all of the loaded headlines in one place, since sometimes headlines are missed during the middle of gameplay.
There are a few things I would change:
- I am hosting with GitHub Pages and therefore don’t have access to server functionality. If I did, I would move the RSS Fetch feature to the server and cache the results there. Once an hour or perhaps once a day the server would query the news page xml, compile the results and attempt to remove similar or duplicate headlines. Then, all the client would need to do is make one call to the server to get the complete list. As it stands now, the client makes multiple fetch calls to each different organization and compiles the results in the browser. Not the greatest way to do this. In addition—from what I’ve read—a server query is an effective way to get around CORS blockages.
- Add some function to allow the user to choose which news sources they want to display from a list. Or… allow the user to enter their own URI. If the source doesn’t already exist in the database, fetch it and cache it as well.
Modules, Closures, Import/Export:
As the project grew I knew it’d be necessary to split the classes up into their own files, both for clarity and management of complexity. Up until a point I had been linking the files in my index.html header, and therefore treating each object as though it were from the same (global) scope.
<head>
<script src="input.js" type="text/javascript"></script>
<script src="game.js" type="text/javascript"></script>
<script src="main.js" type="text/javascript"></script>
<script src="ball.js" type="text/javascript"></script>
<script src="paddle.js" type="text/javascript"></script>
<!--and so on...-->
</head>
Then, while playing GTA5, I noticed the {java.update();} COFFEESHOP: Javascript, like java, has an import analogue. Removing the script tags under the header and moving the main entry script into the body tag as a type="module” allowed me to link each script using import and export as needed.
Move entry script to body.
<!--index.html-->
<body>
<!--html content...-->
<script src="main.js" type="module"></script>
</body>
Import relevant scripts.
//main.js
import World from './world.js';
//...
Export relevant scripts.
//world.js
export default function World(){
//...
}
I found that using this system of organization forced me to think in terms of the single responsibility principle. It also helped mitigate some of the disorganization I felt as the project grew. Coming from a c++ object-oriented background, I knew that some of my objects would need references to other objects in the game. Import/Export helped in alleviating some of those organizational pains. That being said, I still ran into issues. For example, my world.js class contains instances of the ball class, and the paddle class. The ball class handles the collision checking with paddle, and therefore needs to know about the paddle class’ properties. In c++ I might’ve passed a reference to an object manager class containing pointers to all relevant objects, minimizing overhead. As far as I can tell, javascript is pass-by-value by default. My naive solution was to pass the relevant class to the necessary location’s init function, copying it so it would get it’s own version of the needed data. So I’d pass the paddle object to ball’s init.
var paddle = new Paddle();
var ball = new Ball();
ball.init(paddle);
This solution quickly loses viability. What if paddle needs access to ball? In order for ball.init to take the paddle object, paddle first needs to be declared. That’s a circularity issue. Do I create a function for every class that accepts all the values of all of the other needed classes in the game? What about when you need certain data at different levels of scope/visibility?
I scrapped this approach and instead opted to pass each class a copy of the world object. It’s hardly an elegant solution, but it solved my visibility issues. If I were to do this project over, I’d create a middle layer between the world and the objects that kept track of each object for the sake of access to each object. Passing the world object also passes around a lot of redundant information. I looked into closures because I was searching for a way to render private variables and reduce the lateral scope of my objects. That being said, I was able to find an effective solution in declaring object properties as var
, versus as properties as with this
.
function World(){
var x = 15;
this.y = 15;
}
var world = new World();
console.log(world.x); //outputs undefined
console.log(world.y); //outputs 15
Collisions:
The simplest collision system for breakout is to use a form of AABB (axis-aligned bounding box) collision. This involves detecting whether the ball hits a brick (or whatever), and if it does, reverse the y-direction of the ball. There are four conditions to trigger a collision:
//when obj1 is an object with radius
export function areColliding(obj1, obj2){
if(
//right side collision
obj1.position.x - obj1.radius <= obj2.position.x + obj2.w &&
//left side collision
obj1.position.x + obj1.radius >= obj2.position.x &&
//bottom collision
obj1.position.y - obj1.radius <= obj2.position.y + obj2.h &&
//top collision
obj1.position.y + obj1.radius >= obj2.position.y)
return true; //collision detected!
else
return false; //no collision
}
Once this initial detection is triggered, the collision needs to be resolved by defining a new behaviour for the colliding objects. The MDN tutorial resolves the collision by merely reversing the y direction of the ball. I wanted something a little more in-depth. Using the above method, for instance, if the ball lands between two adjacent bricks, it will eliminate both bricks while maintaining its initial direction (having reversed its y-axis twice). In addition, the ball will, rather unrealistically, reverse its y-direction even if it hits the left or right side of the brick. If the brick is indestructible, this means that it will get stuck inside of the brick, repeatedly detecting a collision and not resolving back along the x-axis. I fix this by checking which brick edge the ball has collided with after the initial collision detection. For the brick’s top and bottom, the ball’s y-axis direction is reversed, and for the left and right, the ball’s direction on the x-axis is reversed, while maintaining its y direction. Then, I just reposition the ball to be outside of the brick so to avoid the possibility of a collision being detected again. Once one of the four directions are resolved, no more are checked and we continue.
//check to see about which side of the brick the ball is on
//this is dictate which axis to reverse
//right side collision
if(gameWorld.balls[i].position.x > this.position.x + this.w){
//reverse x axis direction
gameWorld.balls[i].direction.x = -gameWorld.balls[i].direction.x;
//move the ball to outside the brick to avoid it getting stuck next loop
gameWorld.balls[i].position.x =
this.position.x +
this.w +
gameWorld.balls[i].radius;
}
//do the same for each edge
Powerups:
Breakout is pretty boring without modifiers, so I added a powerup system. Using my level creation class, I read which type of block to create, then build an array of blocks constituting the level based on screen dimensions. An example:
/*
0 = no brick
1 = special brick
2 = invincible brick
3 = 1 hit brick
4 = 2 hit brick
5 = 3 hit brick
*/
//define the bricks in the level
export const level1 = [
[ 3, 3, 3, 3 ],
[ 3, 3, 1, 3 ],
[ 3, 3, 3, 3 ],
[ 4, 4, 4, 4 ]
];
foreach level(row, rNum){
foreach row(brick, bNum){
switch(brick){
case 1: //special brick
//bNum and rNum are used to determine the x & y positions of the brick
createBrick(bNum, rNum, isSpecial, isInvincible, hp, color, etc);
break;
case etc:
//...
}
}
}
let createBrick = function(bNum, rNum, isSpecial, isInvincible, hp, color){
let pos = {
x: canvasWidth / bricksInRow * bNum,
y: topSpace + brickHeight * rNum
}
bricks.push(new Brick(bricksInRow, pos, gameWorld, isSpecial, isInvincible, hp, color));
}
If the brick is a powerup brick, it is passed a parameter on creation identifying it as such. When a collision is detected with the ball, it spawns a new powerup object and pushes it into the world object’s gameObject array for updating. Then, that new powerup object checks for collision with the paddle as it slowly descends to the bottom of the screen. If it collides with the paddle, it deletes itself and calls the world object’s setPowerUp function with a randomly selected powerup. This function checks to see whether a powerup is already active. If there isn’t an active powerup, then we call back to the randomly selected powerup’s start function. This start function modifies the game in some way. I chose five different modifiers, although more are of course possible: short/long paddle, slow/fast ball, multi-ball. The world object’s setPowerUp function also starts a timer corresponding to the appropriate powerup (the negative ones are shorter while others last longer), that runs until which point the powerup’s stop function is called, resetting the modifiers. All powerups except the multiball last for more than 5 seconds. The multiball is compatible/stackable with other modifiers, doesn’t require a reset and therefore no timer.
Menu:
When building out the menu, I knew I wanted to display an aggregated list of headlines, and figured an overlay would be the best option. The ☰ button triggers (using javascript) both a game pause and the overlay, which switches its display from ‘none’ to ‘block’ and vice versa. I find the best way to work with CSS is to fiddle with settings until they start to resemble the desired effect. So using font-size, background-color, font-family, padding, margins, transparency, z-index, as well as keeping the correct positioning was a matter of tinkering until just right. For example, selecting headlines from the list triggers scrolling, which at first meant my menu’s X (set to position: absolute
) would be stuck attached at the top-left of the page inside the browser. When you scrolled, then, the X would stay behind. To have it follow as you scroll down through the headlines, it needed to be set to position: fixed
.
Another such issue is when I set ☰ with position: absolute
, the height of the element pushes the headline text box downward, triggering awkward scrolling mid-game when longer headlines are displayed. Layering using position and z-index fixes this issue, but, of course, requires some tinkering.