planck-js
Version:
2D JavaScript physics engine for cross-platform HTML5 game development
357 lines (288 loc) • 8.85 kB
JavaScript
planck.testbed('Asteroid', function(testbed) {
var pl = planck, Vec2 = pl.Vec2;
var SHIP = 2;
var BULLET = 4;
var ASTEROID = 4;
var SPACE_WIDTH = 16;
var SPACE_HEIGHT = 9;
var SHIP_SIZE = 0.30;
var FIRE_RELOAD_TIME = 100;
var BULLET_LIFE_TIME = 2000;
testbed.width = SPACE_WIDTH;
testbed.height = SPACE_HEIGHT;
testbed.step = tick;
testbed.ratio = 64;
testbed.y = 0;
var asteroidRadius = 0.9;
var asteroidSpeed = 2;
var asteroidLevels = 4;
var level;
var lives;
var gameover;
var allowCrashTime = 0;
var allowFireTime = 0;
var world = pl.World();
var asteroidBodies = [];
var bulletBodies = [];
var shipBody;
testbed.keydown = function(code, char) {
if (testbed.activeKeys.fire) {
gameover && start();
}
};
// Todo: check if several bullets hit the same asteroid in the same time step
world.on('pre-solve', function(contact) {
var fixtureA = contact.getFixtureA();
var fixtureB = contact.getFixtureB();
var bodyA = contact.getFixtureA().getBody();
var bodyB = contact.getFixtureB().getBody();
var aship = bodyA === shipBody;
var bship = bodyB === shipBody;
var abullet = fixtureA.getFilterCategoryBits() & BULLET;
var bbullet = fixtureB.getFilterCategoryBits() & BULLET;
if ((aship || bship) && allowCrashTime < globalTime) {
// Ship collided with something
var ship = aship ? bodyA : bodyB;
var asteroid = !aship ? bodyA : bodyB;
setTimeout(function () {
crash(ship, asteroid);
}, 1);
}
if (abullet || bbullet) {
// Bullet collided with something
var bullet = abullet ? bodyA : bodyB;
var asteroid = !abullet ? bodyA : bodyB;
setTimeout(function () {
hit(bullet, asteroid);
}, 1);
}
});
function start() {
gameover = false;
level = 1;
lives = 3;
uiStatus();
setupShip(true);
addAsteroids();
uiStart();
}
function end() {
gameover = true;
uiEnd();
}
function setupShip() {
shipBody = world.createBody({
type : 'dynamic',
angularDamping : 2.0,
linearDamping : 0.5,
position : Vec2(),
});
shipBody.createFixture(pl.Polygon([
Vec2(-0.15, -0.15),
Vec2(0, -0.1),
Vec2(0.15, -0.15),
Vec2(0, 0.2)
]), {
density : 1000,
filterCategoryBits : SHIP,
filterMaskBits : ASTEROID
});
allowCrashTime = globalTime + 2000;
}
var globalTime = 0;
function tick(dt) {
globalTime += dt;
if (shipBody) {
// Set velocities
if (testbed.activeKeys.left && !testbed.activeKeys.right) {
shipBody.applyAngularImpulse(0.1, true);
} else if (testbed.activeKeys.right && !testbed.activeKeys.left) {
shipBody.applyAngularImpulse(-0.1, true);
}
// Thrust: add some force in the ship direction
if (testbed.activeKeys.up) {
var f = shipBody.getWorldVector(Vec2(0.0, 1.0));
var p = shipBody.getWorldPoint(Vec2(0.0, 2.0));
shipBody.applyLinearImpulse(f, p, true);
}
// Fire
if (testbed.activeKeys.fire && globalTime > allowFireTime) {
var magnitude = 2, angle = shipBody.Getangle + Math.PI / 2;
// Create a bullet body
var bulletBody = world.createDynamicBody({
// mass : 0.05,
position: shipBody.getWorldPoint(Vec2(0, SHIP_SIZE)),
linearVelocity: shipBody.getWorldVector(Vec2(0, magnitude)),
bullet: true
});
bulletBody.createFixture(new pl.Circle(0.05), {
filterCategoryBits: BULLET,
filterMaskBits: ASTEROID
});
bulletBodies.push(bulletBody);
// Keep track of the last time we shot
allowFireTime = globalTime + FIRE_RELOAD_TIME;
// Remember when we should delete this bullet
bulletBody.dieTime = globalTime + BULLET_LIFE_TIME;
}
wrap(shipBody);
}
for (var i = 0; i !== bulletBodies.length; i++) {
var bulletBody = bulletBodies[i];
// If the bullet is old, delete it
if (bulletBody.dieTime <= globalTime) {
bulletBodies.splice(i, 1);
world.destroyBody(bulletBody);
i--;
continue;
}
wrap(bulletBody);
}
for (var i = 0; i !== asteroidBodies.length; i++) {
var asteroidBody = asteroidBodies[i];
wrap(asteroidBody);
}
}
// Adds some asteroids to the scene.
function addAsteroids() {
while (asteroidBodies.length) {
var asteroidBody = asteroidBodies.shift();
world.destroyBody(asteroidBody);
// asteroidBody.uiRemove();
}
for (var i = 0; i < level; i++) {
var shipPosition = shipBody.getPosition();
var x = shipPosition.x;
var y = shipPosition.y;
// Aviod the ship!
while (Math.abs(x - shipPosition.x) < asteroidRadius * 2
&& Math.abs(y - shipPosition.y) < asteroidRadius * 2) {
x = rand(SPACE_WIDTH);
y = rand(SPACE_HEIGHT);
}
var vx = rand(asteroidSpeed);
var vy = rand(asteroidSpeed);
var va = rand(asteroidSpeed);
// Create asteroid body
var asteroidBody = makeAsteroidBody(x, y, vx, vy, va, 0);
asteroidBody.level = 1;
}
}
function asteroidLevelRadius(level) {
return asteroidRadius * (asteroidLevels - level) / asteroidLevels;
}
function makeAsteroidBody(x, y, vx, vy, va, level) {
var asteroidBody = world.createKinematicBody({
// mass : 10,
position : Vec2(x, y),
linearVelocity : Vec2(vx, vy),
angularVelocity : va
});
asteroidBodies.push(asteroidBody);
var radius = asteroidLevelRadius(level);
var n = 8, path = [];
for (var i = 0; i < n; i++) {
var a = i * 2 * Math.PI / n;
var x = radius * (Math.sin(a) + rand(0.3));
var y = radius * (Math.cos(a) + rand(0.3));
path.push(Vec2(x, y));
}
asteroidBody.createFixture(pl.Polygon(path), {
filterCategoryBits : ASTEROID,
filterMaskBits : BULLET | SHIP
});
return asteroidBody;
}
function crash(ship, asteroid) {
if (!shipBody) return;
lives--;
uiStatus();
// Remove the ship body for a while
world.destroyBody(shipBody);
shipBody = null;
if (lives <= 0) {
end();
return;
}
setTimeout(function() {
// Add ship again
setupShip();
}, 1000);
}
function hit(asteroidBody, bulletBody) {
var aidx = asteroidBodies.indexOf(asteroidBody);
var bidx = bulletBodies.indexOf(bulletBody);
if (aidx != -1 && bidx != -1) {
// Remove asteroid
world.destroyBody(asteroidBody);
asteroidBodies.splice(aidx, 1);
// asteroidBody.uiRemove();
// Remove bullet
world.destroyBody(bulletBody);
bulletBodies.splice(bidx, 1);
// bulletBody.uiRemove();
// Add new sub-asteroids
splitAsteroid(asteroidBody);
}
if (asteroidBodies.length == 0) {
level++;
uiStatus();
addAsteroids();
}
}
function splitAsteroid(parent) {
if (parent.level < 4) {
var angleDisturb = Math.PI / 2 * Math.random();
for (var i = 0; i < 4; i++) {
var angle = Math.PI / 2 * i + angleDisturb;
var r = asteroidLevelRadius(0) - asteroidLevelRadius(parent.level);
var sp = parent.getWorldPoint(Vec2(r * Math.cos(angle), r * Math.sin(angle)));
var vx = rand(asteroidSpeed);
var vy = rand(asteroidSpeed);
var va = rand(asteroidSpeed);
var child = makeAsteroidBody(sp.x, sp.y, vx, vy, va, parent.level);
child.level = parent.level + 1;
child.setAngle(rand() * Math.PI);
}
}
}
// If the body is out of space bounds, wrap it to the other side
function wrap(body) {
var p = body.getPosition();
p.x = wrapNumber(p.x, -SPACE_WIDTH / 2, SPACE_WIDTH / 2);
p.y = wrapNumber(p.y, -SPACE_HEIGHT / 2, SPACE_HEIGHT / 2);
body.setPosition(p);
}
function wrapNumber(num, min, max) {
if (typeof min === 'undefined') {
max = 1, min = 0;
} else if (typeof max === 'undefined') {
max = min, min = 0;
}
if (max > min) {
num = (num - min) % (max - min);
return num + (num < 0 ? max : min);
} else {
num = (num - max) % (min - max);
return num + (num <= 0 ? min : max);
}
}
// Returns a random number between -0.5 and 0.5
function rand(value) {
return (Math.random() - 0.5) * (value || 1);
}
function uiStart() {
console.log('Game started');
}
function uiEnd() {
console.log('Game over');
testbed.status('Game Over!');
}
function uiStatus() {
console.log('Level: ' + level + ' Lives: ' + lives);
testbed.status('Level', level);
testbed.status('Lives', lives);
}
start();
return world;
});