G GAMESRICH
Kids & Toddlers · Beginner

How to Build a Toddler Sensory Game: Budak Kecik

Create gentle responsive touch interactions with pentatonic sound synthesis and particle confetti.

By GamesRich Engineering 6 min read Zero External Libraries

Play the Live Game First

Experience the mechanics firsthand to understand what you are building.

PLAY LIVE GAME ↗

Architecture & Core Concept

Unlike competitive games, toddler sensory activities prioritize immediate tactile joy with zero penalty. Every tap generates a visual burst (expanding icon + exploding sparkles) paired with a calming musical note. The audio frequency maps to screen width: notes = [262, 330, 392, 523, 659, 784] // C Major pentatonic Because all notes belong to the pentatonic scale, any combination of taps sounds musical and pleasing.

Key Game Mechanics Taught:

Step-by-Step Implementation

1

Full-Screen Canvas with High DPI Support

Keep canvas graphics sharp on Retina displays by multiplying width and height by devicePixelRatio while scaling CSS width to 100vw/100vh.

Step 1 Implementation
function resizeCanvas() {
  const dpr = Math.min(window.devicePixelRatio || 1, 2);
  canvas.width = window.innerWidth * dpr;
  canvas.height = window.innerHeight * dpr;
  canvas.style.width = window.innerWidth + 'px';
  canvas.style.height = window.innerHeight + 'px';
  ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
}
window.addEventListener('resize', resizeCanvas);
2

Pentatonic Note Web Audio Synthesis

Avoid harsh notes by restricting synthesis to a pentatonic scale. The horizontal touch position determines the note pitch.

Step 2 Implementation
let audioCtx;
const pentatonicNotes = [262, 330, 392, 523, 659, 784]; // C4 to G5

function playChime(x) {
  audioCtx ||= new (window.AudioContext || window.webkitAudioContext)();
  const osc = audioCtx.createOscillator();
  const gain = audioCtx.createGain();

  const noteIndex = Math.floor((x / window.innerWidth) * pentatonicNotes.length);
  osc.frequency.value = pentatonicNotes[noteIndex] || 440;
  osc.type = 'sine'; // Soft, warm wave

  gain.gain.setValueAtTime(0.055, audioCtx.currentTime);
  gain.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + 0.3);

  osc.connect(gain).connect(audioCtx.destination);
  osc.start();
  osc.stop(audioCtx.currentTime + 0.35);
}
3

Expanding Icon & Sparkle Burst

When a pointer touches the screen, push a main expanding icon and 24 scattering sparkles into the particle simulation list.

Step 3 Implementation
function createBurst(x, y) {
  playChime(x);
  things.push({
    x, y,
    icon: ['★','♥','●','🐟','🌸'][Math.floor(Math.random() * 5)],
    grow: 0,
    life: 1.0,
    color: '#ffd447'
  });

  for (let i = 0; i < 20; i++) {
    const angle = Math.random() * Math.PI * 2;
    const speed = 70 + Math.random() * 200;
    sparkles.push({
      x, y,
      vx: Math.cos(angle) * speed,
      vy: Math.sin(angle) * speed,
      life: 0.7,
      size: 4 + Math.random() * 6
    });
  }
}

Complete Standalone Source Code

Here is the full, self-contained implementation. Save this as an index.html file on your computer and open it in any web browser to run your game immediately:

Complete Runnable Game (index.html)
<!DOCTYPE html>
<html><head><meta charset="utf-8"><title>Budak Kecik Sensory Demo</title>
<style>body{margin:0;overflow:hidden;background:#8066f4;touch-action:none;}</style>
</head><body><canvas id="c"></canvas>
<script>
const c = document.querySelector('#c'), ctx = c.getContext('2d');
let things = [], sparkles = [];
function resize() { c.width = innerWidth; c.height = innerHeight; }
window.onresize = resize; resize();

let audio;
function beep(x) {
  audio ||= new (window.AudioContext || window.webkitAudioContext)();
  const o = audio.createOscillator(), g = audio.createGain();
  const notes = [262, 330, 392, 523, 659, 784];
  o.frequency.value = notes[Math.floor((x/innerWidth)*notes.length)] || 440;
  o.type = 'sine';
  g.gain.setValueAtTime(0.05, audio.currentTime);
  g.gain.exponentialRampToValueAtTime(0.001, audio.currentTime + 0.28);
  o.connect(g).connect(audio.destination);
  o.start(); o.stop(audio.currentTime + 0.3);
}

function tap(x, y) {
  beep(x);
  things.push({ x, y, icon:['★','♥','●','🌸'][Math.floor(Math.random()*4)], grow:0, life:1 });
  for(let i=0;i<16;i++){
    const a = Math.random()*Math.PI*2, s = 80+Math.random()*150;
    sparkles.push({ x, y, vx:Math.cos(a)*s, vy:Math.sin(a)*s, life:0.6 });
  }
}

window.addEventListener('pointerdown', e => tap(e.clientX, e.clientY));

let last = performance.now();
function loop(t) {
  const dt = Math.min((t-last)/1000, 0.03); last = t;
  things.forEach(q => { q.life -= dt; q.grow += (1 - q.grow) * dt * 8; });
  things = things.filter(q => q.life > 0);
  sparkles.forEach(s => { s.x += s.vx*dt; s.y += s.vy*dt; s.vy += 150*dt; s.life -= dt; });
  sparkles = sparkles.filter(s => s.life > 0);

  ctx.fillStyle = '#8066f4'; ctx.fillRect(0,0,innerWidth,innerHeight);
  things.forEach(q => {
    ctx.save(); ctx.translate(q.x, q.y); ctx.scale(q.grow, q.grow);
    ctx.font = 'bold 64px sans-serif'; ctx.fillStyle = '#ffd447';
    ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
    ctx.fillText(q.icon, 0, 0); ctx.restore();
  });
  ctx.fillStyle = '#fff';
  sparkles.forEach(s => {
    ctx.beginPath(); ctx.arc(s.x, s.y, 5, 0, 7); ctx.fill();
  });
  requestAnimationFrame(loop);
}
requestAnimationFrame(loop);
</script>
</body></html>

Ideas to Expand Your Game

Now that you have the core mechanics running, consider adding: