UNPKG

p2s

Version:

A JavaScript 2D physics engine.

539 lines (468 loc) 17.1 kB
<!DOCTYPE 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>