Saturday, June 24, 2017

3D Breakout in ~120 lines

Controls are WASD; game appears after the jump!
<script src="https://ajax.googleapis.com/ajax/libs/threejs/r76/three.min.js"></script>
<script class="myscript">
var overlay, paddle, ball, camera, scene, shadow, colliders, text;
var blockheight = 20, blockdim = 2, speed = 5, dball;
var min = new THREE.Vector3(-90, -50, -90);
var max = new THREE.Vector3(90, 270, 90);
var camstart = new THREE.Vector3(0, 110, max.z+500);

var keys = {};
[onkeydown, onkeyup] = [e => keys[e.key] = 1, e => delete keys[e.key]];
var time = 0, loop = attractloop, rdr;
var render = () =>
 (time++, loop(), rdr.render(scene, camera), requestAnimationFrame(render));

document.body.onload = () => {
 rdr = new THREE.WebGLRenderer({antialias: true, canvas: cvs});
 camera = new THREE.PerspectiveCamera(45, 1, 0.1, 1000);
 camera.position.copy(camstart);
 resetscene();
 render();
};

function resetscene() {
 [colliders, scene] = [{}, new THREE.Scene()];
 scene.add(
  paddle = new THREE.Mesh(new THREE.BoxGeometry(75, 10, 75),
   new THREE.MeshNormalMaterial()),
  ball = new THREE.Mesh(new THREE.SphereGeometry(10),
   new THREE.MeshBasicMaterial({color: 0xee0000})),
  shadow = new THREE.Mesh(new THREE.CircleGeometry(8),
   new THREE.MeshBasicMaterial({side: THREE.DoubleSide, color: 0x444444})),
  overlay = new THREE.Sprite(new THREE.SpriteMaterial({
   map: new THREE.CanvasTexture(overlaycvs),
   depthTest: false, depthWrite: false})));
 paddle.position.y = min.y;
 colliders[paddle.uuid] = paddle;
 ball.position.copy(min).lerp(max, 0.5);
 shadow.rotation.x = Math.PI/2;
 shadow.position.set(ball.position.x, min.y+8, ball.position.z);
 [overlay.hue, text] = [0, "click to play"];
 for (var bx = min.x; bx <= max.x; bx+=(max.x - min.x)/blockdim)
 for (var by = max.y; by >= max.y-blockdim*(blockheight+1); by-=(blockheight+1))
 for (var bz = min.z; bz <= max.z; bz+=(max.z - min.z)/blockdim) {
  var block = new THREE.Mesh(new THREE.BoxGeometry(
    (max.x - min.x)/blockdim-1, blockheight, (max.z - min.z)/blockdim-1),
   new THREE.MeshNormalMaterial());
  block.position.set(bx, by, bz);
  scene.add(block);
  colliders[block.uuid] = block;
 }
 dball = new THREE.Vector3(Math.random()*3-1.5, -2, Math.random()*3-1.5);
}

function attractloop() {
 overlaycvs.width = 512; // easiset way to clear a canvas
 Object.assign(overlaycvs.getContext("2d"), {
  fillStyle: "hsl("+(overlay.hue++%360)+",100%,50%)",
  font: "italic small-caps 45px Georgia", textAlign: "center"});
 overlaycvs.getContext("2d").fillText(text, 256, 310);
 overlay.material.map.needsUpdate = true;
 overlay.position.set(0, camera.position.y+Math.sin(time/40)*5, 0);
 var scale = (2 + Math.cos(time/10)/40) * Math.tan(camera.fov * Math.PI/360)
  * (overlay.position.distanceTo(camera.position));
 overlay.scale.set(scale, scale, 2);
 spincam();
 cvs.onclick = () => (cvs.onclick = 0, resetscene(), loop = function() {
  time += 2*Math.abs(Math.sin(time/100));
  if (camera.position.z > camstart.z-.02)[time, loop] = [0, playloop];
  spincam()});
}

function spincam() {
 camera.position.set(Math.sin(time/100)*(camstart.z), camera.position.y,
  Math.cos(time/100)*(camstart.z));
 camera.lookAt(new THREE.Vector3(0, camstart.y, 0));
}

function playloop() {
 with(paddle) {
  rotation.set(0,0,0);
  Object.keys(keys).forEach(k => (({
   w: () => {position.z -=speed; rotation.x -=.1},
   a: () => {position.x -=speed; rotation.z +=.1},
   s: () => {position.z +=speed; rotation.x +=.1},
   d: () => {position.x +=speed; rotation.z -=.1}})[k] || (_=>_))());
  position.clamp(min, max);
 }
 ball.position.add(dball);
 var old = ball.position.clone();
 var hitnormal, hitbox, hitdist = 150;
 Object.values(colliders).forEach(box =>
  box.geometry.faces.forEach(f => {
   var dist = (new THREE.Triangle())
    .setFromPointsAndIndices(["a","b","c"].map(
     i => box.localToWorld(box.geometry.vertices[f[i]].clone())), 0, 1, 2)
    .closestPointToPoint(ball.position)
    .distanceToSquared(ball.position);
   if (dist > hitdist || f.normal.dot(dball) >= 0) return;
   hitnormal = f.normal.clone().applyEuler(box.rotation);
   [hitbox, hitdist] = [box, dist];
 }));
 if (hitnormal) {
  dball.reflect(hitnormal);
  if (hitbox.uuid != paddle.uuid) {
   delete colliders[hitbox.uuid];
   scene.remove(hitbox);
 }}
 ball.position.x = THREE.Math.clamp(ball.position.x, min.x, max.x);
 ball.position.z = THREE.Math.clamp(ball.position.z, min.z, max.z);
 ["x", "z"].forEach(d => old[d] != ball.position[d]? dball[d] *= -1 : 0);
 shadow.position.set(ball.position.x, min.y+8, ball.position.z);
 dball.y = dball.y + .001*(dball.y > 0? 1 : -1);
 text = ball.position.y < min.y - 50? "try again?" :
  ball.position.y > max.y + 50? "you win!" : 0;
 if (!text) return;
 scene.remove(shadow);
 [time, loop] = [0, attractloop];
}
</script>
<canvas id="cvs" width="512" height="512"></canvas>
<canvas id="overlaycvs" width="512" height="512" style="display:none"></canvas>

No comments:

Post a Comment