Game Development 11 min read

Implementing a Mid‑Autumn Festival Dice Game with Three.js and cannon‑es

This tutorial explains how to create an interactive Mid‑Autumn Festival dice‑game using Three.js for 3D rendering, cannon‑es for physics simulation, glTF/glb model loading, heightfield collision handling, and JavaScript logic to determine game results.

Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Implementing a Mid‑Autumn Festival Dice Game with Three.js and cannon‑es

The article introduces the traditional Mid‑Autumn dice game and demonstrates how to recreate it as a web‑based 3D interactive experience using Three.js and the cannon‑es physics engine.

Implementation Steps

Load dice and basin 3D models.

Set up a physics engine scene.

Bind Three.js objects to cannon‑es bodies.

Implement the game rules.

Model Loading

Models are obtained from Sketchfab in glTF or glb format; glb is preferred for its smaller binary size.

Three.js Model Loader

/**
 * Load a GLTF model
 */
let loadGLTF = async (url): Promise
=> {
  return new Promise(resolve => {
    const loader = new GLTFLoader();
    loader.load(url, object => {
        resolve(object as THREE.Group);
    });
  });
};

/**
 * Initialize models
 */
let loadModel = async () => {
  let basinModel = await loadGLTF("/basin2.glb");
  let diceModel = await loadGLTF("/dice.glb");

  // dice setup
  dice = diceModel.scene.children[0];
  dice.scale.set(0.1, 0.1, 0.1);
  dice.traverse(function (child) {
    if (child.isMesh) {
      child.castShadow = true;
      child.material.metalness = 1;
      child.material.emissive = child.material.color;
      child.material.emissiveMap = child.material.map;
    }
  });

  // basin setup
  basin = basinModel.scene.children[0];
  basin.scale.set(18, 18, 18);
  basin.position.y = 0.7;
  basin.position.x = 0;
  scene.add(basin);
};

Physics Engine Simulation

The cannon‑es library provides gravity, collision, and sleep handling.

let world = new CANNON.World();
world.gravity.set(0, -9.82, 0);
world.allowSleep = true;

Dice bodies are created with a Box shape:

/**
 * Create a dice physics body
 */
const generateDiceBody = () => {
  const size = 0.1;
  const halfExtents = new CANNON.Vec3(size, size, size);
  const dice = new CANNON.Body({
    mass: 0.1,
    material: new CANNON.Material({
      friction: 0.1,
      restitution: 0.7
    }),
    shape: new CANNON.Box(halfExtents)
  });
  dice.sleepSpeedLimit = 1.0;
  world.addBody(dice);
  return dice;
};

Because a simple box cannot represent the basin, a Heightfield shape is generated from a grid of heights that form a concave bowl.

const initBasin = () => {
  const numRows = 60;
  const numCols = 60;
  let heights = [];
  for (let i = 0; i < numRows; i++) {
    const row = [];
    for (let j = 0; j < numCols; j++) {
      const x = (j / (numCols - 1) - 0.5) * 20;
      const z = (i / (numRows - 1) - 0.5) * 20;
      const radius = 4.2;
      const height = Math.sqrt(x * x + z * z) <= radius ? -1.7 : 0;
      row.push(height);
    }
    heights.push(row);
  }
  const heightfieldShape = new CANNON.Heightfield(heights, { elementSize: 0.2 });
  const heightfieldBody = new CANNON.Body({
    mass: 0,
    material: new CANNON.Material({ friction: 0.1, restitution: 0.7 })
  });
  heightfieldBody.addShape(heightfieldShape);
  heightfieldBody.position.set(-5.9, 1.3, 5.9);
  heightfieldBody.quaternion.setFromEuler(-Math.PI / 2, 0, 0);
  world.addBody(heightfieldBody);
};

Binding Three.js Objects to Physics Bodies

let dices = [];
/** Initialize dice positions */
let initDice = () => {
  for (let i = 1; i <= 6; i++) {
    let cDice = generateDiceBody();
    cDice.quaternion.setFromEuler(Math.PI / 2, 0, 0);
    let tDice = dice.clone();
    scene.add(tDice);
    dices.push({ tDice, cDice });
  }
};

// Sync positions each frame
dices.forEach(({ cDice, tDice }) => {
  if (cDice && tDice) {
    tDice.position.copy(cDice.position);
    tDice.quaternion.copy(cDice.quaternion);
  }
});

Game Rules

Dice values are derived from their rotation angles, and the result is determined by counting occurrences of each face according to traditional rules.

let getDicePoints = () => {
  let points = [];
  dices.forEach(({ tDice }) => {
    let xAngle = Math.round((tDice.rotation.x / Math.PI) * 180);
    let zAngle = Math.round((tDice.rotation.z / Math.PI) * 180);
    let point;
    if (xAngle == -90) point = 1;
    else if (xAngle == 90) point = 5;
    else if (xAngle + zAngle == -90 || xAngle + zAngle == 270) point = 4;
    else if (xAngle + zAngle == 0 || xAngle + zAngle == 360) point = 3;
    else if (xAngle + zAngle == 180 || xAngle + zAngle == -180) point = 6;
    else point = 2;
    points.push(point);
  });
  return points;
};

const getName = () => {
  let obj = {};
  result.value.forEach(index => {
    obj[index] = (obj[index] || 0) + 1;
  });
  if (obj[4] == 1) {
    return Object.keys(obj).length === 6 ? "对堂" : "一秀";
  } else if (obj[4] == 2) return "二举";
  else if (obj[4] == 3) return "三红";
  else if (obj[4] == 4) {
    return obj[2] == 2 ? "状元插金花" : "状元";
  } else if (obj[4] == 5) return "五王";
  else if (obj[4] == 6) return "六捧红";
  else if (Object.keys(obj).some(k => obj[k] == 4)) return "四进";
  else if (obj[6] == 5) return "五子登科";
  else if (obj[6] == 6) return "手捧黑";
  return "再接再厉";
};

Conclusion

The tutorial covers model acquisition and format differences, physics engine usage for both simple and complex shapes, and the specific rules of the Mid‑Autumn dice game, providing a solid foundation for building similar WebGL games.

game developmentThree.jsWebGLphysics simulationglTFcannon-es
Rare Earth Juejin Tech Community
Written by

Rare Earth Juejin Tech Community

Juejin, a tech community that helps developers grow.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.