© Bohua Xu

rollDice🎲[1.0]

Dec 8, 2024 · 3min

Simple 3D Dice Rolling

Demo

🎮 Try it online

User Guide

Initial Interface

Initial Interface

Basic Operations

  • Click any dice to start rolling

Hidden Features

  1. Cheat Mode
    • Double-click in the lower-left corner area to activate the cheat system
    • A dice control panel will appear after activation
    • Click "Reset Dice" button to stack dice neatly
Cheat Mode Panel
  1. Dice Control Panel
    • Allows individual adjustment of each die’s value
    • Provides real-time preview of modifications

Dice States

Reset StateAfter Throw

After ThrowReset State

Core Functionality Implementation

1. Physics Engine Integration

Using @react-three/cannon for realistic physics effects:

const [ref, api] = useBox(() => ({
  mass: 1,
  position,
  args: [1.5, 1.5, 1.5],
  linearDamping: 0.4,
  angularDamping: 0.3,
  material: {
    friction: 0.3,
    restitution: 0.6,
  }
}))
const [ref, api] = useBox(() => ({
  mass: 1,
  position,
  args: [1.5, 1.5, 1.5],
  linearDamping: 0.4,
  angularDamping: 0.3,
  material: {
    friction: 0.3,
    restitution: 0.6,
  }
}))
  • mass: Sets the dice mass
  • linearDamping: Controls linear motion damping
  • angularDamping: Controls rotational damping
  • friction: Friction coefficient
  • restitution: Elasticity coefficient

2. State Management

Using Zustand for dice state management:

const useDiceStore = create<DiceState>((set, get) => ({
  diceCount: 5,
  diceValues: Array(6).fill(1),
  diceRefs: new Set(),
  
  throwAllDice: () => {
    const { diceRefs } = get()
    diceRefs.forEach(api => {
      // Random throw force and direction
      const maxVelocity = 12 + Math.random() * 6
      const height = 15 + Math.random() * 8
      // ...
    })
  }
}))
const useDiceStore = create<DiceState>((set, get) => ({
  diceCount: 5,
  diceValues: Array(6).fill(1),
  diceRefs: new Set(),
  
  throwAllDice: () => {
    const { diceRefs } = get()
    diceRefs.forEach(api => {
      // Random throw force and direction
      const maxVelocity = 12 + Math.random() * 6
      const height = 15 + Math.random() * 8
      // ...
    })
  }
}))

3. Dice Material Rendering

3.1 Texture Generation

Using Canvas to dynamically generate dice face textures:

export function createDiceTexture(number: number): THREE.Texture {
  const canvas = document.createElement('canvas')
  const context = canvas.getContext('2d')
  // Draw background and dots
  const dotPositions = getDotPositions(number)
  dotPositions.forEach(([x, y]) => {
    context.arc(x, y, dotRadius, 0, Math.PI * 2)
  })
  return new THREE.CanvasTexture(canvas)
}
export function createDiceTexture(number: number): THREE.Texture {
  const canvas = document.createElement('canvas')
  const context = canvas.getContext('2d')
  // Draw background and dots
  const dotPositions = getDotPositions(number)
  dotPositions.forEach(([x, y]) => {
    context.arc(x, y, dotRadius, 0, Math.PI * 2)
  })
  return new THREE.CanvasTexture(canvas)
}

3.2 Material Application

const Dice: React.FC<DiceProps> = () => {
  const textures = useRef([
    createDiceTexture(6),
    createDiceTexture(5),
    createDiceTexture(2),
    createDiceTexture(1),
    createDiceTexture(4),
    createDiceTexture(3),
  ])

  return (
    <Box args={[1.45, 1.45, 1.45]} castShadow>
      {textures.current.map((texture, index) => (
        <meshPhysicalMaterial
          key={index}
          attach={`material-${index}`}
          map={texture}
          metalness={0.3}
          roughness={0.4}
          clearcoat={0.5}
        />
      ))}
    </Box>
  )
}
const Dice: React.FC<DiceProps> = () => {
  const textures = useRef([
    createDiceTexture(6),
    createDiceTexture(5),
    createDiceTexture(2),
    createDiceTexture(1),
    createDiceTexture(4),
    createDiceTexture(3),
  ])

  return (
    <Box args={[1.45, 1.45, 1.45]} castShadow>
      {textures.current.map((texture, index) => (
        <meshPhysicalMaterial
          key={index}
          attach={`material-${index}`}
          map={texture}
          metalness={0.3}
          roughness={0.4}
          clearcoat={0.5}
        />
      ))}
    </Box>
  )
}
  • Using Canvas to dynamically generate textures for each face
  • Applying meshPhysicalMaterial for realistic material effects
  • Caching textures with useRef to avoid recreation

4. Interaction Feedback

Combining sound and vibration for feedback:

onCollide: (event: any) => {
  if (event.body) {
    vibrate(VIBRATION_PATTERNS.DICE_HIT);
  }
}
onCollide: (event: any) => {
  if (event.body) {
    vibrate(VIBRATION_PATTERNS.DICE_HIT);
  }
}

5. Smooth Animation

Using interpolation for fluid motion:

useFrame(() => {
  if (!ref.current) return
  
  const lerpFactor = 0.1
  smoothedPosition.current[0] += (currentPosition.x - smoothedPosition.current[0]) * lerpFactor
  // ...
})
useFrame(() => {
  if (!ref.current) return
  
  const lerpFactor = 0.1
  smoothedPosition.current[0] += (currentPosition.x - smoothedPosition.current[0]) * lerpFactor
  // ...
})

Key Technical Points

Collision Detection

useEffect(() => {
  const currentPosition = [0, 0, 0]
  const unsubscribe = api.position.subscribe((v: [number, number, number]) => {
    // Detect boundary collisions
    const maxBound = 9
    if (Math.abs(v[0]) > maxBound || Math.abs(v[2]) > maxBound) {
      api.velocity.set(0, 0, 0)
    }
  })
  return () => unsubscribe()
}, [api])
useEffect(() => {
  const currentPosition = [0, 0, 0]
  const unsubscribe = api.position.subscribe((v: [number, number, number]) => {
    // Detect boundary collisions
    const maxBound = 9
    if (Math.abs(v[0]) > maxBound || Math.abs(v[2]) > maxBound) {
      api.velocity.set(0, 0, 0)
    }
  })
  return () => unsubscribe()
}, [api])

Motion State Monitoring

useEffect(() => {
  let isMoving = false;
  const unsubscribeVelocity = api.velocity.subscribe((v: [number, number, number]) => {
    const speed = Math.sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]);
    if (speed > 0.1 && !isMoving) {
      isMoving = true;
      playDiceRollingSound();
    } else if (isMoving && speed < 0.1) {
      isMoving = false;
      stopDiceRollingSound();
    }
  });
  return () => unsubscribeVelocity();
}, [api])
useEffect(() => {
  let isMoving = false;
  const unsubscribeVelocity = api.velocity.subscribe((v: [number, number, number]) => {
    const speed = Math.sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]);
    if (speed > 0.1 && !isMoving) {
      isMoving = true;
      playDiceRollingSound();
    } else if (isMoving && speed < 0.1) {
      isMoving = false;
      stopDiceRollingSound();
    }
  });
  return () => unsubscribeVelocity();
}, [api])
CC BY-NC-SA 4.0 2021-PRESENT © Bohua Xu