Categories
Uncategorised

js13kgame competition 2023 diary

I’m entering the js13kgames competition this year. Here’s my diary.

Here’s the code on GitHub.
And here’s the game.

I’m semi-retired having worked with real-time 3d for nearly 30 years. I create video courses mainly teaching game programming.

js13kgames post-mortem page

13th August

The theme of this years competition is announced. 13th century. I gave it some thought and decided on a quest for the Holy Grail. I’m doing a WebXR game using ThreeJS. I’ve already created a working project framework using npm and webpack with help from Matt McKenna.

With a 13k size limit Blender models are a no-no. All assets need to be created in code. I fiddled with the ThreeJS Editor and came up with this as the player character.

Sir Coadalot in the ThreeJS Editor

Downloading this as a JSON file is 12K uncompressed. Let’s remake it in code.

14th August

Big day. Before the competition theme was announced I’d been working on the key components I thought my game would need. A VRButton. I’d already created one for my WebXR course. But I made a tweak to it for displaying the VR Cardboard icon from an svg string.

vr-cardboard.svg icon

I had created the most basic 3D physics engine. If you look in the source you’ll find it in the src>SimplePhysics folder. Just 3 files,

SPWorld.jsThis is where rigid bodies are added and collisions calculated
SPBody.jsA single rigid body
SPCollider.jsA SPBody instance has a single collider which can only be a Sphere or a AABB ( Axis Aligned Bounding Box )
SimplePhysics demo

Minified and zipped it comes in under 2K.

If you want to see it in action and you’ve downloaded the repo then rename index.js as index-game.js and rename index-sp.js as index.js. If you’ve got the game running that’s npm run start then you can see it in a browser using localhost:8080. The physics isn’t perfect to say the least but needs must when the entire game budget is only 13k.

My first step in creating the game was to change a sphere into my player character. The downloaded json file from the ThreeJS editor game the necessary geometries, material colours and mesh positions and orientations. Here’s the code to create the knight.

createModel(){
  const gSkirt = new THREE.CylinderGeometry(0.4, 0.6, 0.5, 32, 1, true );
  const gHead = new THREE.SphereGeometry(0.4, 24, 10);
  const pHelmet = [
    new THREE.Vector2(0.5, 0),
    new THREE.Vector2(0.5, 0.2),
    new THREE.Vector2(0.45, 0.2),
    new THREE.Vector2(0.4, 0.3),
    new THREE.Vector2(0.3, 0.4),
    new THREE.Vector2(0, 0.5),
  ];
  const gHelmet = new THREE.LatheGeometry(pHelmet, 12);
  const pTunic = [
    new THREE.Vector2(0.45, 0),
    new THREE.Vector2(0.43, 0.1),
    new THREE.Vector2(0.4, 0.2),
    new THREE.Vector2(0.32, 0.3),
    new THREE.Vector2(0.16, 0.4),
    new THREE.Vector2(0.05, 0.5),
  ];
  const gTunic = new THREE.LatheGeometry(pTunic, 12);
  const gBelt = new THREE.CylinderGeometry(0.45, 0.45, 0.2, 32, 1, false);

  const mSkirt = new THREE.MeshStandardMaterial( { color: 15991041 } );
  const mHead = new THREE.MeshStandardMaterial( { color: 16373422 } );
  const mHelmet = new THREE.MeshStandardMaterial( { color: 0xC7C7C7 } );
  const mTunic = new THREE.MeshStandardMaterial( { color: 16777215 } );
  const mBelt = new THREE.MeshStandardMaterial( { color: 12615993 } );

  const root = new THREE.Group();
  const skirt = new THREE.Mesh( gSkirt, mSkirt );  
  skirt.matrix.fromArray(
    [1,0,0,0,0,1,0,0,0,0,1,0,0,0.25,0,1]
  );
  root.add(skirt);
  const head = new THREE.Mesh( gHead, mHead ); 
  head.matrix.fromArray(
   [1,0,0,0,0,1,0,0,0,0,1,0,0,1.3466628932086855,0,1]
  );
  root.add(head);
  const helmet = new THREE.Mesh( gHelmet, mHelmet );
  helmet.matrix.fromArray(
   [1,0,0,0,0,1,0,0,0,0,1,0,0,1.4010108612494776,0,1]
  );
  root.add(helmet);
  const tunic = new THREE.Mesh( gTunic, mTunic );
  tunic.matrix.fromArray(
    [1,0,0,0,0,1,0,0,0,0,1,0,0,0.6106004423389476,0,1]);
  root.add(tunic);
  const belt = new THREE.Mesh( gBelt, mBelt );
  belt.matrix.fromArray(
    [1.2,0,0,0,0,1,0,0,0,0,1,0,-0.04,
     0.5495005511829094,0,1]
  );
  root.add(belt);

  root.traverse( object => {
    if ( object.matrixAutoUpdate ){
      object.matrix.decompose( object.position, object.quaternion, object.scale );
     }
   });

  return root;
}
Sir Coadalot

