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<THREE.Group> => {
  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.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

Game DevelopmentWebGLphysics 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

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.