Last time I implemented the player flying around, but now it’s time for letting the player shoot.
First thing I do is create a own class for Bullet objects which takes a position and rotation as an argument:
class Bullet{
constructor(pos, rotation){<
this.pos = createVector(pos.x, pos.y);
this.dir = p5.Vector.fromAngle(rotation - PI / 2).normalize();
}
tick(){
}
render(){
}
}
And in my "sketch.js" I'll add a array to hold my bullets, loop through them to tick and render them, and add a way to create them in the keyPressed function when pressing space, sending my players position and rotation as arguments:
var bullets = [];
function draw() {
...
for(let i = 0; i < bullets.length; i++) bullets[i].render();
}
function tick() {
...
for(let i = 0; i < bullets.length; i++) bullets[i].tick();
}
function keyPressed() {
...
//Spacebar
if(keyCode == 32){
bullets.push(new Bullet(ship.pos, ship.rotation));
}
}
By taking the position and rotation from the player, I can create the bullet right on top of the player and give it the same direction of the player. I also added the tick and render functions:
class Bullet{
constructor(pos, rotation){
this.pos = createVector(pos.x, pos.y); //Create a new position based on player position
////Creates a direction vector based off on players rotation. Offset it by Pi/2 to match the ship
this.dir = p5.Vector.fromAngle(rotation - PI / 2).normalize();
this.speed = 15; //Bullet speed
this.size = 3; //Bullet size
//Creates a velocity vector by multiplying dir vector with the speed
this.vel = this.dir.mult(this.speed);
}
tick(){
this.pos.add(this.vel); //Moves the bullets pos by the velocity
}
render(){
ellipse(this.pos.x, this.pos.y, this.size); //Renders the bullet
}
}
After watching some footage of the original game, I saw that the bullets also goes from one side of the screen to the other. I added the feature by simply copy-pasting the previous made code from "ship.js":
tick(){
this.pos.add(this.vel); //Moves the bullets pos by the velocity
//Keeps the bullet inside the canvas
if(this.pos.x > width + this.size / 2){
this.pos.x = 0;
}else if(this.pos.x < 0 - this.size / 2){
this.pos.x = width;
}
if(this.pos.y > height){
this.pos.y = 0
}else if(this.pos.y < 0){
this.pos.y = height;
}
}
However, I need to remove the bullet after it has traveled a certain distance. To do that I created a variable to keep track of the distance traveled, and mark it for removal:
constructor(pos, rotation){
...
this.dist = 0; //Distance traveled
this.maxDist = width; //Max distance before removal
this.remove = false;
}
tick(){
...
this.dist += abs(this.vel.x) + abs(this.vel.y); //Records the distance travelled
if(this.dist >= this.maxDist) this.remove = true; //If traveled above the limit, mark for removal
...
}
The reason I mark it for removal instead of directly removing it on the spot, is because in the tick function I'm still looping through all the bullets. Removing something from a array/list while using it, usually leads to problems with the index number.
Now, to remove the bullets all I have to do is loop through the bullet array backwards (to avoid problems with the index number when you remove an object) and remove those marked in my "sketch.js":
function tick() {
...
//Bullets tick
for (let i = 0; i < bullets.length; i++) bullets[i].tick();
for (let i = bullets.length - 1; i >= 0; i--) {
if (bullets[i].remove) bullets.splice(i, 1);
}
}
And the player can now shoot bullets!
Now we need something to shoot! For rendering and rotating the asteroids I used some of the same code I made for the player:
class Asteroid {
constructor(x, y, size) {
this.pos = createVector(x, y);
this.size = size;
this.radius = 15;
this.angularSpeed = random(0.01, 0.03);
this.rotation = random(0, 2 * PI);
this.rotation = 0;
}
tick() {
this.rotation += this.angularSpeed;
}
render() {
push(); //Makes so that translate and rotate functions only apply to this object
translate(this.pos.x, this.pos.y); //Moves the (0, 0) point to given coords
rotate(this.rotation); //Rotates the object in radians
noFill(); //Draw shapes without fill
stroke(255); //Set the stroke color to white
let r = this.radius;
let ox = -6, oy = -17; //Offsets the asteroid
line(-r * 1.5 + ox, r / 2 + oy, -r / 2 + ox, -r / 2 + oy);
line(-r / 2 + ox, -r / 2 + oy, r / 2 + ox, r / 4 + oy);
line(r / 2 + ox, r / 4 + oy, r * 2 + ox, 0 + oy);
line(r * 2 + ox, 0 + oy, r * 1.75 + ox, r + oy);
line(r * 1.75 + ox, r + oy, r * 2 + ox, r * 2 + oy);
line(r * 2 + ox, r * 2 + oy, r / 2 + ox, r * 3 + oy);
line(r / 2 + ox, r * 3 + oy, -r + ox, r * 2.5 + oy);
line(-r + ox, r * 2.5 + oy, -r * 1.5 + ox, r / 2 + oy);
pop(); //Makes so that translate and rotate functions only apply to this object
}
}
I render multiple line segments to make it look like the asteroids in the game. I decided to render it using a radius variable, since I need to have different sized asteroids. The idea worked perfectly, though I noticed a problem where the asteroids x and y coordinates were not centered around where I actually draw the asteroid. That creates a big problem since I rotate the asteroid around that point. To fix that I created an "ox" and an "oy" variable to offset where I could just offset the rendered asteroid.
I create an array of asteroids and loop through them to both tick and render them, and voila!
The last thing to do now is making the bullets explode the asteroids. I first need to find a way to do hit detection. I decided to just create some rectangles for the bullets and asteroids since it's the most efficient method. The hitboxes won't align properly with what's rendering the screen though.
P5.js doesn't have any build in support for hitbox detection so I need to create that myself. First thing I do is create "rectangles" for my asteroids and bullets:
Asteroids
tick() {
...
//Hitbox
this.rectLeftSide = createVector(this.pos.x - this.radius * 1.5, this.pos.y - this.radius * 1.5);
this.rectRightSide = createVector(this.pos.x + this.radius * 1.5, this.pos.y + this.radius * 1.5);
}
Bullets
tick(){
...
//Hitbox
//Only create right side since I already have left side as "pos"
this.rectRightSide = createVector(this.pos.x + this.size, this.pos.y + this.size);
}
Creating a vector every single tick is not really good performance wise, but for this simple little game it will be fine. I don't really create a rectangle in these objects, but rather keep track off where the top-left side and bottom right-side of a rectangle would be.
Now I need a function to check if two "rectangles" are intersecting. Funnily enough, the fastest way to check if two rectangles are intersecting is to check if they are not intersecting at all:
function intersects(r1x1, r1y1, r1x2, r1y2, r2x1, r2y1, r2x2, r2y2){
return !(r1x1 > r2x2 || r2x1 > r1x2 || r1y1 > r2y2 || r2y1 > r1y2);
}
I can now make a nested loop to check if a bullet and an asteroid is collided, and remove them both:
function tick() {
...
//Hitbox detection
for (let i = 0; i < bullets.length; i++){
for (let j = 0; j < asteroids.length; j++){
var b = bullets[i];
var a = asteroids[j];
if(intersects(b.pos.x, b.pos.y, b.rectRightSide.x, b.rectRightSide.y,
a.rectLeftSide.x, a.rectLeftSide.y, a.rectRightSide.x, a.rectRightSide.y)){
a.remove = true;
b.remove = true;
}
}
}
}
Next time I need to make the asteroid split up instead of just being removed, spawn asteroids and make them move!
Full code for "sketch.js"
var ship;
var bullets = [];
var asteroids = [];
function setup() {
createCanvas(800, 640);
ship = new Ship(width / 2, height / 2, 12);
asteroids.push(new Asteroid(width / 2, height / 4, 1));
}
function draw() {
background(0);
tick();
ship.render();
for (let i = 0; i < bullets.length; i++) bullets[i].render();
for (let i = 0; i < asteroids.length; i++) asteroids[i].render();
}
function tick() {
ship.tick();
//Bullets tick
for (let i = 0; i < bullets.length; i++) bullets[i].tick();
for (let i = bullets.length - 1; i >= 0; i--) {
if (bullets[i].remove) bullets.splice(i, 1);
}
//Asteroids tick
for (let i = 0; i < asteroids.length; i++) asteroids[i].tick();
for (let i = asteroids.length - 1; i >= 0; i--) {
if (asteroids[i].remove) asteroids.splice(i, 1);
}
//Hitbox detection
for (let i = 0; i < bullets.length; i++){
for (let j = 0; j < asteroids.length; j++){
var b = bullets[i];
var a = asteroids[j];
if(intersects(b.pos.x, b.pos.y, b.rectRightSide.x, b.rectRightSide.y,
a.rectLeftSide.x, a.rectLeftSide.y, a.rectRightSide.x, a.rectRightSide.y)){
a.remove = true;
b.remove = true;
}
}
}
}
function keyPressed() {
if (keyCode == UP_ARROW) ship.setAcceleration(0.1);
if (keyCode == LEFT_ARROW) {
ship.rotate(-0.075);
} else if (keyCode == RIGHT_ARROW) {
ship.rotate(0.075);
}
//Spacebar
if (keyCode == 32) {
bullets.push(new Bullet(ship.pos, ship.rotation));
}
}
function keyReleased() {
if (keyCode == UP_ARROW) ship.setAcceleration(0);
if (keyCode == LEFT_ARROW) {
ship.rotate(0);
}
if (keyCode == RIGHT_ARROW) {
ship.rotate(0);
}
}
function intersects(r1x1, r1y1, r1x2, r1y2, r2x1, r2y1, r2x2, r2y2){
return !(r1x1 > r2x2 || r2x1 > r1x2 || r1y1 > r2y2 || r2y1 > r1y2);
}
[collapse]
Full code for "ship.js"
class Ship {
constructor(x, y, size) {
this.pos = createVector(x, y);
this.size = size;
this.acc = createVector(0, 0);
this.vel = createVector(0, 0);
this.rotation = 0;
this.angularSpeed = 0;
this.dir = createVector(0, 0);
this.addedAcc = 0;
this.friction = 0.015;
this.maxVel = 10;
}
tick() {
//Movement
this.movement();
this.rotation += this.angularSpeed;
}
movement() {
//Acceleration
this.acc = p5.Vector.fromAngle(this.rotation - PI / 2).normalize().mult(this.addedAcc);
//Velocity
this.vel.add(this.acc);
//Prevents Ship from going too fast
if(this.vel.x > this.maxVel) this.vel.x = this.maxVel;
if(this.vel.y > this.maxVel) this.vel.y = this.maxVel;
if(this.vel.x < -this.maxVel) this.vel.x = -this.maxVel;
if(this.vel.y < -this.maxVel) this.vel.y = -this.maxVel;
//Moves the ship according to the velocity
this.pos.add(this.vel);
//Friction
let frictionVec = createVector(this.vel.x, this.vel.y);
if(this.vel.x != 0 || this.vel.y != 0) this.vel.add(frictionVec.normalize().mult(-this.friction));
//Keeps the ship inside the canvas
if(this.pos.x > width + this.size / 2){
this.pos.x = 0;
}else if(this.pos.x < 0 - this.size / 2){
this.pos.x = width;
}
if(this.pos.y > height){
this.pos.y = 0
}else if(this.pos.y < 0){
this.pos.y = height;
}
}
render() {
push(); //Makes so that translate and rotate functions only apply to this object
translate(this.pos.x, this.pos.y); //Moves the (0, 0) point to given coords
rotate(this.rotation); //Rotates the object in radians
noFill(); //Draw shapes without fill
stroke(255); //Set the stroke color to white
triangle(-this.size, this.size, 0, -this.size * 1.5, this.size, this.size) //Draws a triangle
pop(); //Makes so that translate and rotate functions only apply to this object
}
setAcceleration(acc) {
this.addedAcc = acc;
}
rotate(angularSpeed) {
this.angularSpeed = angularSpeed;
}
}
[collapse]
Full code for "bullet.js"
class Bullet{
constructor(pos, rotation){
this.pos = createVector(pos.x, pos.y); //Create a new position based on player position
////Creates a direction vector based off on players rotation. Offset it by Pi/2 to match the ship
this.dir = p5.Vector.fromAngle(rotation - PI / 2).normalize();
this.speed = 15; //Bullet speed
this.size = 3; //Bullet size
//Creates a velocity vector by multiplying dir vector with the speed
this.vel = this.dir.mult(this.speed);
this.dist = 0; //Distance traveled
this.maxDist = width; //Max distance before removal
this.remove = false;
this.rectRightSide = createVector(pos.x + this.size, pos.y + this.size);
}
tick(){
this.pos.add(this.vel); //Moves the bullets pos by the velocity
this.dist += abs(this.vel.x) + abs(this.vel.y); //Records the distance travelled
if(this.dist >= this.maxDist) this.remove = true; //If traveled above the limit, mark for removal
//Keeps the bullet inside the canvas
if(this.pos.x > width + this.size / 2){
this.pos.x = 0;
}else if(this.pos.x < 0 - this.size / 2){
this.pos.x = width;
}
if(this.pos.y > height){
this.pos.y = 0
}else if(this.pos.y < 0){
this.pos.y = height;
}
//Hitbox
//Only create right side since I already have left side as "pos"
this.rectRightSide = createVector(this.pos.x + this.size, this.pos.y + this.size);
}
render(){
fill(255);
stroke(255);
ellipse(this.pos.x, this.pos.y, this.size); //Renders the bullet
}
}
[collapse]
Full code for "asteroid.js"
class Asteroid {
constructor(x, y, size) {
this.pos = createVector(x, y);
this.size = size;
this.radius = 15;
this.angularSpeed = random(0.01, 0.03);
this.rotation = random(0, 2 * PI);
this.rotation = 0;
this.rectLeftSide = createVector(this.pos.x - this.radius * 1.5, this.pos.y - this.radius * 1.5);
this.rectRightSide = createVector(this.radius * 3, this.radius * 3);
}
tick() {
this.rotation += this.angularSpeed;
//Hitbox
this.rectLeftSide = createVector(this.pos.x - this.radius * 1.5, this.pos.y - this.radius * 1.5);
this.rectRightSide = createVector(this.pos.x + this.radius * 1.5, this.pos.y + this.radius * 1.5);
}
render() {
push(); //Makes so that translate and rotate functions only apply to this object
translate(this.pos.x, this.pos.y); //Moves the (0, 0) point to given coords
rotate(this.rotation); //Rotates the object in radians
noFill(); //Draw shapes without fill
stroke(255); //Set the stroke color to white
let r = this.radius;
let ox = -6, oy = -17; //Offsets the asteroid
line(-r * 1.5 + ox, r / 2 + oy, -r / 2 + ox, -r / 2 + oy);
line(-r / 2 + ox, -r / 2 + oy, r / 2 + ox, r / 4 + oy);
line(r / 2 + ox, r / 4 + oy, r * 2 + ox, 0 + oy);
line(r * 2 + ox, 0 + oy, r * 1.75 + ox, r + oy);
line(r * 1.75 + ox, r + oy, r * 2 + ox, r * 2 + oy);
line(r * 2 + ox, r * 2 + oy, r / 2 + ox, r * 3 + oy);
line(r / 2 + ox, r * 3 + oy, -r + ox, r * 2.5 + oy);
line(-r + ox, r * 2.5 + oy, -r * 1.5 + ox, r / 2 + oy);
pop(); //Makes so that translate and rotate functions only apply to this object
}
}
[collapse]