I also created a castle tower in code. I added my JoyStick control for testing on the desktop. Put it all together and had this – not bad for day 1

August 15th

I worked on animations for the player character today. Given the tight 13k budget. Using a 3D content creator like Blender and exporting as a GLB is a none starter. So I used the ThreeJS Editor, carefully moving and rotating the sword root object into various poses then writing down its position and rotation.

Inspector panel in the ThreeJS Editor

Having got a set of keyframes. I created a JS object.

const config1 = {
  duration: 0.4,
  times: [0, 0.1, 0.3],
  pos:[{ x:0, y:0, z:0 }, { x:-0.261, y:0.522, z:0.201 }, { x:-0.293, y:0.722, z:0.861 }],
  rot:[{ x:0, y:0, z:0 }, { x:21.69, y:13.79, z:-9.18 }, { x:-2.23, y:4.21, z:175.94 }]
}

And a function to convert this into a ThreeJS AnimationClip

createAnim(name, config){
  const pvalues = [], qvalues = [];
  const v = new THREE.Vector3(), q = new THREE.Quaternion(), e = new THREE.Euler();
  const d2r = Math.PI/180;

  for(let i=0; i<config.times.length; i++){
    const pos = config.pos[i];
    const rot = config.rot[i];
    v.set(pos.x, pos.y, pos.z).toArray( pvalues, pvalues.length );
    e.set(rot.x*d2r, rot.y*d2r, rot.z*d2r);
    q.setFromEuler(e).toArray( qvalues, qvalues.length );
  }

  const pos = new THREE.VectorKeyframeTrack( '.position', config.times, pvalues );
  const rot = new THREE.QuaternionKeyframeTrack( '.quaternion', config.times, qvalues );

  return new THREE.AnimationClip( name, config.duration, [ pos, rot ] );
}

I used a little test code to see it in action.

Sir Coadalot and sword

Of course the player needs an enemy. Meet the Black knight. Just the same with different material colours and one point on the helmet LatheGeometry points array changed.

August 16th

Today I coded the castle walls and towers. Added a DebugControls class to allow keyboard entry when testing using the WebXR emulator on a desktop. I also added some bad guys. Super primitive AI they just move toward the player character. The bad news is I’ve only got 1k left to complete the game. Something might have to go!!! Here’s a screengrab from my Quest2

August 17th

Today I refactored the game. Removed the BasicUI. Removed the OBJParser and the Rock OBJ String. Instead I create a rock using an IcosahedronGeometry instance then randomly perturb the vertex positions.

class Rock extends THREE.Mesh{
  constructor(radius=0.5){
    const geometry = new THREE.IcosahedronGeometry(radius, 8, 6);
    geometry.translate( 0, radius, 0 );
    const vertices = geometry.getAttribute('position');
    for(let i=0; i<vertices.array.length; i++){
      vertices.array[i] += (Math.random()-0.5) * 0.35;
    }
    vertices.needsUpdate = true;
    const material = new THREE.MeshPhongMaterial( {color: 0xaaaaaa } );
    super(geometry, material);
  }
}

