p2s
Version:
A JavaScript 2D physics engine.
539 lines (468 loc) • 17.1 kB
HTML
<html lang="en">
<head>
<title>p2.js Asteroids</title>
<meta charset="utf-8">
<script src="../../build/p2.js"></script>
<style>
body {
background-color: black;
margin:0;
padding:0;
overflow: hidden;
color:white;
font-family:"Courier New", Courier, monospace;
font-size: 24px;
}
a {
color:white;
text-decoration: none;
font-weight: bold;
}
canvas {
width:100%;
height:100%;
}
.textBox {
margin:10px;
display: inline-block;
}
.textBox.centered {
width:500px;
height:100px;
margin-left: -250px;
margin-top: -50px;
position: absolute;
top:50%;
left:50%;
vertical-align: middle;
text-align: center;
}
.textBox.bottomRight {
position:absolute;
right:0;
bottom:0;
}
.textBox.bottomLeft {
position:absolute;
left:0;
bottom:0;
}
.hidden {
display: none;
}
</style>
</head>
<body>
<div id="logo" class="textBox bottomRight">POWERED BY <a href="https://github.com/schteppe/p2.js">P2.JS</a> PHYSICS</div>
<div id="logo" class="textBox bottomLeft">GAME BY <a href="https://twitter.com/schteppe">@SCHTEPPE</a></div>
<div id="level" class="textBox"></div>
<div id="lives" class="textBox"></div>
<div id="gameover" class="textBox centered hidden">GAME OVER</div>
<div id="instructions" class="textBox centered">ARROW KEYS = CONTROL SHIP<br/>SPACE = SHOOT</div>
<script>
var canvas, ctx, w, h, zoom,
shipSize = 0.3, spaceWidth = 16, spaceHeight = 9, hideShip = false, allowShipCollision = true,
world, shipShape, shipBody, shipReloadTime = 0.1, shipTurnSpeed = 4,
bulletBodies = [], bulletShape, bulletRadius = 0.03, bulletLifeTime = 2,
asteroidShapes = [], numAsteroidLevels = 4, asteroidRadius = 0.9, maxAsteroidSpeed = 2,asteroids = [], numAsteroidVerts = 10,
SHIP = Math.pow(2,1),
BULLET = Math.pow(2,2),
ASTEROID = Math.pow(2,3),
initSpace = asteroidRadius * 2,
level = 1,
lives = 3,
lastShootTime = 0,
removeBodies = [],
addBodies = [];
var keyLeft = 0, keyRight = 0, keyUp = 0, keyShoot = 0;
init();
requestAnimationFrame(animate);
function init(){
// Init canvas
canvas = document.createElement("canvas");
canvas.width = window.innerWidth * window.devicePixelRatio;
canvas.height = window.innerHeight * window.devicePixelRatio;
document.body.insertBefore(canvas,document.getElementById("logo"));
w = canvas.width;
h = canvas.height;
zoom = h / spaceHeight;
if(w / spaceWidth < zoom) zoom = w / spaceWidth;
ctx = canvas.getContext("2d");
ctx.lineWidth = 2 / zoom;
ctx.strokeStyle = ctx.fillStyle = 'white';
// Init physics world
world = new p2.World({
gravity : [0, 0]
});
// Turn off friction, we don't need it.
world.defaultContactMaterial.friction = 0;
// Add ship physics
shipShape = new p2.Circle({
radius: shipSize,
collisionGroup: SHIP, // Belongs to the SHIP group
collisionMask: ASTEROID // Only collide with the ASTEROID group
});
shipBody = new p2.Body({
mass: 1,
damping: 0,
angularDamping: 0
});
shipBody.addShape(shipShape);
world.addBody(shipBody);
world.on('postStep', function(){
// Thrust: add some force in the ship direction
shipBody.applyForceLocal([0, keyUp * 2]);
// Set turn velocity of ship
shipBody.angularVelocity = (keyLeft - keyRight) * shipTurnSpeed;
});
// Init asteroid shapes
addAsteroids();
// Update the text boxes
updateLevel();
updateLives();
}
// Animation loop
function animate(time){
requestAnimationFrame(animate);
updatePhysics(time);
render();
}
var lastTime;
var maxSubSteps = 5; // Max physics ticks per render frame
var fixedDeltaTime = 1 / 30; // Physics "tick" delta time
function updatePhysics(time){
allowShipCollision = true;
if(keyShoot && !hideShip && world.time - lastShootTime > shipReloadTime){
shoot();
}
for(var i=0; i < bulletBodies.length; i++){
var b=bulletBodies[i];
// If the bullet is old, delete it
if(b.dieTime <= world.time){
bulletBodies.splice(i,1);
world.removeBody(b);
i--;
continue;
}
}
// Remove bodies scheduled to be removed
for (var i = 0; i < removeBodies.length; i++) {
world.removeBody(removeBodies[i]);
}
removeBodies.length = 0;
// Add bodies scheduled to be added
for (var i = 0; i < addBodies.length; i++) {
world.addBody(addBodies[i]);
}
addBodies.length = 0;
// Warp all bodies
for(var i=0; i < world.bodies.length; i++){
warp(world.bodies[i]);
}
// Get the elapsed time since last frame, in seconds
var deltaTime = lastTime ? (time - lastTime) / 1000 : 0;
lastTime = time;
// Make sure the time delta is not too big (can happen if user switches browser tab)
deltaTime = Math.min(1 / 10, deltaTime);
// Move physics bodies forward in time
world.step(fixedDeltaTime, deltaTime, maxSubSteps);
}
function shoot(){
var angle = shipBody.angle + Math.PI / 2;
// Create a bullet body
var bulletBody = new p2.Body({
mass: 0.05,
position: [
shipShape.radius * Math.cos(angle) + shipBody.position[0],
shipShape.radius * Math.sin(angle) + shipBody.position[1]
],
damping: 0,
velocity: [ // initial velocity in ship direction
2 * Math.cos(angle) + shipBody.velocity[0],
2 * Math.sin(angle) + shipBody.velocity[1]
],
});
// Create bullet shape
bulletShape = new p2.Circle({
radius: bulletRadius,
collisionGroup: BULLET, // Belongs to the BULLET group
collisionMask: ASTEROID // Can only collide with the ASTEROID group
});
bulletBody.addShape(bulletShape);
bulletBodies.push(bulletBody);
world.addBody(bulletBody);
// Keep track of the last time we shot
lastShootTime = world.time;
// Remember when we should delete this bullet
bulletBody.dieTime = world.time + bulletLifeTime;
}
// If the body is out of space bounds, warp it to the other side
function warp(body){
var p = body.position;
if(p[0] > spaceWidth /2) p[0] = -spaceWidth/2;
if(p[1] > spaceHeight/2) p[1] = -spaceHeight/2;
if(p[0] < -spaceWidth /2) p[0] = spaceWidth/2;
if(p[1] < -spaceHeight/2) p[1] = spaceHeight/2;
// Set the previous position too, to not mess up the p2 body interpolation
body.previousPosition[0] = p[0];
body.previousPosition[1] = p[1];
}
function render(){
// Clear the canvas
ctx.clearRect(0,0,w,h);
// Transform the canvas
// Note that we need to flip the y axis since Canvas pixel coordinates
// goes from top to bottom, while physics does the opposite.
ctx.save();
ctx.translate(w/2, h/2); // Translate to the center
ctx.scale(zoom, -zoom); // Zoom in and flip y axis
// Draw all things
drawShip();
drawBullets();
drawBounds();
drawAsteroids();
// Restore transform
ctx.restore();
}
function drawShip(){
if(!hideShip){
var x = shipBody.interpolatedPosition[0],
y = shipBody.interpolatedPosition[1],
radius = shipShape.radius;
ctx.save();
ctx.translate(x,y); // Translate to the ship center
ctx.rotate(shipBody.interpolatedAngle); // Rotate to ship orientation
ctx.beginPath();
ctx.moveTo(-radius*0.6,-radius);
ctx.lineTo(0,radius);
ctx.lineTo( radius*0.6,-radius);
ctx.moveTo(-radius*0.5, -radius*0.5);
ctx.lineTo( radius*0.5, -radius*0.5);
ctx.closePath();
ctx.stroke();
ctx.restore();
}
}
function drawAsteroids(){
for(var i=0; i < asteroids.length; i++){
var a = asteroids[i],
x = a.interpolatedPosition[0],
y = a.interpolatedPosition[1],
radius = a.shapes[0].radius;
ctx.save();
ctx.translate(x,y); // Translate to the center
ctx.rotate(a.interpolatedAngle);
ctx.beginPath();
for(var j=0; j < numAsteroidVerts; j++){
var xv = a.verts[j][0],
yv = a.verts[j][1];
if(j==0) ctx.moveTo(xv,yv);
else ctx.lineTo(xv,yv);
}
ctx.closePath();
ctx.stroke();
ctx.restore();
}
}
function drawBullets(){
for(var i=0; i < bulletBodies.length; i++){
var bulletBody = bulletBodies[i],
x = bulletBody.interpolatedPosition[0],
y = bulletBody.interpolatedPosition[1];
ctx.beginPath();
ctx.arc(x,y,bulletRadius,0,2*Math.PI);
ctx.fill();
ctx.closePath();
}
}
function drawBounds(){
ctx.beginPath();
ctx.moveTo(-spaceWidth/2, -spaceHeight/2);
ctx.lineTo(-spaceWidth/2, spaceHeight/2);
ctx.lineTo( spaceWidth/2, spaceHeight/2);
ctx.lineTo( spaceWidth/2, -spaceHeight/2);
ctx.lineTo(-spaceWidth/2, -spaceHeight/2);
ctx.closePath();
ctx.stroke();
}
function updateLevel(){
var el = document.getElementById("level");
el.innerHTML = "Level "+level;
}
function updateLives(){
var el = document.getElementById("lives");
el.innerHTML = "Lives "+lives;
}
// Returns a random number between -0.5 and 0.5
function rand(){
return Math.random()-0.5;
}
// Adds some asteroids to the scene.
function addAsteroids(){
for(var i=0; i<level; i++){
var x = rand() * spaceWidth,
y = rand() * spaceHeight,
vx = rand() * maxAsteroidSpeed,
vy = rand() * maxAsteroidSpeed,
va = rand() * maxAsteroidSpeed;
// Aviod the ship!
if(Math.abs(x-shipBody.position[0]) < initSpace){
if(y-shipBody.position[1] > 0){
y += initSpace;
} else {
y -= initSpace;
}
}
// Create asteroid body
var asteroidBody = new p2.Body({
mass:10,
position:[x,y],
velocity:[vx,vy],
angularVelocity : va,
damping: 0,
angularDamping: 0
});
asteroidBody.addShape(createAsteroidShape(0));
asteroids.push(asteroidBody);
addBodies.push(asteroidBody);
asteroidBody.level = 1;
addAsteroidVerts(asteroidBody);
}
}
function createAsteroidShape(level){
var shape = new p2.Circle({
radius: asteroidRadius * (numAsteroidLevels - level) / numAsteroidLevels,
collisionGroup: ASTEROID, // Belongs to the ASTEROID group
collisionMask: BULLET | SHIP // Can collide with the BULLET or SHIP group
});
return shape;
}
// Adds random .verts to an asteroid body
function addAsteroidVerts(asteroidBody){
asteroidBody.verts = [];
var radius = asteroidBody.shapes[0].radius;
for(var j=0; j < numAsteroidVerts; j++){
var angle = j*2*Math.PI / numAsteroidVerts,
xv = radius*Math.cos(angle) + rand()*radius*0.4,
yv = radius*Math.sin(angle) + rand()*radius*0.4;
asteroidBody.verts.push([xv,yv]);
}
}
// Catch key down events
window.onkeydown = function(evt) {
handleKey(evt.keyCode,1);
}
// Catch key up events
window.onkeyup = function(evt) {
handleKey(evt.keyCode,0);
}
// Handle key up or down
function handleKey(code,isDown){
switch(code){
case 32: keyShoot = isDown; break;
case 37: keyLeft = isDown; break;
case 38:
keyUp = isDown;
document.getElementById("instructions").classList.add("hidden");
break;
case 39: keyRight = isDown; break;
}
}
// Catch impacts in the world
// Todo: check if several bullets hit the same asteroid in the same time step
world.on("beginContact",function(evt){
var bodyA = evt.bodyA,
bodyB = evt.bodyB;
if(!hideShip && allowShipCollision && (bodyA === shipBody || bodyB === shipBody)){
// Ship collided with something
allowShipCollision = false;
var otherBody = (bodyA === shipBody ? bodyB : bodyA);
if(asteroids.indexOf(otherBody) !== -1){
lives--;
updateLives();
// Remove the ship body for a while
removeBodies.push(shipBody);
hideShip = true;
if(lives > 0){
var interval = setInterval(function(){
// Check if the ship position is free from asteroids
var free = true;
for(var i=0; i<asteroids.length; i++){
var a = asteroids[i];
if(Math.pow(a.position[0]-shipBody.position[0],2) + Math.pow(a.position[1]-shipBody.position[1],2) < initSpace){
free = false;
}
}
if(free){
// Add ship again
shipBody.force[0] =
shipBody.force[1] =
shipBody.velocity[0] =
shipBody.velocity[1] =
shipBody.angularVelocity =
shipBody.angle = 0;
hideShip = false;
world.addBody(shipBody);
clearInterval(interval);
}
},100);
} else {
document.getElementById('gameover').classList.remove('hidden');
}
}
} else if(bulletBodies.indexOf(bodyA) !== -1 || bulletBodies.indexOf(bodyB) !== -1){
// Bullet collided with something
var bulletBody = (bulletBodies.indexOf(bodyA) !== -1 ? bodyA : bodyB),
otherBody = (bodyB === bulletBody ? bodyA : bodyB);
if(asteroids.indexOf(otherBody) !== -1){
explode(otherBody,bulletBody);
}
}
});
function explode(asteroidBody,bulletBody){
var aidx = asteroids.indexOf(asteroidBody);
var idx = bulletBodies.indexOf(bulletBody);
if(aidx != -1 && idx != -1){
// Remove asteroid
removeBodies.push(asteroidBody);
asteroids.splice(aidx,1);
// Remove bullet
removeBodies.push(bulletBody);
bulletBodies.splice(idx,1);
// Add new sub-asteroids
var x = asteroidBody.position[0],
y = asteroidBody.position[1];
if(asteroidBody.level < 4){
var angleDisturb = Math.PI/2 * Math.random();
for(var i=0; i<4; i++){
var angle = Math.PI/2 * i + angleDisturb;
var shape = createAsteroidShape(asteroidBody.level);
var r = asteroidBody.shapes[0].radius - shape.radius;
var subAsteroidBody = new p2.Body({
mass: 10,
position: [
x + r * Math.cos(angle),
y + r * Math.sin(angle)
],
velocity: [rand(),rand()],
damping: 0,
angularDamping: 0
});
subAsteroidBody.addShape(shape);
subAsteroidBody.level = asteroidBody.level + 1;
addBodies.push(subAsteroidBody);
addAsteroidVerts(subAsteroidBody);
asteroids.push(subAsteroidBody);
}
}
}
if(asteroids.length == 0){
level++;
updateLevel();
addAsteroids();
}
}
</script>
</body>
</html>