Skip to content

Commit 9d8f7c1

Browse files
committed
t14:00 Added ship collision
1 parent 457d4ef commit 9d8f7c1

File tree

4 files changed

+156
-42
lines changed

4 files changed

+156
-42
lines changed

Diff for: README.md

+38-3
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ Since I've already worked on a project to reproduce [PONG](https://armlessjohn40
2121
* Implement collision mechanics
2222
~~* Collision with the black hole~~
2323
~~* Collision with the borders~~
24-
* Collision between Ships
24+
~~* Collision between Ships~~
2525
* Collision between Shots
2626
* Collision Ship-Shots
2727
* Create gameover screen
@@ -149,6 +149,41 @@ function addGravity(element, cx, cy, gravity) {
149149

150150
## Implement collision mechanics
151151
### 11:35 - Collision with the black hole
152-
The black hole spawns any Ship that reaches its position to a random position in the board with `speed=0`.
152+
The black hole spawns any Ship that reaches its position to a random position in the board with `speed=0`. This is checked in the object's update method.
153153
### 12:20 - Collision with the borders
154-
The game board wraps around itself, making it infinite. So, whenever a player or shot reaches the borders, they're spawned back in the other side of the board.
154+
The game board wraps around itself, making it infinite. So, whenever a player or shot reaches the borders, they're spawned back in the other side of the board. This also happens for the `Shot` class. The collision is checked in the object's update method.
155+
### 14:00 - Collision between Ships
156+
When the two players collide, there is an explosion and the game should end. This check is made in the gameloop's update method.
157+
The collision is calculated using [Separating Axis Theorem](https://en.wikipedia.org/wiki/Hyperplane_separation_theorem). It ended up in a function with 15 constants and a single if statement to tell whether the ships have collided.
158+
159+
```javascript
160+
checkCollision = function(sprite1, sprite2) {
161+
// Limits of the sprite
162+
const p1c = sprite1.corners;
163+
const p2c = sprite2.corners;
164+
// Translate sprites to make p1c[0] the origin
165+
const p1cT = sprite1.corners.map(val => [val[0]-p1c[0][0], val[1]-p1c[0][1]]);
166+
const p2cT = sprite2.corners.map(val => [val[0]-p1c[0][0], val[1]-p1c[0][1]]);
167+
// Calculate the rotation to align the p1 bounding box
168+
const angle = Math.atan2(p1cT[2][1], p1cT[2][0]);
169+
// Rotate vetcors to align
170+
const p1cTR = p1cT.map(val => gameScreen.rotateVector(val, angle));
171+
const p2cTR = p2cT.map(val => gameScreen.rotateVector(val, angle));
172+
// Calculate extreme points of the bounding boxes
173+
const p1left = Math.min(...p1cTR.map(value => value[0]))
174+
const p1right = Math.max(...p1cTR.map(value => value[0]))
175+
const p1top = Math.min(...p1cTR.map(value => value[1]))
176+
const p1bottom = Math.max(...p1cTR.map(value => value[1]))
177+
const p2left = Math.min(...p2cTR.map(value => value[0]))
178+
const p2right = Math.max(...p2cTR.map(value => value[0]))
179+
const p2top = Math.min(...p2cTR.map(value => value[1]))
180+
const p2bottom = Math.max(...p2cTR.map(value => value[1]))
181+
// Check if shadows overlap in both axes
182+
if (p2left < p1right && p1left < p2right && p2top < p1bottom && p1top < p2bottom) return true;
183+
return false;
184+
}
185+
```
186+
187+
I also created an `explode` method in the `Ship` class so when they collide, it shows a satisfying explosion. The explosion have 4 frames of dots generated randomly with varying radius.
188+
189+
![explosion](report-assets/explosion.gif "explosion")

Diff for: draw.js

+68-19
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,13 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
2222
const ROTATION_SPEED = 3;
2323
const THRUSTER_SPEED = 0.002;
2424
const FIRE_LENGTH = 10;
25-
const SHOT_DISTANCE = 300;
25+
const SHOT_DISTANCE = 200;
2626
const SHOT_SPEED = 1;
2727
const SHOT_SIZE = 5;
2828
const SHOT_INTERVAL = 500;
2929
const BLACKHOLE_SIZE = 12;
3030
const MAXACCEL = 1;
31+
const BLAST_SIZE = 50;
3132

3233
function drawArray(array, width=1, color="#FFF") {
3334
array = array.slice();
@@ -100,6 +101,10 @@ function addGravity(element, cx, cy, gravity) {
100101
element.speedY += (fy<MAXACCEL?fy:MAXACCEL);
101102
}
102103

104+
function checkNumber(number) {
105+
return !isNaN(parseFloat(number)) && isFinite(number);
106+
}
107+
103108
class BaseSprite {
104109
constructor(x, y) {
105110
this.x = x;
@@ -111,6 +116,31 @@ class BaseSprite {
111116
update() {
112117
this.x += this.speedX;
113118
this.y += this.speedY;
119+
120+
// border collision
121+
let spriteToBorder = Game.radius - Math.sqrt(Math.pow(this.x -
122+
Game.width/2,2) + Math.pow(this.y - Game.height/2, 2));
123+
if (spriteToBorder <= 0) {
124+
let angle = Math.atan2(this.y-Game.height/2, this.x-Game.width/2)+Math.PI;
125+
let x = (Game.radius-10)*Math.cos(angle)+Game.width/2;
126+
let y = (Game.radius-10)*Math.sin(angle)+Game.height/2;
127+
this.resetSprite(x, y);
128+
}
129+
}
130+
resetSprite(x, y, rotation=false, speedX=false, speedY=false) {
131+
if (checkNumber(speedX)) this.speedX = speedX;
132+
if (checkNumber(speedY)) this.speedY = speedY;
133+
if (checkNumber(rotation)) this.updateRotation(rotation);
134+
this.x = x;
135+
this.y = y;
136+
}
137+
respawnSprite(speedX=false, speedY=false, angle=false) {
138+
let location = Math.random()*Math.PI*2;
139+
if (angle) location = angle;
140+
let x = (Game.radius-10)*Math.cos(location)+Game.width/2;
141+
let y = (Game.radius-10)*Math.sin(location)+Game.height/2;
142+
let rotation = Math.random()*Math.PI*2;
143+
this.resetSprite(x, y, rotation, speedX, speedY);
114144
}
115145
}
116146

@@ -163,12 +193,21 @@ class Ship extends BaseSprite {
163193
return retval;
164194
}
165195

196+
get corners() {
197+
let lt = this.rotateVector([this.left, this.top]);
198+
let rt = this.rotateVector([this.right, this.top]);
199+
let lb = this.rotateVector([this.left, this.bottom]);
200+
let rb = this.rotateVector([this.right, this.bottom]);
201+
let retval = [lt, rt, lb, rb].map(value => [value[0]+this.x, value[1]+this.y])
202+
return retval;
203+
}
204+
166205
draw() {
167206
// draw ship
168207
this.showShape.forEach(value => drawArray(value
169208
.map(vector => [vector[0]*this.size+this.x, vector[1]*this.size+this.y])));
170209
// draw thrusters fire
171-
if (this.thrusters) {
210+
if (this.thrusters && !this.dead) {
172211
let fireLength = Math.random()*FIRE_LENGTH*this.size;
173212
let fireArray = [this.rear, [this.rear[0]-fireLength*Math.cos(this.rotation), this.rear[1]-fireLength*Math.sin(this.rotation)]];
174213
drawArray(fireArray);
@@ -177,6 +216,7 @@ class Ship extends BaseSprite {
177216
this.shots.forEach(shot => shot.draw());
178217
}
179218
update() {
219+
if (this.dead) return;
180220
super.update();
181221
this.thrusters = false;
182222
if (Key.isDown(this.keyUp)) {
@@ -216,31 +256,40 @@ class Ship extends BaseSprite {
216256
// black hole collision
217257
let playerToBlackhole = Math.sqrt(Math.pow(this.x - gameScreen.blackhole.x, 2)+
218258
Math.pow(this.y - gameScreen.blackhole.y, 2));
219-
if (playerToBlackhole <= BLACKHOLE_SIZE) gameScreen.respawnPlayer(this, 0, 0);
259+
if (playerToBlackhole <= BLACKHOLE_SIZE) this.respawnSprite(0, 0);
220260

221-
// border collision
222-
let playerToBorder = Game.radius - Math.sqrt(Math.pow(this.x -
223-
Game.width/2,2) + Math.pow(this.y - Game.height/2, 2));
224-
if (playerToBorder <= 0) {
225-
let angle = Math.atan2(this.y-Game.height/2, this.x-Game.width/2)+Math.PI;
226-
let x = (Game.radius-10)*Math.cos(angle)+Game.width/2;
227-
let y = (Game.radius-10)*Math.sin(angle)+Game.height/2;
228-
this.resetPlayer(x, y);
229-
}
230261
}
231262
updateRotation(angle) {
232-
if (!isNaN(parseFloat(angle)) && isFinite(angle)) this.rotation = angle;
263+
if (checkNumber(angle)) this.rotation = angle;
233264
this.showShape = this.shape
234265
.map(value0 => value0
235266
.map(value1 => this.rotateVector(value1)
236267
));
237268
}
238-
resetPlayer(x, y, rotation=false, speedX=false, speedY=false) {
239-
if (!isNaN(parseFloat(speedX)) && isFinite(speedX)) this.speedX = speedX;
240-
if (!isNaN(parseFloat(speedY)) && isFinite(speedY)) this.speedY = speedY;
241-
if (!isNaN(parseFloat(rotation)) && isFinite(rotation)) this.updateRotation(rotation);
242-
this.x = x;
243-
this.y = y;
269+
explode() {
270+
if (this.dead) return;
271+
this.dead = true;
272+
let spriteRadius = Math.max(...[this.top, this.bottom, this.left, this.right].map((val) => Math.abs(val)))
273+
let blast0 = this.fillExplosion(spriteRadius, BLAST_SIZE);
274+
let blast1 = this.fillExplosion(spriteRadius*2, BLAST_SIZE);
275+
let blast2 = this.fillExplosion(spriteRadius*5, BLAST_SIZE);
276+
let blast3 = this.fillExplosion(spriteRadius, BLAST_SIZE);
277+
let empty = []
278+
this.showShape = blast0;
279+
setTimeout(()=> this.showShape = blast1, 60);
280+
setTimeout(()=> this.showShape = blast2, 120);
281+
setTimeout(()=> this.showShape = blast3, 180);
282+
setTimeout(()=> this.showShape = empty, 240);
283+
}
284+
fillExplosion(radius, debris) {
285+
let array = []
286+
while (array.length < debris) {
287+
let theta = Math.random()*2*Math.PI;
288+
let r = Math.random()*radius;
289+
let [x, y] = [r*Math.cos(theta), r*Math.sin(theta)]
290+
array.push([[x, y], [x+1, y+1]])
291+
}
292+
return array;
244293
}
245294
}
246295

Diff for: gameScreen.js

+50-20
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,11 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
2020
"use strict";
2121

2222
const STARS = 40;
23-
const GRAVITY = 50;
23+
const GRAVITY = 20;
2424

25-
const p1Spawn = [100, 100];
26-
const p2Spawn = [500, 500];
25+
const p1Spawn = [150, 150];
26+
// const p1Spawn = [450, 410];
27+
const p2Spawn = [450, 450];
2728
const player1Keys = {
2829
keyUp: 87,
2930
keyDown: 83,
@@ -43,23 +44,23 @@ const player1Vectors = [
4344
];
4445
const player2Vectors = [
4546
[[8, 0], [1, 2], [-8, 2], [-8, -2], [1, -2], [8, 0]],
46-
[[-1, 2], [-6, 4], [-8, 4], [-8, 2]],
47-
[[-1, -2], [-6, -4], [-8, -4], [-8, -2]]
47+
[[-1, 2], [-6, 4], [-8, 4], [-8, 2]],
48+
[[-1, -2], [-6, -4], [-8, -4], [-8, -2]],
49+
[[8, 0], [-8, 0]]
4850
];
4951

5052
gameScreen.init = function () {
5153
// Setup background
5254
gameScreen.stars = []
5355
let [xc, yc] = [Game.width/2, Game.height/2]
5456
while (gameScreen.stars.length < STARS) {
55-
let [xp, yp] = [Math.random()*Game.width, Math.random()*Game.height]
56-
if (Math.sqrt(Math.pow(xp-xc, 2)+Math.pow(yp-yc, 2))<Game.radius) {
57-
gameScreen.stars.push([xp, yp]);
58-
}
57+
let theta = Math.random()*2*Math.PI;
58+
let r = Math.random()*Game.radius;
59+
gameScreen.stars.push([r*Math.cos(theta)+Game.width/2, r*Math.sin(theta)+Game.height/2]);
5960
}
6061
// Create players
61-
gameScreen.player1 = new Ship(...p1Spawn, player1Keys, player1Vectors, 2);
62-
gameScreen.player2 = new Ship(...p2Spawn, player2Keys, player2Vectors, 2);
62+
gameScreen.player1 = new Ship(...p1Spawn, player1Keys, player1Vectors, 1.5);
63+
gameScreen.player2 = new Ship(...p2Spawn, player2Keys, player2Vectors, 1.5);
6364
gameScreen.player1.updateRotation(Math.PI/4);
6465
gameScreen.player2.updateRotation(-3*Math.PI/4);
6566
gameScreen.blackhole = new Blackhole(Game.width/2, Game.height/2)
@@ -82,15 +83,44 @@ gameScreen.update = function () {
8283
gameScreen.player1.update();
8384
gameScreen.player2.update();
8485
gameScreen.blackhole.update();
85-
addGravity(gameScreen.player1, Game.width/2, Game.height/2, GRAVITY);
86-
addGravity(gameScreen.player2, Game.width/2, Game.height/2, GRAVITY);
86+
// addGravity(gameScreen.player1, Game.width/2, Game.height/2, GRAVITY);
87+
// addGravity(gameScreen.player2, Game.width/2, Game.height/2, GRAVITY);
88+
89+
//check for players collision
90+
if (gameScreen.checkCollision(gameScreen.player1, gameScreen.player2)) {
91+
gameScreen.player1.explode();
92+
gameScreen.player2.explode();
93+
}
94+
}
95+
96+
gameScreen.rotateVector = function(vector, angle) {
97+
let x = (vector[0]*Math.cos(angle)-vector[1]*Math.sin(angle));
98+
let y = (vector[1]*Math.cos(angle)+vector[0]*Math.sin(angle));
99+
return [x, y]
87100
}
88101

89-
gameScreen.respawnPlayer = function(player, speedX=false, speedY=false, angle=false) {
90-
let location = Math.random()*Math.PI*2;
91-
if (angle) location = angle;
92-
let x = (Game.radius-10)*Math.cos(location)+Game.width/2;
93-
let y = (Game.radius-10)*Math.sin(location)+Game.height/2;
94-
let rotation = Math.random()*Math.PI*2;
95-
player.resetPlayer(x, y, rotation, speedX, speedY);
102+
gameScreen.checkCollision = function(sprite1, sprite2) {
103+
// Limits of the sprite
104+
const p1c = sprite1.corners;
105+
const p2c = sprite2.corners;
106+
// Translate sprites to make p1c[0] the origin
107+
const p1cT = sprite1.corners.map(val => [val[0]-p1c[0][0], val[1]-p1c[0][1]]);
108+
const p2cT = sprite2.corners.map(val => [val[0]-p1c[0][0], val[1]-p1c[0][1]]);
109+
// Calculate the rotation to align the p1 bounding box
110+
const angle = Math.atan2(p1cT[2][1], p1cT[2][0]);
111+
// Rotate vetcors to align
112+
const p1cTR = p1cT.map(val => gameScreen.rotateVector(val, angle));
113+
const p2cTR = p2cT.map(val => gameScreen.rotateVector(val, angle));
114+
// Calculate extreme points of the bounding boxes
115+
const p1left = Math.min(...p1cTR.map(value => value[0]))
116+
const p1right = Math.max(...p1cTR.map(value => value[0]))
117+
const p1top = Math.min(...p1cTR.map(value => value[1]))
118+
const p1bottom = Math.max(...p1cTR.map(value => value[1]))
119+
const p2left = Math.min(...p2cTR.map(value => value[0]))
120+
const p2right = Math.max(...p2cTR.map(value => value[0]))
121+
const p2top = Math.min(...p2cTR.map(value => value[1]))
122+
const p2bottom = Math.max(...p2cTR.map(value => value[1]))
123+
// Check if shadows overlap in both axes
124+
if (p2left < p1right && p1left < p2right && p2top < p1bottom && p1top < p2bottom) return true;
125+
return false;
96126
}

Diff for: report-assets/explosion.gif

132 KB
Loading

0 commit comments

Comments
 (0)