I limited the scene to one tree type. This gained me 2K. I was unfeasibly happy by this. That’s what happens with this competition! And makes it fun.

I updated the castles, created Player and Enemy classes that extend the Knight class so I can create the models using the Knight class but have different behaviour for the Player and an Enemy. And I created some new props.

Props

August 18th

Today I setup patrolling for the bad guys. Just a four cornered path and the enemy moves around this path unless the player is within 10 world units. I also started work on the introduction panel and gameover panel. No way in the byte allowance I can use a custom font. That would blow the budget straightaway.

Patrolling

August 19th

Main thing today was making the sword functional. I added an Object3D to the end of the sword. In the Player update method I do a check using the physics engine to see if this object position intersects any colliders. If the ThreeJS object associated with the physics body has the name ‘Gate’ or ‘Enemy’, I call methods of the object. For Gate that is the method openGate. I have a problem though I only have 33 bytes left. I did some checking, removing the sfx increases the bytes to 330. But removing the CollisionEffect increases the remaining bytes to over 2K. All assets are nearly complete. So 2K should be enough. Looks like I need to simplify the CollisionEffect.

Opening Gate

August 20th

A week into the competition and the game is developing well. I was travelling today so didn’t do much. I created a ForceField that will be visible for 10secs after a Shield pickup. It uses an InstancedMesh. An InstancedMesh instance takes geometry and material just like a Mesh. In addition it has a third parameter, count. The count parameter is the number of duplicates of the geometry. To position and orientate each mesh you use the setMatrixAt method. Passing an index and a matrix. Here’s the update method showing how the motion of the shields is handled.

update(dt){
  this.time += dt;
        
  const PI2 = Math.PI * 2;
  const inc = PI2/ForceField.count;
  let index = 0;

  for(let row=0; row<ForceField.rows; row++){
    const n = (row % 2) ? 1 : -1;
    const y = (ForceField.height/ForceField.rows) * row;
    for(let i=0; i<ForceField.count; i++ ){
      const t = (this.time * n) % PI2;
      const r = (this.time * -1) % PI2;
      const z = Math.sin(t+i*inc) * ForceField.radius;
      const x = Math.cos(t+i*inc) * ForceField.radius;
      this.obj.position.set(x,y,z);
      this.obj.rotation.set(0,t,0);
      this.obj.updateMatrix();
      this.meshes.setMatrixAt( index ++, this.obj.matrix );
    }
  }

  this.meshes.instanceMatrix.needsUpdate = true;
}
ForceField

August 21st

Travelling again today so didn’t achieve much. Main thing was rewriting the CollisionEffect as a InstancedMesh, rather than extending the custom class GPUParticleSystem. Gained nearly 1700 bytes. Well worth it.

August 22nd-23rd

Lot’s of debugging. I now have the basis of a game. Lots of fine tuning to do. I have 384 bytes left. But a bit of tiding up might gain me enough to add some sound.

4th September

I was away for the last few days with my daughter and the grandkids. Didn’t get anything done! I did a session of debugging yesterday, added a little sound and with 23 bytes left submitted!

13K is a serious limit and restricted what I could add as gameplay. But I really enjoyed working within this restriction. Particularly happy with the physics engine. Looking forward to next years theme.

Disappointingly there was a bug on the js13kgames site which made my game unplayable on the voting site so it received no votes and came last in the WebXR category!!! The problem was a cross-origin problem meaning the Three.JS library wouldn’t load from the path provided by the organisers. Frustrating after the spending many hours creating the game. Heh-ho, nevertheless I enjoyed the challenge.

By niklever

Started coding in 1983 using Sinclair Basic. Recently developed HTML5 games and THREE.js web apps. Course developer on the Udemy and Packt platforms.

Leave a Reply

Your email address will not be published. Required fields are marked *