Cara Membuat 6 Game Pembelajaran Berbasis Kamera: Tutorial Lengkap dari Persiapan sampai Hasil
Belajar matematika dengan menjepit angka di udara, menggambar segitiga menggunakan ujung jari, atau mencari benda sesuai kosakata bahasa Inggris bisa menjadi pengalaman yang berbeda dari mengerjakan kuis biasa. Kamera laptop atau ponsel menjadi penghubung antara gerakan pengguna dan aktivitas di layar.
Web pembelajaran berbasis kamera dapat dirancang untuk membaca posisi tangan, mengikuti gerak tubuh, atau mengenali kategori benda. Tantangannya bukan sekadar membuat gambar bergerak. Pengembang perlu menentukan kapan suatu gerakan dianggap sebagai pilihan, bagaimana jawaban dinilai, dan apa yang terjadi ketika kamera kehilangan jejak pengguna.
Panduan ini memandu pembuatan enam game melalui langkah berurutan, dengan contoh proyek untuk mempelajari input kamera, aturan permainan, hasil, dan penyimpanannya. Setelah tutorial praktik, tersedia rancangan framework, database, API, pengujian, deployment, dan prompt untuk mengembangkannya menjadi platform kelas. Gameplay keenam contoh berpusat pada kamera; navigasi akun dan pengaturan tetap menggunakan antarmuka biasa. Alternatif input hanya dibahas sebagai dukungan aksesibilitas. Contoh game merupakan gagasan pembangunan yang berlandaskan kemampuan teknis dari dokumentasi resmi. Ilustrasinya dibuat dengan AI untuk membantu membayangkan pengalaman bermain, bukan tangkapan layar produk yang sudah tersedia.

Ilustrasi konsep enam pengalaman belajar yang menggunakan kamera sebagai input. Dibuat dengan ImageGen.
Cara mengikuti panduan dari awal sampai akhir
Gunakan panduan ini sebagai enam latihan yang berbagi satu proyek. Kerjakan persiapan bersama terlebih dahulu, kemudian selesaikan Jepit Jawaban sampai hasilnya tersimpan. Sesudah itu, pilih game kedua sesuai kebutuhan belajar. Anda tidak perlu membuat enam repository, enam alur login, atau enam sambungan kamera yang berbeda. Setiap bab menunjukkan bagian yang dipakai bersama dan bagian yang diubah.
Ada dua hasil kerja yang perlu dibedakan. Jalur praktik menghasilkan prototipe lokal keenam game: kamera membaca input, permainan memberi umpan balik, hasil tersimpan pada browser, dan kamera berhenti. Seluruh kode jalur ini ditulis langsung dalam artikel, mulai dari fondasi bersama sampai modul keenam game. Jalur platform kelas menambahkan akun guru dan murid, PostgreSQL, penugasan, laporan, serta pengamanan API. Rancangan jalur kedua dijelaskan setelah tutorial game. Kode praktik di artikel ini belum membangun server, akun, atau dashboard kelas; bagian platform memberi rancangan pengembangannya.
Untuk jalur praktik, kita memakai JavaScript biasa, Vite, dan Canvas agar pembaca dapat melihat hubungan input dan aturan permainan tanpa mempelajari framework UI lebih dahulu. React dan TypeScript tetap menjadi pilihan untuk pengembangan platform yang lebih besar. Mengganti lapisan UI tidak mengharuskan Anda menulis ulang penghitung sudut, aturan pinch, atau interpreter robot apabila logika tersebut dipisahkan sejak awal.
Seluruh angka ambang, toleransi, dan waktu tahan dalam tutorial merupakan parameter rancangan contoh, bukan spesifikasi akurasi model. Gunakan nilai awalnya untuk belajar, lalu ukur pada perangkat dan pengguna sasaran. Buat file di komputer Anda sesuai nama yang tertulis pada setiap blok kode. Import, penyambungan antarmodul, pengujian, dan skema database dijelaskan di sini sehingga tidak perlu mengunduh proyek contoh atau lampiran artikel.
Istilah yang digunakan dalam tutorial
| Istilah | Arti dalam panduan |
|---|---|
| Web, browser | Web adalah aplikasi yang diakses melalui jaringan atau server lokal; browser adalah perangkat lunak yang menjalankannya. |
| JavaScript, TypeScript | JavaScript adalah bahasa kode contoh. TypeScript menambahkan sistem tipe pada pengembangan JavaScript. |
| Node.js, npm | Node.js menjalankan alat pengembangan; npm memasang paket dan menjalankan script proyek. |
| Paket, dependensi, library | Paket adalah unit distribusi kode. Dependensi adalah paket yang dibutuhkan proyek. Library menyediakan fungsi yang dipanggil aplikasi. |
| Vite, build, bundle | Vite adalah alat pengembangan dan pembuatan berkas aplikasi. Build menghasilkan berkas distribusi; bundle menggabungkan kode yang diperlukan. |
| Framework, React, Next.js | Framework menyediakan struktur pengembangan. React adalah library antarmuka; Next.js adalah framework berbasis React. |
| Canvas, Phaser | Canvas adalah bidang gambar yang dikendalikan kode browser. Phaser adalah framework game yang dapat mengelola scene dan objek permainan. |
| MediaPipe, TensorFlow.js | MediaPipe menyediakan task persepsi seperti landmark tangan. TensorFlow.js menyediakan eksekusi model pembelajaran mesin di JavaScript. |
| Model, inferensi, detector | Model adalah komponen terlatih. Inferensi menjalankan model pada input. Detector adalah modul yang menghasilkan informasi dari gambar. |
| Hand Landmarker, Pose Landmarker, Gesture Recognizer | Task MediaPipe untuk titik tangan, titik tubuh, dan kategori gestur tangan. |
| COCO-SSD | Model deteksi objek yang digunakan pada contoh Berburu Benda. |
| Frame, stream, track | Frame adalah satu gambar video. Stream adalah aliran media. Track adalah komponen media, misalnya video dari kamera. |
| Landmark, koordinat, normalisasi | Landmark adalah titik acuan. Koordinat menyatakan posisinya. Normalisasi mengubah posisi ke skala yang konsisten. |
| Pinch, drag, drop | Pinch adalah cubitan jari; drag memindahkan objek yang dipegang; drop melepasnya ke suatu area. |
| State, event, lifecycle | State adalah keadaan saat ini. Event adalah kejadian. Lifecycle adalah urutan pembentukan, pemakaian, dan penghentian sumber daya. |
| Hysteresis, dwell, cooldown | Hysteresis memakai ambang masuk dan keluar berbeda. Dwell mensyaratkan kondisi bertahan. Cooldown memberi jeda sebelum tindakan berikutnya. |
| Confidence, visibility, tracking | Confidence adalah skor keluaran model. Visibility adalah indikator keterlihatan landmark. Tracking mengikuti target antarframe. |
| Template, stroke, resample, rubrik | Template adalah jalur acuan. Stroke adalah satu goresan. Resample menyusun ulang sampel jalur. Rubrik menentukan cara menilai. |
| Interpreter, grid, fixture | Interpreter menjalankan perintah terbatas. Grid adalah papan berkotak. Fixture adalah input uji yang ditentukan sebelumnya. |
| WASM, CPU, GPU, worker | WASM adalah format kode yang dapat dijalankan browser. CPU/GPU adalah unit pemrosesan. Worker menjalankan pekerjaan terpisah dari thread utama. |
| Frontend, backend, API | Frontend adalah aplikasi pengguna. Backend adalah layanan server. API adalah kontrak interaksi antarkomponen. |
| HTTP, HTTPS, origin | HTTP adalah protokol komunikasi web. HTTPS menambahkan perlindungan transport. Origin menggabungkan skema, host, dan port. |
| JSON, DTO, payload | JSON adalah format data. DTO membatasi bentuk data antarlapisan. Payload adalah isi data suatu kiriman. |
| localStorage, IndexedDB | Penyimpanan browser. Contoh kecil memakai localStorage; antrean offline platform dapat dirancang memakai IndexedDB. |
| PostgreSQL, SQL, database | PostgreSQL adalah sistem database relasional. SQL adalah bahasa operasinya. Database menyimpan data terstruktur. |
| Primary key, foreign key, constraint, index | Identitas baris, hubungan antarbaris, aturan konsistensi, dan struktur bantu pencarian database. |
| UUID, idempotensi, transaksi | UUID adalah format identitas. Idempotensi menjaga retry tetap konsisten. Transaksi mengelompokkan operasi database. |
| Autentikasi, otorisasi, session, token | Pemeriksaan identitas, pemeriksaan izin, konteks masuk pengguna, dan bukti akses yang dipakai mekanisme autentikasi. |
| Cookie, CSRF, RLS | Cookie menyimpan informasi yang dikirim browser menurut aturannya. CSRF adalah penyalahgunaan request lintas situs. RLS membatasi baris melalui kebijakan database. |
| NestJS, Laravel, Sanctum | Framework backend TypeScript, framework backend PHP, dan paket autentikasi Laravel. |
| MVP, pilot, PRD, ADR | Versi minimum untuk membuktikan alur, uji terbatas, dokumen kebutuhan produk, dan catatan keputusan arsitektur. |
| Deployment, staging, rollback, backup, restore | Penerapan aplikasi, lingkungan praproduksi, pengembalian rilis, salinan pemulihan, dan proses memulihkannya. |
Definisi di atas memberi konteks membaca. Untuk kontrak API library, ikuti dokumentasi resmi yang ditautkan pada bagian teknis. Nama game, mekanik, struktur file, rubrik, serta urutan kerja merupakan rancangan tutorial ini.
Persiapan bersama: satu proyek untuk enam game
Langkah 1. Siapkan perangkat dan alat kerja
Siapkan laptop dengan kamera, editor kode, terminal, serta Node.js yang memenuhi kebutuhan Vite. Dokumentasi Vite mencantumkan kebutuhan Node.js 20.19+ atau 22.12+; periksa kembali jika memakai versi lain. Kode yang menjadi dasar tutorial telah dibangun dengan Node.js 24.19.0. Vite (2026/10), Getting Started
Periksa pemasangan melalui terminal:
node -v
npm -v
Pemeriksaan sebelum lanjut: kedua perintah menampilkan versi. Jika terminal tidak mengenali perintah, selesaikan pemasangan Node.js dahulu. Memasang MediaPipe tidak akan memperbaiki Node.js yang belum tersedia. Siapkan meja, posisi kamera yang tidak mudah bergeser, dan cahaya yang cukup untuk uji awal. Hindari memulai pengujian dengan banyak orang sekaligus di dalam gambar.
Langkah 2. Buat proyek kosong dan pasang dependensi
Buat folder kerja sendiri. Tidak perlu mengunduh proyek atau berkas contoh dari artikel. Buka terminal dan jalankan:
mkdir game-kamera
cd game-kamera
npm init -y
Buka folder tersebut melalui editor. Ganti seluruh isi package.json dengan konfigurasi berikut, lalu simpan. Nama file adalah petunjuk lokasi kode yang Anda buat, bukan tautan unduhan.
{
"name": "enam-game-kamera-tutorial",
"version": "1.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"test": "node --test tests/*.test.js",
"assets": "node scripts/copy-wasm.mjs"
},
"dependencies": {
"@mediapipe/tasks-vision": "1.1.1",
"@tensorflow-models/coco-ssd": "2.2.3",
"@tensorflow/tfjs": "4.22.0",
"vite": "8.3.4"
}
}
Kemudian pasang dependensinya:
npm install
Perintah ini menghasilkan package-lock.json di komputer Anda. Simpan berkas itu untuk instalasi berikutnya memakai npm ci. Versi tutorial adalah Vite 8.3.4, MediaPipe Tasks Vision 1.1.1, TensorFlow.js 4.22.0, serta COCO-SSD 2.2.3. Versi tersebut menyatakan konfigurasi dasar yang sudah diperiksa, bukan klaim bahwa semuanya selalu merupakan versi terbaru.
Melalui editor, buat folder src, src/games, scripts, tests, public, dan public/models. Tulis file fondasi pada langkah 4, termasuk script penyalin WASM, sebelum menjalankan npm run assets. File logika game ditulis pada bab masing-masing. Seluruh isinya ada dalam artikel ini.
Langkah 3. Lengkapi tiga berkas model MediaPipe
Library JavaScript dan berkas model merupakan dua kebutuhan yang berbeda. npm install memasang library. Untuk menjalankan deteksi, siapkan juga tiga model resmi MediaPipe pada folder kerja Anda. Berkas model adalah dependensi standar dari penyedia library, bukan berkas contoh pendamping artikel. Unduh model dari bagian Models pada dokumentasi resmi berikut dan simpan dengan nama persis seperti tabel. Panduan web resmi menjelaskan penggunaan berkas model melalui modelAssetPath. Google (2026/08), Hand Landmarker Web
| Kegunaan | Halaman model resmi | Nama di dalam proyek |
|---|---|---|
| Jepit dan gambar | Hand Landmarker, Models | public/models/hand_landmarker.task |
| Sudut dan jalur | Pose Landmarker, Models | public/models/pose_landmarker_lite.task |
| Kode gestur | Gesture Recognizer, Models | public/models/gesture_recognizer.task |
Untuk pose, pilih varian Lite yang sesuai nama konfigurasi contoh. Pastikan hasil unduhan benar-benar berkas model, bukan halaman HTML yang tersimpan dengan ekstensi .task. Perhatikan juga sistem operasi yang menyembunyikan ekstensi sehingga nama berubah menjadi .task.task. Tidak perlu memasukkan model tangan kedua pada game Kode Gestur karena adapter game tersebut menggunakan Gesture Recognizer.
Berburu Benda memakai jalur berbeda. Fungsi coco.load() menginisialisasi COCO-SSD dan mengambil aset model yang dibutuhkan paket. Karena contoh belum mengubah lokasi aset tersebut, koneksi internet diperlukan untuk pemuatan model objek. Sebelum penggunaan terbatas jaringan, rancang penyajian aset sendiri sesuai dokumentasi paket dan uji ulang. TensorFlow (diakses 2026/10), COCO-SSD README
Langkah 4. Tulis fondasi dan kenali tanggung jawab file
| File | Tugas yang dikerjakan | Diubah ketika |
|---|---|---|
index.html |
Memuat aplikasi dan wadah tampilan | Mengubah kerangka halaman |
src/main.js |
Menu, kamera, loop, hasil, cleanup | Mengubah alur sesi |
src/detectors.js |
Menyamakan keluaran library | Mengganti model atau runtime |
src/core.js |
Geometri, pinch, dwell, lintasan, interpreter | Mengubah aturan bersama |
src/render.js |
Menggambar tampilan game | Mengubah kartu, papan, atau pointer |
src/storage.js |
Membaca dan menyimpan hasil lokal | Mengubah penyimpanan prototipe |
src/games/pinch.js |
Aturan Jepit Jawaban | Mengubah soal dan drag kartu |
src/games/draw.js |
Aturan Gambar Bentuk | Mengubah template dan rubrik |
src/games/angle.js |
Aturan Sudut Lengan | Mengubah target atau sisi lengan |
src/games/lane.js |
Kalibrasi dan pilihan jalur | Mengubah kuis berbasis tubuh |
src/games/objects.js |
Target dan kestabilan objek | Mengubah kosakata |
src/games/code.js |
Antrean perintah dan puzzle robot | Mengubah puzzle |
tests/core.test.js |
Fixture perilaku berisiko | Memastikan perubahan tidak merusak aturan |
Setiap modul game mengekspor createGame(). Fungsi tersebut membuat sesi baru dengan state yang masih bersih. Properti kind memilih detector; instruction menjelaskan gerakan; update(observation) mengubah state; view() menyediakan data tampilan. Ketika selesai, update() mengembalikan jawaban, skor latihan, dan penjelasan. Dengan kontrak ini, main.js tidak perlu mengetahui rumus khusus keenam game.
Kode fondasi yang dibuat sebelum game pertama
Salin setiap blok ke file dengan nama yang ditunjukkan. Jangan menaruh semua kode dalam satu file karena jalur import mengikuti pembagian ini. Langkah pertama membuat modul sementara untuk keenam game. Modul sementara hanya menampilkan pesan jika dipilih; pada setiap bab, ganti seluruh isinya dengan implementasi game di bab tersebut. Cara ini membuat semua jalur import tersedia saat Anda baru menyelesaikan game pertama.
scripts/init-games.mjs: menyiapkan modul sementara
import { mkdir, writeFile } from 'node:fs/promises';
await mkdir('src/games', { recursive: true });
for (const name of ['pinch', 'draw', 'angle', 'lane', 'objects', 'code']) {
await writeFile(`src/games/${name}.js`,
'export function createGame() { throw new Error("Tulis modul game ini mengikuti babnya terlebih dahulu."); }\n',
{ flag: 'wx' });
}
console.log('Enam modul sementara dibuat. Ganti melalui langkah tiap game.');
Jalankan sekali pada proyek baru:
node scripts/init-games.mjs
Jangan menjalankannya untuk menimpa implementasi yang sudah Anda tulis. Script memakai flag: 'wx', sehingga akan berhenti jika file tujuan sudah ada. Pada kasus itu, pertahankan file yang sudah ada dan buat hanya file yang belum tersedia melalui editor.
index.html
Wadah halaman dan pemuatan modul utama.
<!doctype html><html lang="id"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>Lab Enam Game Kamera</title></head><body><div id="app"></div><script type="module" src="/src/main.js"></script></body></html>
scripts/copy-wasm.mjs
Salin WASM dari paket MediaPipe yang dipasang.
import {cp,mkdir} from 'node:fs/promises';
await mkdir('public/wasm',{recursive:true});
await mkdir('public/models',{recursive:true});
await cp('node_modules/@mediapipe/tasks-vision/wasm','public/wasm',{recursive:true});
console.log('WASM disalin dari paket yang terkunci. Tambahkan tiga model .task sesuai README.');
src/core.js
Fungsi geometri, gesture, dwell, rubrik lintasan, dan interpreter yang dipakai game.
export const clamp=(v,a=0,b=1)=>Math.min(b,Math.max(a,v));
export const distance=(a,b)=>Math.hypot(a.x-b.x,a.y-b.y);
export function angle(a,b,c){
const u={x:a.x-b.x,y:a.y-b.y},v={x:c.x-b.x,y:c.y-b.y};
const n=Math.hypot(u.x,u.y)*Math.hypot(v.x,v.y);
return n<1e-8?null:Math.acos(clamp((u.x*v.x+u.y*v.y)/n,-1,1))*180/Math.PI;
}
export class Dwell{
constructor(ms){this.ms=ms;this.reset();}
reset(){this.key=null;this.since=null;this.fired=false;}
step(key,now){
if(key==null){this.reset();return false;}
if(key!==this.key){this.key=key;this.since=now;this.fired=false;}
if(!this.fired&&now-this.since>=this.ms){this.fired=true;return true;}
return false;
}
}
export class Pinch{
constructor(){this.reset();}
reset(){this.active=false;this.armed=false;}
step(ratio){
if(!Number.isFinite(ratio)){this.reset();return 'lost';}
if(!this.armed){if(ratio>.5)this.armed=true;return 'idle';}
if(!this.active&&ratio<.3){this.active=true;return 'start';}
if(this.active&&ratio>.5){this.active=false;return 'end';}
return this.active?'hold':'idle';
}
}
export function handInput(o){
const p=o.hand;if(!p||p.length<21)return null;
const px=i=>({x:p[i].x*o.width,y:p[i].y*o.height});
const palm=distance(px(0),px(9));
if(palm<1)return null;
return {pointer:{x:clamp(1-p[8].x),y:clamp(p[8].y)},ratio:distance(px(4),px(8))/palm};
}
export function shoulderX(o){
const p=o.pose;
if(!p||[11,12].some(i=>(p[i]?.visibility??0)<.6))return null;
return 1-(p[11].x+p[12].x)/2;
}
export const inside=(p,r)=>p.x>=r.x&&p.x<=r.x+r.w&&p.y>=r.y&&p.y<=r.y+r.h;
export function sampleTriangle(n=90){
const corners=[{x:.5,y:.22},{x:.2,y:.78},{x:.8,y:.78},{x:.5,y:.22}];
return Array.from({length:n},(_,i)=>{const t=i/n*3,j=Math.floor(t),f=t-j;return{x:corners[j].x+(corners[j+1].x-corners[j].x)*f,y:corners[j].y+(corners[j+1].y-corners[j].y)*f};});
}
export function resample(points,step=.008){
if(points.length<2)return points.slice();
const out=[points[0]];let next=step,total=0;
for(let i=1;i<points.length;i++){
const a=points[i-1],b=points[i],d=distance(a,b);
if(d===0)continue;
while(next<=total+d){const f=(next-total)/d;out.push({x:a.x+(b.x-a.x)*f,y:a.y+(b.y-a.y)*f});next+=step;}
total+=d;
}return out;
}
export function traceScore(points,template,tol=.055){
const path=resample(points);
if(path.length<20)return {precision:0,coverage:0,score:0};
const close=(p,list)=>list.some(q=>distance(p,q)<=tol);
const precision=path.filter(p=>close(p,template)).length/path.length;
const coverage=template.filter(p=>close(p,path)).length/template.length;
return {precision,coverage,score:Math.round(100*(precision+coverage?2*precision*coverage/(precision+coverage):0))};
}
export function runRobot(program,puzzle){
let {x,y,dir}=puzzle.start;const path=[{x,y}],directions=[[0,-1],[1,0],[0,1],[-1,0]];
if(program.length>24)return{success:false,reason:'Program melebihi 24 langkah',path};
for(const command of program){
if(command==='R')dir=(dir+1)%4;
else if(command==='F'){
const [dx,dy]=directions[dir],nx=x+dx,ny=y+dy;
if(nx<0||ny<0||nx>=puzzle.size||ny>=puzzle.size||puzzle.walls.some(w=>w.x===nx&&w.y===ny))return{success:false,reason:'Robot menabrak batas atau penghalang',path};
x=nx;y=ny;path.push({x,y});
}else return{success:false,reason:'Perintah tidak dikenal',path};
}
const success=x===puzzle.goal.x&&y===puzzle.goal.y;
return{success,reason:success?'Robot mencapai tujuan':'Program selesai sebelum tujuan',path};
}
src/detectors.js
Adapter yang memilih dan menjalankan model sesuai jenis game.
export async function createDetector(kind){
if(kind==='objects'){
const tf=await import('@tensorflow/tfjs');await tf.ready();
const coco=await import('@tensorflow-models/coco-ssd');
const model=await coco.load({base:'lite_mobilenet_v2'});
return{async infer(video){return{objects:await model.detect(video,10,.5)};},close(){model.dispose();}};
}
const {FilesetResolver,HandLandmarker,PoseLandmarker,GestureRecognizer}=await import('@mediapipe/tasks-vision');
const files=await FilesetResolver.forVisionTasks('/wasm');
const common={runningMode:'VIDEO',baseOptions:{delegate:'CPU'}};
let model;
if(kind==='hand')model=await HandLandmarker.createFromOptions(files,{...common,numHands:1,baseOptions:{...common.baseOptions,modelAssetPath:'/models/hand_landmarker.task'}});
else if(kind==='pose')model=await PoseLandmarker.createFromOptions(files,{...common,numPoses:1,baseOptions:{...common.baseOptions,modelAssetPath:'/models/pose_landmarker_lite.task'}});
else if(kind==='gesture')model=await GestureRecognizer.createFromOptions(files,{...common,numHands:1,baseOptions:{...common.baseOptions,modelAssetPath:'/models/gesture_recognizer.task'}});
else throw new Error('Jenis detector tidak dikenal');
return{
async infer(video,now){
if(kind==='gesture'){const r=model.recognizeForVideo(video,now);return{hand:r.landmarks[0],gesture:r.gestures[0]?.[0]};}
const r=model.detectForVideo(video,now);
return kind==='hand'?{hand:r.landmarks[0]}:{pose:r.landmarks[0]};
},close(){model.close();}
};
}
src/storage.js
Penyimpanan ringkasan hasil pada browser yang sama.
const key='tutorial-camera-results-v1';
export function readResults(){try{return JSON.parse(localStorage.getItem(key)||'[]');}catch{return [];}}
export function saveResult(event){
const list=readResults();
if(list.some(x=>x.eventId===event.eventId))return;
// Prototype: limit local history. Not a shared classroom database.
localStorage.setItem(key,JSON.stringify([...list,event].slice(-100)));
}
export function clearResults(){localStorage.removeItem(key);}
src/render.js
Gambar kartu, lintasan, angka sudut, jalur, label, serta papan robot.
export function render(canvas,view,game){
const c=canvas.getContext('2d'),W=canvas.width,H=canvas.height;
c.clearRect(0,0,W,H);c.fillStyle='#eef6f7';c.fillRect(0,0,W,H);c.lineWidth=4;c.font='24px system-ui';
const text=(s,x,y,color='#153846')=>{c.fillStyle=color;c.fillText(s,x*W,y*H);};
const rect=(r,color)=>{c.fillStyle=color;c.fillRect(r.x*W,r.y*H,r.w*W,r.h*H);};
const line=(points,color,close=false)=>{if(!points.length)return;c.strokeStyle=color;c.beginPath();points.forEach((p,i)=>i?c.lineTo(p.x*W,p.y*H):c.moveTo(p.x*W,p.y*H));if(close)c.closePath();c.stroke();};
if(view.target){rect(view.target,'#a3d8c9');text('Kotak jawaban',.38,.78);view.cards.forEach(r=>{rect(r,'#fff');text(r.label,r.x+.06,r.y+.1);});text('Pilih yang setara 1/2',.32,.1);}
if(view.template){line(view.template,'#a6bcc2',true);line(view.points,'#087f8c');}
if(game==='angle'){text(view.value===null?'Tiga titik belum terlihat':`${Math.round(view.value)} derajat`,.3,.45);text('Target: 90 ± 15 derajat',.3,.6);}
if(game==='lane'){['A: ikan','B: kucing','C: ayam'].forEach((t,i)=>{const r={x:i/3+.025,y:.25,w:.28,h:.5};rect(r,view.lane===i?'#a3d8c9':'#fff');text(t,r.x+.025,.52);});}
if(game==='objects'){
text('Show a cup',.35,.18);
view.boxes.slice(0,8).forEach((b,i)=>text(`${b.class}: ${Math.round(b.score*100)}%`,.15,.3+i*.07));
}
if(view.puzzle){const p=view.puzzle,size=.1,x0=.32,y0=.12;
for(let y=0;y<p.size;y++)for(let x=0;x<p.size;x++){rect({x:x0+x*size,y:y0+y*size,w:size-.005,h:size-.005},p.walls.some(w=>w.x===x&&w.y===y)?'#344b56':x===p.goal.x&&y===p.goal.y?'#90d5a3':'#fff');}
text('↑',x0+p.start.x*size+.025,y0+p.start.y*size+.075);
if(view.result)line(view.result.path.map(p=>({x:x0+p.x*size+size/2,y:y0+p.y*size+size/2})),'#087f8c');
text(view.program.join(' ')||'Program masih kosong',.1,.72);text('F: maju R: putar kanan',.1,.84);
}
if(view.pointer){c.fillStyle='#e2663f';c.beginPath();c.arc(view.pointer.x*W,view.pointer.y*H,10,0,Math.PI*2);c.fill();}
}
src/style.css
Tampilan dasar aplikasi praktik yang Anda buat.
:root{font-family:system-ui,sans-serif;color:#173b48;background:#f8fbfc}body{margin:0}main{max-width:1200px;padding:24px;margin:auto}button,select{font:inherit;padding:10px;margin:6px;border:1px solid #8bb8be;border-radius:8px}button{cursor:pointer;background:#e0f1ef}button:disabled{opacity:.5}.panels{display:grid;grid-template-columns:1fr 1.5fr;gap:16px}video,canvas{width:100%;background:#14333f;border-radius:12px;align-self:start}video{transform:scaleX(-1)}pre{white-space:pre-wrap;overflow-wrap:anywhere;background:white;padding:16px;max-height:350px;overflow:auto}#status{font-weight:600}#message{min-height:3em}@media(max-width:700px){main{padding:12px}.panels{grid-template-columns:1fr}video{max-height:220px}}
src/main.js
Hubungkan menu, kamera, model, game, tampilan, hasil, dan cleanup.
import './style.css';
import {createDetector} from './detectors.js';
import {render} from './render.js';
import {readResults,saveResult,clearResults} from './storage.js';
const makers={pinch:()=>import('./games/pinch.js'),draw:()=>import('./games/draw.js'),angle:()=>import('./games/angle.js'),lane:()=>import('./games/lane.js'),objects:()=>import('./games/objects.js'),code:()=>import('./games/code.js')};
const titles=['Jepit Jawaban','Gambar Bentuk','Sudut Lengan','Jalur Jawaban','Berburu Benda','Kode Gestur'];
document.querySelector('#app').innerHTML=`<main><h1>Lab Game Kamera</h1><p>Prototipe tutorial. Kamera diproses lokal; hasil latihan disimpan pada browser ini.</p><label>Game <select id="game">${Object.keys(makers).map((k,i)=>`<option value="${k}">${titles[i]}</option>`).join('')}</select></label><button id="start">Mulai / ulang</button><button id="stop">Hentikan kamera</button><p id="instruction"></p><p id="status" role="status" aria-live="polite"></p><section class="panels"><video id="camera" autoplay muted playsinline></video><canvas id="scene" width="960" height="540"></canvas></section><p id="message"></p><h2>Hasil lokal</h2><button id="export">Unduh JSON</button><button id="clear">Hapus hasil lokal</button><pre id="results"></pre></main>`;
const video=document.querySelector('#camera'),canvas=document.querySelector('#scene'),status=document.querySelector('#status'),message=document.querySelector('#message'),select=document.querySelector('#game'),button=document.querySelector('#start');
let active=null,generation=0,busy=false,tryNumbers={};
function showResults(){document.querySelector('#results').textContent=JSON.stringify(readResults(),null,2);}
function stop(){
generation++;const old=active;active=null;
if(old){old.stop=true;clearTimeout(old.timer);old.stream?.getTracks().forEach(t=>t.stop());
// Closing waits for the last async inference before disposing model resources.
if(!old.infering)old.close();}
video.pause();video.srcObject=null;status.textContent='Kamera dihentikan.';
}
async function start(){
if(busy)return;stop();busy=true;button.disabled=true;
const session=generation,id=select.value;let detector,stream,state;
try{
status.textContent='Memuat model. Izinkan kamera ketika browser meminta.';
const game=(await makers[id]()).createGame();document.querySelector('#instruction').textContent=game.instruction;
detector=await createDetector(game.kind);
if(session!==generation){detector.close();return;}
stream=await navigator.mediaDevices.getUserMedia({audio:false,video:{width:{ideal:960},height:{ideal:540},facingMode:'user'}});
if(session!==generation){stream.getTracks().forEach(t=>t.stop());detector.close();return;}
video.srcObject=stream;await video.play();
if(session!==generation){stream.getTracks().forEach(t=>t.stop());detector.close();return;}
let closed=false;
state={stream,stop:false,infering:false,timer:null,close(){if(!closed){detector.close();closed=true;}}};active=state;
let lastVideo=-1,lastTime=null,submitted=false;
const tick=async()=>{
if(state.stop)return;state.infering=true;
try{
if(video.readyState>=2&&video.currentTime!==lastVideo){
lastVideo=video.currentTime;const now=performance.now();
const observation=await detector.infer(video,now);
if(state.stop||session!==generation)return;
// A long stall resets timing gates instead of counting unseen time as a hold.
if(lastTime!==null&&now-lastTime>750)game.update({now:now-1,width:video.videoWidth,height:video.videoHeight});
lastTime=now;
const result=game.update({...observation,now,width:video.videoWidth,height:video.videoHeight});
render(canvas,game.view(),id);message.textContent=game.view().message||'';
status.textContent='Kamera aktif. Ikuti instruksi permainan.';
if(result&&!submitted){
submitted=true;const attemptId=crypto.randomUUID(),tryNo=(tryNumbers[id]||0)+1;tryNumbers[id]=tryNo;
const event={eventId:crypto.randomUUID(),attemptId,game:id,itemKey:game.itemKey,tryNo,inputMode:'camera',...result,createdAt:new Date().toISOString()};
let saved=true;try{saveResult(event);}catch{saved=false;}
showResults();message.textContent=result.feedback;stop();
status.textContent=saved?'Latihan selesai dan hasil tersimpan lokal. Kamera dihentikan.':'Latihan selesai, tetapi penyimpanan lokal gagal. Salin hasil yang tampil sebelum mengulang.';
if(!saved)document.querySelector('#results').textContent=JSON.stringify(event,null,2);
}
}
}catch(error){stop();status.textContent='Inferensi gagal: '+error.message;}
finally{state.infering=false;if(state.stop)state.close();else state.timer=setTimeout(tick,100);}
};tick();
}catch(error){stream?.getTracks().forEach(t=>t.stop());detector?.close();if(session===generation)status.textContent='Belum bisa mulai: '+error.message;}
finally{busy=false;button.disabled=false;}
}
button.addEventListener('click',start);document.querySelector('#stop').addEventListener('click',stop);select.addEventListener('change',stop);
document.addEventListener('visibilitychange',()=>{if(document.hidden)stop();});window.addEventListener('pagehide',stop);
document.querySelector('#clear').addEventListener('click',()=>{clearResults();showResults();});
document.querySelector('#export').addEventListener('click',()=>{const url=URL.createObjectURL(new Blob([JSON.stringify(readResults(),null,2)],{type:'application/json'})),a=document.createElement('a');a.href=url;a.download='hasil-latihan-kamera.json';a.click();setTimeout(()=>URL.revokeObjectURL(url),1000);});
showResults();
Setelah fondasi tersimpan, jalankan:
npm run assets
Periksa bahwa public/wasm berisi berkas hasil salinan dari paket. Lengkapi model pada langkah 3, lalu ganti modul src/games/pinch.js dengan kode bab Jepit Jawaban sebelum mencoba game pertama. Lima game lain dapat tetap menjadi modul sementara sampai babnya dikerjakan.
Langkah 5. Jalankan halaman dan periksa aset
npm run dev
Buka URL localhost yang ditampilkan terminal. Jangan membuka index.html langsung dari pengelola berkas. Halaman harus menampilkan menu game, tombol mulai, panel kamera, bidang permainan, dan riwayat hasil. Kamera baru diminta saat tombol mulai ditekan. API getUserMedia() memerlukan konteks aman serta persetujuan pengguna; localhost dan domain HTTPS perlu diperlakukan sesuai aturan browser. Alamat HTTP melalui IP jaringan ponsel tidak otomatis setara localhost pada laptop. MDN (diakses 2026/10), getUserMedia, Security
Jika model gagal dimuat, periksa tab Network pada alat pengembang. Request /models/hand_landmarker.task harus menghasilkan berkas model, bukan respons 404 atau halaman fallback. Request /wasm/... harus menemukan berkas yang disalin script. Jangan mengubah kode drag kartu untuk memperbaiki kegagalan unduhan model; kedua masalah tersebut berada pada lapisan berbeda.
Langkah 6. Pahami alur satu frame
main.js memeriksa apakah video mempunyai frame baru. Adapter menjalankan detector yang sesuai, lalu menambahkan waktu dan dimensi video ke observasi. Modul game memeriksa input, memperbarui state, dan menyediakan tampilan. Renderer menggambar hasilnya. Jika game menghasilkan jawaban, aplikasi menyimpan event, menampilkan penjelasan, dan menghentikan kamera.
Loop contoh menjalankan satu inferensi aktif dan menunggu hasilnya sebelum menjadwalkan inferensi berikutnya. Jeda penjadwalan 100 milidetik ditambah waktu inferensi membatasi pekerjaan prototipe; ini bukan janji frame rate tertentu. Untuk produksi, ukur waktu nyata dan pertimbangkan worker setelah dukungan runtime diperiksa. Dokumentasi MediaPipe menyebut inferensi sinkron pada thread utama sebagai hal yang perlu diperhatikan. Google (2026/08), Pose Landmarker Web, Run the task
Langkah 7. Samakan koordinat sebelum membuat aturan
Video ditampilkan seperti cermin melalui CSS. Pointer game mengikuti pilihan tersebut dengan x = 1 - landmark.x; koordinat y tidak dibalik. Rumus jarak tangan dan sudut memakai dimensi sumber video, bukan ukuran CSS panel. Dengan demikian, label kiri pada layar mengikuti pandangan pengguna dan geometri tidak berubah hanya karena panel diperkecil.
Kamera dan bidang permainan ditempatkan sebagai dua panel, bukan overlay dengan crop tersembunyi. Ini sengaja menyederhanakan latihan koordinat. Jika nanti menumpuk landmark tepat di atas video, tambahkan perhitungan ukuran tampilan, letterbox, crop, serta transformasi cermin. Jangan menyalin koordinat prototipe ke overlay yang mempunyai aturan tampilan berbeda tanpa penyesuaian.
Langkah 8. Periksa penghentian dan hasil sebelum menambah fitur
Tekan Hentikan kamera, pindah game, atau pindah tab. Controller harus berhenti dan track kamera dilepas. Saat tombol mulai ditekan kembali, createGame() membuat state baru. Hasil inferensi sesi lama diabaikan memakai nomor generasi sesi. Jika inferensi objek masih berjalan ketika pengguna berhenti, model dibersihkan setelah pekerjaan aktif selesai.
Penyimpanan contoh berisi ringkasan jawaban pada localStorage, maksimum 100 hasil terakhir. Data ini hanya riwayat browser yang sama, dapat dihapus pengguna, dan bukan catatan nilai sekolah. Tersedia tombol unduh JSON serta hapus hasil. Setelah memahami batas ini, kerjakan tutorial pertama. Simpan integrasi server sampai alur kamera dan jawaban lokal benar-benar dapat dijelaskan.
Pemeriksaan fondasi selesai: dependensi terpasang, WASM tersedia, tiga model berada di lokasi yang benar, halaman localhost terbuka, kamera dapat dimulai dan dihentikan, serta tidak ada error pemuatan aset. Keenam tutorial berikut berangkat dari keadaan tersebut. Tulis modul pada bab yang akan dicoba terlebih dahulu; memilih game yang masih memakai modul sementara akan menampilkan pesan untuk melengkapi babnya.
Apa itu web pembelajaran berbasis kamera?
Web pembelajaran berbasis kamera adalah aplikasi di browser yang menggunakan gambar dari kamera sebagai input aktivitas. Gerakan tangan dapat menjadi penunjuk, posisi tubuh dapat memilih jalur jawaban, dan benda di depan kamera dapat menjadi bagian latihan kosakata.
Pada tingkat teknis, ada beberapa pendekatan yang berbeda. Pelacakan landmark menghasilkan titik-titik pada tangan atau tubuh. Pengenalan gestur menghasilkan kategori seperti telapak terbuka atau kepalan tangan. Deteksi objek menghasilkan kategori benda beserta lokasinya. Perbedaan ini menentukan mekanik permainan yang masuk akal untuk dibangun.
Sebagai contoh, MediaPipe Hand Landmarker menyediakan 21 titik tangan. Aplikasi kemudian perlu mengubah titik tersebut menjadi tindakan, misalnya mengambil kartu. Model tidak secara otomatis memahami bahwa kartu tertentu adalah jawaban yang benar untuk suatu soal.
Pemisahan antara deteksi dan aturan belajar menjadi fondasi penting. Deteksi bertugas menjawab “di mana tangan berada?”, sedangkan logika aplikasi menjawab “apakah pengguna sedang memilih?” dan “apakah pilihannya tepat?”. Dengan pembagian tersebut, materi dapat diubah tanpa membangun ulang seluruh sistem kamera.
Untuk proyek awal, satu pemain per perangkat lebih mudah dirancang dan diuji. Pengguna dapat duduk di depan laptop atau meletakkan ponsel pada penyangga. Aktivitas yang meminta banyak gerakan tubuh sebaiknya dikembangkan setelah interaksi sederhana terbukti nyaman pada perangkat sasaran.
Perbandingan enam game yang bisa dibuat
Enam contoh berikut memakai kamera sebagai input utama. Nama game digunakan untuk memudahkan pembahasan, bukan menunjukkan produk komersial tertentu.
| Game | Interaksi kamera | Materi yang cocok | Teknologi deteksi | Kesulitan rancangan |
|---|---|---|---|---|
| Jepit Jawaban | Menjepit dan memindahkan kartu | Matematika, pecahan, klasifikasi | Hand Landmarker | Menengah |
| Gambar Bentuk di Udara | Mengikuti jalur dengan ujung jari | Bentuk, sisi, sudut | Hand Landmarker | Menengah sampai tinggi |
| Sudut dengan Lengan | Membentuk sudut melalui posisi lengan | Geometri dasar | Pose Landmarker | Menengah sampai tinggi |
| Jalur Jawaban | Bergeser atau condong untuk memilih jalur | Kuis konsep dan kosakata | Pose Landmarker | Menengah |
| Berburu Benda dan Kosakata | Menunjukkan benda di depan kamera | Kosakata, observasi lingkungan | COCO-SSD atau model kustom | Menengah sampai tinggi |
| Kode Gestur | Membuat urutan instruksi dengan gestur | Pola, algoritme, debugging | Gesture Recognizer | Menengah |
Tingkat kesulitan tersebut merupakan perkiraan desain. Kompleksitas sebenarnya dipengaruhi perangkat yang harus didukung, variasi gerakan, jumlah materi, dan kebutuhan dashboard guru. Tidak semua game membutuhkan tampilan 3D atau backend sejak versi pertama.
1. Tutorial Jepit Jawaban: dari kamera sampai hasil pecahan

Konsep Jepit Jawaban: gerakan pinch diterjemahkan menjadi aksi mengambil dan memindahkan kartu. Ilustrasi ImageGen.
Hasil akhir bab ini adalah satu soal pecahan yang bisa diselesaikan dengan tangan: peserta mengambil kartu, memindahkannya, melepas ke kotak, lalu membaca penjelasan. Kamera berhenti setelah jawaban diterima. Gunakan bab ini untuk memahami seluruh alur sebelum menambah banyak soal. Jangan memulai dengan leaderboard, level, atau animasi yang belum diperlukan untuk membuktikan input.
Langkah 1. Tetapkan soal, jawaban, dan kondisi selesai
Soal latihan adalah “Pilih pecahan yang setara dengan 1/2”. Tiga kartu berisi 2/4, 2/3, dan 3/4 dengan ID a, b, dan c. Jawaban benar adalah a. Kondisi selesai bukan saat telunjuk menyentuh kartu, melainkan saat peserta melepaskan kartu ke area jawaban melalui transisi pinch yang sah.
Tuliskan penjelasan sebelum membuat mekanik: 2/4 setara dengan 1/2 karena pembilang dan penyebut dibagi dua. Penjelasan ini ditampilkan baik setelah jawaban benar maupun salah dengan kalimat yang sesuai. Nilai contoh 100 atau 0 menyatakan hasil satu soal, bukan penilaian komprehensif kemampuan pecahan. Gunakan itemKey: 'pecahan-1' untuk mengenali materi lokal ini.
Langkah 2. Pilih file dan library yang benar
Buka src/games/pinch.js, src/core.js, dan src/render.js. Modul game menyatakan kind: 'hand'; main.js akan memuat Hand Landmarker melalui adapter. Tidak perlu memasang detector pose atau Gesture Recognizer tambahan untuk menghitung cubitan. Task tangan menyediakan titik yang kemudian ditafsirkan oleh logika aplikasi. Google (2026/08), Hand Landmarker Web, Handle results
Lokasi kartu dan kotak menggunakan skala 0 sampai 1. Misalnya kartu pertama dimulai pada x 0,12 dan y 0,22. Renderer mengalikan nilai itu dengan ukuran Canvas. Jika ingin mengubah tata letak, ubah data area dan gambarannya bersama, sehingga area yang terlihat sama dengan area yang diperiksa aturan game.
Langkah 3. Jadikan telunjuk sebagai pointer
Fungsi handInput() mengambil ujung telunjuk sebagai pointer dan menghitung rasio pinch. Rasio merupakan jarak ujung ibu jari ke ujung telunjuk dibagi jarak pergelangan ke pangkal jari tengah pada contoh ini. Semua jarak dihitung di bidang piksel sumber video agar x dan y mempunyai skala setara. Fungsi mengembalikan null bila data tidak cukup.
Mulai game dengan telapak terlihat dan jari terbuka. Gerakkan telunjuk ke empat sisi bidang permainan. Pemeriksaan: pointer bergerak ke kanan ketika tangan bergerak ke kanan pada tampilan cermin. Bila terbalik, periksa transformasi x sekali pada adapter input. Membalik CSS sekaligus membalik data dua kali akan menghasilkan arah yang keliru.
Langkah 4. Ubah cubitan menjadi kejadian, bukan sinyal berulang
Class Pinch memiliki ambang mulai 0,3 dan ambang lepas 0,5 pada konfigurasi contoh. Tangan harus terbuka dahulu untuk mengaktifkan penerimaan cubitan. Ketika rasio melampaui batas mulai, keluar event start satu kali; selama jari masih menjepit, keluar hold; setelah terbuka, keluar end. Kehilangan tangan menghasilkan lost dan menonaktifkan penerimaan sampai tangan terbuka lagi.
Urutan ini mencegah kartu tiba-tiba diambil ketika peserta masuk ke bingkai dengan jari sudah terjepit. Nilai ambang perlu diuji berdasarkan bentuk tangan dan posisi kamera. Ubah satu parameter pada satu waktu, lalu ulangi gerakan yang sama. Jangan menurunkan ambang hanya karena satu percobaan gagal tanpa memeriksa apakah landmark memang terlihat.
Langkah 5. Ambil kartu dan pertahankan posisi relatifnya
Saat event start, fungsi inside() mencari kartu yang berada di bawah pointer. Jika tidak ada kartu, game tidak melakukan apa-apa. Jika ditemukan, state drag menyimpan kartu, indeks asal, dan selisih posisi pointer terhadap sudut kartu. Selisih tersebut menjaga kartu tetap terasa dipegang pada bagian yang sama, bukan meloncat sehingga sudutnya menempel ke jari.
Saat hold, posisi kartu diperbarui dari pointer dikurangi selisih tadi. Saat lost, kartu kembali ke tempat awal dan drag dibatalkan. Tidak ada jawaban yang diterima. Ini aturan penting: kamera gagal melihat tangan tidak sama dengan peserta sengaja melepaskan pilihan.
Langkah 6. Nilai hanya pada pelepasan yang sah
Ketika event end terjadi saat drag aktif, periksa titik tengah kartu terhadap kotak jawaban. Contoh memakai pusat kartu agar aturan mudah dijelaskan. Jika pusatnya berada di luar kotak, kembalikan kartu ke tempat semula. Jika berada di dalam, tandai game selesai, kembalikan ID opsi, dan tentukan hasil soal.
Kode lengkap modul mekanik terdapat di bawah. Import mengarah ke fungsi bersama yang sudah ditulis pada bagian fondasi, sehingga tidak perlu membuat fungsi bernama serupa dengan perilaku berbeda.
import {Pinch,handInput,inside} from '../core.js';
export function createGame(){
const pinch=new Pinch(),origin=[.12,.4,.68];
const cards=['2/4','2/3','3/4'].map((label,i)=>({id:['a','b','c'][i],label,x:origin[i],y:.22,w:.2,h:.18}));
const target={x:.34,y:.65,w:.32,h:.22};let drag=null,pointer=null,done=false;
return {kind:'hand',title:'Jepit Jawaban',itemKey:'pecahan-1',
instruction:'Tangan terbuka dahulu. Jepit kartu setara 1/2, bawa ke kotak, lalu buka jari.',
update(o){
if(done)return;const h=handInput(o);pointer=h?.pointer;
const event=pinch.step(h?.ratio);
if(event==='lost'){if(drag){drag.card.x=origin[drag.index];drag.card.y=.22;}drag=null;return;}
if(event==='start'){const i=cards.findIndex(c=>inside(pointer,c));if(i>=0)drag={card:cards[i],index:i,dx:pointer.x-cards[i].x,dy:pointer.y-cards[i].y};}
if(drag&&(event==='hold'||event==='end')){drag.card.x=pointer.x-drag.dx;drag.card.y=pointer.y-drag.dy;}
if(event==='end'&&drag){const {card,index}=drag;drag=null;
if(inside({x:card.x+card.w/2,y:card.y+card.h/2},target)){
done=true;return{answer:{optionId:card.id},score:card.id==='a'?100:0,feedback:card.id==='a'?'Benar. 2/4 disederhanakan menjadi 1/2.':'Belum tepat. Pembilang dan penyebut 1/2 dikalikan 2 menghasilkan 2/4.'};
}card.x=origin[index];card.y=.22;
}
},view(){return {cards,target,pointer};}
};
}
Langkah 7. Baca hasil dan simpan satu event
Objek yang dikembalikan game mempunyai answer.optionId, score, dan feedback. Controller menambahkan event ID, attempt ID lokal, jenis game, waktu, dan input mode kamera. Controller menyimpan sekali, menggambar hasil terakhir, lalu memanggil penghentian kamera. Menekan tombol ulang membuat sesi baru; state selesai pada sesi lama tidak dipakai kembali.
Untuk platform kelas, kirim optionId sebagai jawaban dan biarkan server membaca kunci dari versi soal. Nilai 100 dari browser bukan data yang dipercaya server. Detail pemetaan event lokal ke respons API dijelaskan pada bagian sesudah tutorial keenam. Dengan begitu, mekanik drag tetap sama ketika penyimpanan lokal kelak diganti API.
Langkah 8. Jalankan percobaan dari awal sampai akhir
Pilih Jepit Jawaban pada menu, tekan Mulai, dan izinkan kamera. Buka jari sampai input siap. Arahkan pointer ke 2/4, cubit, bawa ke kotak hijau, lalu buka jari. Hasil yang diharapkan adalah penjelasan pecahan, skor latihan 100, satu baris riwayat baru, dan kamera berhenti. Tekan Mulai lagi dan pilih kartu salah untuk memeriksa penjelasan koreksi.
Sesudah itu, coba lepaskan kartu di luar kotak. Permainan harus berlanjut tanpa menyimpan jawaban. Ambil kartu lalu keluarkan tangan dari gambar; kartu harus kembali. Gerakkan tangan terbuka di atas kotak tanpa kartu; tidak boleh muncul hasil. Rangkaian ini membuktikan bahwa game memeriksa tindakan yang dimaksud, bukan sekadar lokasi jari.
Langkah 9. Perbaiki masalah berdasarkan gejalanya
| Gejala | Bagian yang diperiksa | Tindakan awal |
|---|---|---|
| Pointer tidak muncul | Kamera, model, atau handInput |
Pastikan model termuat dan seluruh tangan terlihat |
| Kartu tidak dapat diambil | Aktivasi pinch dan hitbox | Buka jari dahulu, lalu cubit di dalam kartu |
| Kartu meloncat | Selisih posisi drag | Simpan dx dan dy saat start, jangan hitung ulang selama hold |
| Jawaban keluar saat tangan hilang | Cabang lost | Batalkan drag tanpa evaluasi |
| Satu cubitan menghasilkan dua nilai | State done dan penerimaan event | Kunci hasil sesi dan simpan event satu kali |
Langkah 10. Kembangkan setelah latihan pertama selesai
Pindahkan isi soal menjadi data berversi apabila menambah materi. Pertahankan ID opsi yang stabil ketika label atau urutan kartu berubah. Tambahkan tombol bantuan atau narasi, tetapi catat bantuan terpisah dari jawaban. Untuk perpindahan soal otomatis, reset pinch, drag, dan gate pemilihan dahulu; jangan membiarkan cubitan terakhir menjawab soal berikutnya.
Prompt pengembangan: “Gunakan modul pinch pada proyek ini. Tambahkan tiga soal pecahan melalui data lokal berversi. Pertahankan aturan satu jawaban per sesi soal, batal saat tracking hilang, dan penjelasan setiap opsi. Tunjukkan file yang berubah serta pengujian bahwa posisi kartu dari soal lama tidak terbawa ke soal baru.”
Bab selesai apabila: satu jawaban benar dan satu jawaban salah dapat diuji dari kamera, hasil tersimpan sekali, pelepasan di luar kotak tidak dinilai, dan kehilangan tangan tidak menghasilkan jawaban.
2. Tutorial Gambar Bentuk: dari lintasan jari sampai rubrik yang jelas

Konsep menggambar bentuk: garis di udara merupakan visualisasi artistik lintasan jari. Ilustrasi ImageGen.
Hasil bab ini adalah aktivitas menjiplak segitiga melalui cubitan dan gerakan telunjuk. Aplikasi membandingkan goresan dengan jalur acuan, menampilkan dua ukuran, menyimpan hasil latihan, lalu menghentikan kamera. Ini contoh penilaian lintasan, belum pengenal tulisan tangan atau tes pengetahuan bentuk bebas.
Langkah 1. Batasi tugas yang akan dinilai
Gunakan satu segitiga dengan titik awal di sudut atas. Instruksi meminta peserta mengikuti tiga sisi dalam satu goresan dan kembali ke awal sebelum membuka jari. Jangan menambahkan lingkaran, huruf, dan angka pada rubrik yang belum mampu membedakannya. Satu tugas yang terdefinisi membuat hasilnya dapat dipahami dan diperbaiki.
Tentukan sejak awal bahwa skor latihan menggambarkan kecocokan lintasan. Kalimat “segitiga mempunyai tiga sisi dan tiga sudut” diberikan sebagai materi. Jika ingin mengukur apakah peserta memahami ciri segitiga, tambahkan soal konsep terpisah pada tahap lanjutan, dengan nilai dan ID item terpisah. Kode praktik pada bab ini belum mengimplementasikan soal konsep tersebut.
Langkah 2. Gunakan detector tangan dan fungsi bersama
Buka src/games/draw.js. Game ini menggunakan kind: 'hand', sehingga model dan izin kamera sama dengan Jepit Jawaban. Fungsi Pinch, handInput, sampleTriangle, dan traceScore diimpor dari core.js. Ini memperlihatkan manfaat fondasi bersama: cara menerima tangan tidak perlu ditulis ulang, sementara makna gerakannya dapat berbeda.
Renderer menggambar jalur abu-abu dan goresan berwarna di bidang permainan. Panel kamera tetap terpisah. Jangan mengira garis yang terlihat pada ilustrasi AI merupakan algoritme pengenalan yang sudah disediakan model. Hand Landmarker menghasilkan titik, sedangkan template dan rubrik merupakan kode tutorial. Google (2026/08), Hand Landmarker Web, Results
Langkah 3. Buat template sebelum merekam goresan
sampleTriangle() menghasilkan sampel titik dari tiga ruas segitiga pada bidang ternormalisasi. Titik sudut contoh berada di atas tengah, bawah kiri, dan bawah kanan. Template sengaja sama antara tampilan dan evaluator; jangan menggambar satu bentuk tetapi menilai terhadap koordinat bentuk lain.
Ubah ukuran tampilan browser dan perhatikan posisi template. Karena data disimpan pada skala 0 sampai 1, gambar mengikuti ukuran Canvas. Untuk prototipe ini, rubrik juga memakai ruang Canvas yang sama. Jika kelak jenis layar diubah menjadi bidang persegi atau memakai crop, evaluasi ulang toleransi dan bentuk visualnya bersama-sama.
Langkah 4. Tentukan kapan pena menulis
Jari terbuka berarti pena belum menulis. Event start membuat array titik baru. Event hold menambahkan titik ketika gerakan cukup jauh dari titik terakhir. Event end mengakhiri goresan dan menjalankan penilaian. Input lost membuang goresan aktif dan meminta peserta mulai lagi. Aturan ini sengaja ketat agar versi awal tidak menghubungkan dua potongan goresan yang sebenarnya terputus.
Contoh membatasi jumlah titik sampai 2.000 dan jarak minimum antartitik sekitar 0,004 pada bidang normalisasi. Batas ini merupakan keputusan prototipe untuk mengendalikan ukuran state. Jika latihan berlangsung lama, jangan terus menambahkan titik tanpa batas. Periksa juga respons perangkat sebelum meningkatkan frekuensi pembacaan kamera.
Langkah 5. Hilangkan keuntungan dari banyaknya frame
Goresan yang direkam perangkat cepat dapat mempunyai lebih banyak titik daripada goresan yang sama pada perangkat lambat. Karena itu, fungsi resample() menyusun sampel menurut jarak sepanjang lintasan. Perbandingan didasarkan pada bentuk jalur, bukan jumlah frame mentah. Titik identik berulang di satu tempat tidak otomatis menjadi gambar yang lengkap.
Periksa fungsi ini dengan data sintetis. Buat array berisi 100 titik yang semuanya sama, lalu nilai terhadap segitiga. Hasil yang diharapkan rendah atau nol karena lintasan belum terbentuk. Buat array yang mengikuti template hingga kembali ke awal; nilainya seharusnya jauh lebih tinggi. Dua pemeriksaan ini membantu membedakan rubrik yang bekerja dari rubrik yang hanya menghitung banyak data.
Langkah 6. Gabungkan ketepatan dan cakupan
Ketepatan adalah bagian sampel goresan yang dekat dengan template. Cakupan adalah bagian sampel template yang didekati goresan. Satu titik yang diam mungkin dekat dengan template, tetapi cakupannya kecil. Coretan besar yang mengenai semua sisi mungkin mempunyai cakupan tinggi, tetapi ketepatannya rendah. Contoh memakai rata-rata harmonik kedua ukuran untuk skor latihan 0 sampai 100.
Toleransi awal adalah 0,055 pada ruang normalisasi. Toleransi ini perlu ditinjau terhadap ukuran layar dan kemampuan gerak sasaran. Goresan dengan cakupan di bawah 20 persen dianggap belum cukup untuk dinilai sebagai hasil final. UI meminta peserta mencoba lagi tanpa menyimpan jawaban akademik yang keliru.
import {Pinch,handInput,sampleTriangle,traceScore,distance} from '../core.js';
export function createGame(){
const pinch=new Pinch(),template=sampleTriangle();let points=[],pointer=null,done=false,message='Jiplak segitiga dalam satu goresan.';
return {kind:'hand',title:'Gambar Bentuk',itemKey:'segitiga-1',
instruction:'Buka jari, arahkan ke sudut atas. Jepit sambil menjiplak, lalu buka untuk menilai.',
update(o){if(done)return;const h=handInput(o);pointer=h?.pointer;const e=pinch.step(h?.ratio);
if(e==='lost'){points=[];message='Tangan hilang. Goresan dibatalkan, mulai lagi.';return;}
if(e==='start'){points=[pointer];message='Ikuti ketiga sisi sampai kembali ke awal.';}
if(e==='hold'&&points.length<2000&&distance(pointer,points.at(-1))>.004)points.push(pointer);
if(e==='end'){
const metric=traceScore(points,template);
if(metric.coverage<.2){message='Goresan belum cukup. Jepit untuk mencoba lagi.';return;}
done=true;return{answer:{precision:metric.precision,coverage:metric.coverage},score:metric.score,feedback:`Ketepatan ${Math.round(metric.precision*100)}%, cakupan ${Math.round(metric.coverage*100)}%. Segitiga mempunyai tiga sisi dan tiga sudut. Nilai ini latihan lintasan, bukan tes pemahaman konsep.`};
}
},view(){return{template,points,pointer,message};}
};
}
Langkah 7. Tampilkan alasan, bukan angka saja
Sesudah goresan diterima, tampilkan ketepatan dan cakupan secara terpisah. Peserta dapat mengetahui apakah perlu mengikuti jalur lebih dekat atau menyelesaikan sisi yang belum dilalui. Simpan kedua ringkasan itu sebagai answer.precision dan answer.coverage. Jangan menyimpan semua frame kamera untuk sekadar menghasilkan laporan latihan.
Rubrik contoh tidak memeriksa urutan sisi, jumlah goresan alternatif, maupun arah gerak. Ia juga tidak membuktikan bahwa bentuk yang dijiplak dapat dikenali ketika dirotasi bebas. Tulis batas tersebut dalam deskripsi materi. Kejelasan batas membuat hasil lebih dapat dipercaya daripada memberi nama “AI pembaca semua bentuk” pada evaluator yang sebenarnya sempit.
Langkah 8. Uji satu sesi lengkap
Pilih Gambar Bentuk, mulai kamera, dan buka jari. Arahkan pointer ke sudut atas, cubit, lalu ikuti sisi kiri, sisi bawah, dan sisi kanan hingga kembali ke awal. Buka jari. Periksa bahwa penjelasan muncul, dua ukuran tampil, satu hasil lokal tersimpan, dan kamera berhenti.
Ulangi dengan hanya mengikuti satu sisi. Hasil harus menunjukkan cakupan lebih kecil atau meminta pengulangan jika belum cukup. Ulangi dengan tangan keluar gambar saat menggambar. Goresan harus dibatalkan; tidak boleh muncul garis penghubung dari titik lama ke posisi saat tangan kembali. Coba juga menggambar lambat dan lebih cepat untuk melihat apakah rubrik terlalu sensitif terhadap frekuensi sampel.
Langkah 9. Cari sumber masalah pada lapisan yang tepat
| Gejala | Penyebab yang perlu diperiksa | Perbaikan awal |
|---|---|---|
| Garis muncul sebelum mulai | Status pinch tidak dipakai | Rekam hanya setelah start |
| Garis melompat setelah tangan kembali | Stroke lama diteruskan | Batalkan saat lost |
| Diam di sudut mendapat nilai tinggi | Hanya mengukur kedekatan | Tambahkan cakupan dan syarat panjang |
| Perangkat cepat selalu unggul | Memakai jumlah frame sebagai nilai | Resample menurut panjang lintasan |
| Bentuk baik tetap rendah | Template dan toleransi tidak konsisten | Bandingkan koordinat data dengan gambar |
Langkah 10. Tambahkan bentuk berikutnya tanpa merusak makna skor
Untuk persegi, ganti pembangkit template dengan empat ruas dan pertahankan skala penilaian. Untuk lingkaran, sampelkan titik dari pusat dan radius yang ditentukan. Pisahkan nama template dan versi rubrik dalam data item. Bila rubrik berubah, jangan menimpa interpretasi hasil lama tanpa mencatat versi.
Prompt pengembangan: “Tambahkan template persegi pada modul draw, dengan data template terpisah dari aturan pinch. Buat fixture lintasan lengkap, satu sisi saja, titik diam, dan coretan berlebihan. Pertahankan dua metrik serta jelaskan bahwa hasil mengukur menjiplak, bukan pengetahuan konsep. Jangan menambah pengenalan tulisan tangan yang belum diimplementasikan.”
Bab selesai apabila: awal dan akhir stroke jelas, tangan hilang membatalkan stroke, titik diam tidak mendapat nilai penuh, hasil menjelaskan ketepatan dan cakupan, serta riwayat menyimpan satu hasil yang sesuai.
3. Tutorial Sudut Lengan: dari tiga titik tubuh sampai target 90 derajat

Konsep belajar sudut melalui posisi lengan. Overlay bersifat ilustratif, bukan pengukuran anatomi. Ilustrasi ImageGen.
Hasil bab ini adalah latihan membentuk sudut siku yang tampak mendekati 90 derajat pada gambar kamera. Aplikasi menampilkan angka, menunggu posisi bertahan, lalu memberi penjelasan tentang sudut siku-siku. Penghitungan menggunakan proyeksi dua dimensi; hasil latihan tidak dimaksudkan sebagai pengukuran kesehatan atau penilaian akhir pengetahuan geometri.
Langkah 1. Tentukan target yang nyaman
Pilih satu target awal, yaitu 90 derajat dengan toleransi rancangan 15 derajat dan waktu tahan 1,2 detik. Peserta boleh mengulang atau menghentikan latihan. Atur kamera sehingga bahu, siku, dan pergelangan kanan tampak dalam gambar. Jangan meminta gerak ekstrem demi mendapatkan angka yang terlihat tepat.
Tentukan bidang pengamatan: lengan bergerak sejajar bidang kamera. Jika lengan mengarah mendekati atau menjauhi kamera, sudut proyeksinya dapat berbeda dari sudut fisik. Beri petunjuk posisi yang sederhana sebelum menampilkan target. Untuk tahap awal, kamera tidak perlu otomatis menentukan semua kemungkinan sudut ruang.
Langkah 2. Aktifkan model pose
Buka src/games/angle.js. Properti kind: 'pose' membuat adapter memuat Pose Landmarker Lite. Pastikan public/models/pose_landmarker_lite.task tersedia. Panduan resmi menjelaskan landmark tubuh dan keluaran koordinatnya. Google (2026/08), Pose Landmarker Web, Handle results
Contoh memakai sisi kanan pada indeks bahu 12, siku 14, dan pergelangan 16, sesuai pemetaan landmark MediaPipe yang dirujuk pada panduan pose. Nama kanan adalah sisi anatomis peserta, bukan sisi kanan panel setelah pencerminan. Jangan mengganti indeks hanya karena tampilan kamera terlihat seperti cermin.
Langkah 3. Tolak titik yang belum layak dihitung
Sebelum menghitung sudut, periksa keberadaan pose dan visibility tiga titik. Contoh menggunakan batas awal 0,6. Jika ada titik di bawah batas, kosongkan nilai dan reset waktu tahan. UI meminta pengguna memastikan tiga bagian lengan terlihat. Kamera yang kehilangan siku tidak boleh menghasilkan sudut baru dari nilai lama.
Batas visibility adalah kebijakan penerimaan contoh, bukan jaminan akurasi. Uji kondisi pakaian, jarak, dan latar yang mewakili pengguna. Bila model sering gagal karena kamera hanya menangkap telapak, perbaiki posisi kamera terlebih dahulu. Memperbesar toleransi sudut tidak memperbaiki landmark yang salah.
Langkah 4. Ubah koordinat menjadi bidang piksel yang sama
Koordinat x dan y landmark dinormalisasi terhadap lebar dan tinggi gambar. Fungsi pixel(i) mengalikan x dengan videoWidth dan y dengan videoHeight. Langkah ini penting karena video 16:9 bukan bidang persegi. Menghitung langsung dari x dan y ternormalisasi dapat mengubah hubungan geometrinya. Google (2026/08), Pose Landmarker Web, Normalized landmarks
Contoh penghitung sudut berada di core.js. Bentuk vektor dari siku ke bahu dan dari siku ke pergelangan. Hitung dot product, bagi dengan hasil kali panjang vektor, batasi hasil pembagian ke rentang minus satu sampai satu, lalu gunakan arccos dan konversi ke derajat. Jika vektor terlalu pendek, kembalikan null agar input tidak valid tidak menjadi angka palsu.
Langkah 5. Periksa rumus dengan titik sintetis
Sebelum melibatkan kamera, gunakan titik bahu (0,0), siku (1,0), dan pergelangan (1,1). Sudutnya harus 90 derajat. Gunakan titik bahu sama dengan siku untuk memeriksa kasus vektor nol; hasilnya harus null. Tes ini tersedia dalam tests/core.test.js.
Jika fungsi salah pada fixture sederhana, jangan mengkalibrasi kamera untuk menutupi kesalahan tersebut. Selesaikan rumus terlebih dahulu. Setelah rumus benar, barulah periksa apakah landmark dari kamera sesuai dengan bagian tubuh yang dimaksud. Pemisahan ini membuat proses belajar lebih mudah karena setiap pemeriksaan mempunyai satu tujuan.
Langkah 6. Syaratkan waktu tahan yang benar-benar berkelanjutan
Setiap frame yang layak menghasilkan sudut. Jika selisih terhadap 90 paling besar 15 derajat, masukkan kunci target ke Dwell. Jika keluar toleransi atau tracking hilang, reset. Ketika kondisi bertahan 1,2 detik, game mengembalikan hasil. Ini menghindari keberhasilan dari satu frame yang kebetulan melewati target.
Controller juga membatalkan hitungan yang melintasi jeda observasi panjang pada contoh ini. Jika perangkat berhenti memproses sementara, waktu yang tidak diamati tidak boleh dianggap sebagai bukti pengguna terus menahan posisi. Parameter jeda tersebut perlu disesuaikan setelah pengukuran pada perangkat sasaran.
import {angle,Dwell} from '../core.js';
export function createGame(){
const hold=new Dwell(1200);let value=null,done=false;
return{kind:'pose',title:'Sudut Lengan',itemKey:'siku-kanan-90',
instruction:'Hadapkan lengan kanan ke bidang kamera. Bentuk 90 derajat dan tahan 1,2 detik. Tidak perlu memaksakan gerak.',
update(o){if(done)return;const p=o.pose;
if(!p||[12,14,16].some(i=>(p[i]?.visibility??0)<.6)){value=null;hold.reset();return;}
const pixel=i=>({x:p[i].x*o.width,y:p[i].y*o.height});value=angle(pixel(12),pixel(14),pixel(16));
if(hold.step(value!==null&&Math.abs(value-90)<=15?'target':null,o.now)){
done=true;return{answer:{target:90,measured:Math.round(value),side:'right'},score:100,feedback:'Target latihan tercapai. Sudut 90 derajat disebut siku-siku. Ini sudut proyeksi gambar dan hasil latihan gerak, bukan pengukuran anatomi atau nilai pemahaman konsep.'};
}
},view(){return{value,message:value===null?'Pastikan bahu, siku, dan pergelangan kanan terlihat.':`Sudut tampak: ${Math.round(value)} derajat. Target 90 ± 15.`};}
};
}
Langkah 7. Pisahkan target gerak dari pemahaman konsep
Simpan target, sudut tampak saat diterima, dan sisi lengan sebagai ringkasan. Skor 100 pada contoh berarti target latihan tercapai menurut rubrik, bukan bahwa peserta menguasai seluruh klasifikasi sudut. Umpan balik memperkenalkan istilah siku-siku. Untuk tes konsep, tambahkan item yang meminta peserta memilih kategori setelah latihan, dengan jalur input dan nilai yang dijelaskan tersendiri.
Jika peserta tidak mencapai target karena tracking sulit, berikan kesempatan memperbaiki kamera atau memakai aktivitas alternatif yang disepakati guru. Status teknis perlu dibedakan dari jawaban konsep salah. Bagian laporan kelas dapat menggabungkan keduanya secara informatif tanpa menyamakan kemampuan gerak dengan kemampuan matematika.
Langkah 8. Coba dari tampilan awal sampai hasil tersimpan
Pilih Sudut Lengan, tekan Mulai, dan posisikan lengan kanan sesuai instruksi. Perhatikan angka yang berubah. Bentuk posisi mendekati siku-siku dan tahan. Hasil yang diharapkan adalah penjelasan target, satu record lokal, serta kamera berhenti. Ulangi dengan bergerak keluar toleransi sebelum waktu selesai; timer harus memulai lagi.
Tutup pergelangan atau keluarkan siku dari gambar. Nilai harus tidak tersedia dan target tidak diterima. Miringkan panel tampilan tanpa mengubah kamera, lalu bandingkan hasil; ukuran CSS semata tidak seharusnya mengubah rumus. Jika orientasi perangkat mengubah sumber video, mulai ulang sesi dan periksa ulang posisi.
Langkah 9. Perbaiki berdasarkan sumber kesalahan
| Gejala | Bagian yang diperiksa | Tindakan |
|---|---|---|
| Sudut berubah aneh pada layar lebar | Skala x dan y | Hitung pada dimensi sumber video |
| Sudut tetap meski lengan hilang | Visibility dan reset | Kosongkan nilai, hentikan dwell |
| Lengan kiri bergerak tetapi target kanan tidak terbaca | Indeks sisi tubuh | Gunakan sisi yang diminta atau ubah ketiga indeks bersama |
| Berhasil terlalu cepat | Waktu tahan dan jeda frame | Pastikan kondisi berkelanjutan |
| Posisi terasa tepat tetapi angka berbeda | Proyeksi dan arah lengan | Sejajarkan lengan dengan bidang kamera |
Langkah 10. Tambahkan target tanpa mengaburkan keterbatasan
Setelah target 90 stabil, buat konfigurasi 60 dan 120 derajat. Gunakan label lancip, siku-siku, atau tumpul berdasarkan materi, bukan berdasarkan dugaan dari kualitas tracking. Untuk pilihan lengan kiri, gunakan indeks 11, 13, dan 15 secara konsisten serta simpan pilihan tersebut pada hasil.
Prompt pengembangan: “Ubah angle menjadi aktivitas dengan target dari data. Pertahankan penghitungan dalam piksel sumber, penolakan vektor nol, reset saat visibility gagal, dan dwell berkelanjutan. Tambahkan pilihan sisi lengan serta fixture kanan dan kiri. Jangan menyatakan angka proyeksi sebagai pengukuran anatomi.”
Bab selesai apabila: rumus lulus fixture, tiga titik tidak valid menunda target, waktu tahan terputus mengulang timer, hasil tersimpan, dan UI menjelaskan makna hasil latihan secara tepat.
4. Tutorial Jalur Jawaban: dari kalibrasi tubuh sampai pilihan kuis

Konsep Jalur Jawaban menggunakan pergeseran tubuh untuk memilih A, B, atau C. Ilustrasi ImageGen.
Hasil bab ini adalah kuis tiga pilihan yang dapat dijawab dengan condong ke kiri, tengah, atau kanan. Sebelum soal aktif, pengguna mengkalibrasi posisi netral dan rentang geraknya. Sesudah membaca, pengguna mengaktifkan pemilihan dengan keluar dari netral, memilih jalur, lalu bertahan. Urutan ini mencegah posisi diam langsung dianggap jawaban tengah.
Langkah 1. Tentukan soal dan aturan memilih
Soal contoh adalah “Hewan mana yang mamalia?” dengan A ikan, B kucing, dan C ayam. Pilihan benar adalah B. Materi ini sengaja mudah agar pengembang dapat memusatkan perhatian pada alur kontrol. Gunakan instruksi tertulis yang selalu terlihat, sebab permainan berbasis tubuh tidak seharusnya mengandalkan tebakan pengguna mengenai arti posisi avatar.
Alur yang ditetapkan adalah kalibrasi netral, kalibrasi kiri, kalibrasi kanan, waktu membaca, aktivasi pilihan, dwell, penjelasan, dan penyimpanan. Tidak ada tuntutan melompat atau berlomba. Mode duduk dapat dicoba selama bahu terlihat dan pengguna mempunyai rentang condong yang nyaman. Posisi kamera sebaiknya tetap selama satu sesi.
Langkah 2. Pakai titik tengah bahu sebagai input
Buka src/games/lane.js dan shoulderX() pada core.js. Game menggunakan Pose Landmarker, kemudian mengambil rata-rata koordinat kedua bahu. Nilai x dicerminkan agar arah layar sesuai tampilan kamera. Jika salah satu bahu tidak cukup terlihat menurut kebijakan contoh, input dikembalikan sebagai null.
Pada task pose, lokasi tubuh berasal dari keluaran model; pemilihan bahu sebagai kontrol merupakan keputusan tutorial. Google (2026/08), Pose Landmarker Web, Results Jangan memakai tinggi badan atau jarak piksel tetap sebagai satu-satunya dasar pembagian jalur karena posisi kamera dan ukuran tubuh pengguna dapat berbeda.
Langkah 3. Ambil posisi netral yang stabil
Kalibrasi pertama meminta peserta duduk atau berdiri nyaman pada posisi netral. Simpan beberapa sampel terakhir. Contoh menganggap stabil jika sebarannya kecil, lalu meminta keadaan tersebut bertahan. Rata-rata sampel diterima sebagai netral. Jumlah sampel dan toleransi sebaran adalah parameter awal yang perlu dicoba, bukan ketetapan semua perangkat.
Lihat pesan kalibrasi sebelum bergerak. Jangan berpindah ke kiri ketika aplikasi masih mengumpulkan posisi netral. Pada produk kelas, tambahkan indikator progres agar peserta tahu kapan boleh berpindah. Prototipe memberikan pesan tahap; penyempurnaan visual tidak mengubah aturan pengambilan sampelnya.
Langkah 4. Kalibrasi kiri dan kanan secara terpisah
Peserta condong ke kiri dengan nyaman dan menahan posisi, lalu mengulang ke kanan. Nilai kiri harus cukup lebih kecil dari netral; nilai kanan harus cukup lebih besar. Pemeriksaan arah mencegah aplikasi menerima posisi netral yang sama untuk semua tahap ketika pengguna belum memahami instruksi.
Setelah ketiga posisi diterima, periksa rentang. Contoh memakai selisih minimal 0,06 pada koordinat x ternormalisasi. Jika rentang tidak memadai, ulangi kalibrasi dan jelaskan penyebabnya. Jangan membagi dengan rentang yang mendekati nol karena gerakan sangat kecil kemudian dapat diterjemahkan menjadi perpindahan jalur ekstrem.
Langkah 5. Normalisasikan pergeseran menurut sisi
Untuk tubuh di kiri netral, bagi selisih posisi dengan rentang kiri. Untuk tubuh di kanan, gunakan rentang kanan. Dengan dua pembagi, pengguna tidak harus mempunyai rentang gerak simetris. Nilai sekitar minus satu berarti kiri kalibrasi, nol netral, dan plus satu kanan kalibrasi.
Tentukan jalur kiri untuk nilai di bawah minus 0,55, kanan di atas 0,55, dan tengah untuk nilai absolut di bawah 0,2. Area di antara batas tidak menghasilkan kandidat. Area kosong tersebut memberi ruang perpindahan tanpa langsung memilih. Ini dead band sederhana; prototipe belum menggunakan state hysteresis jalur penuh.
Langkah 6. Pisahkan membaca, aktivasi, dan konfirmasi
Setelah kalibrasi, contoh memberi 2,5 detik untuk membaca. Selama tahap ini, dwell pilihan direset. Setelah waktu baca, peserta harus bergerak keluar dari netral terlebih dahulu. Baru sesudah itu, jalur yang ditahan 0,9 detik diterima. Jika ingin memilih tengah, peserta bergeser keluar lalu kembali ke tengah.
Aturan aktivasi tersebut sengaja ditulis di instruksi. Tanpa itu, soal dengan jawaban tengah dapat selesai ketika peserta masih diam setelah kalibrasi. Dalam produk yang lebih matang, waktu baca sebaiknya dapat disesuaikan atau diganti konfirmasi siap yang dapat diakses. Jangan mempercepatnya semata-mata agar demonstrasi terlihat lebih ramai.
import {Dwell,shoulderX,clamp} from '../core.js';
export function createGame(){
const hold=new Dwell(900),stages=['neutral','left','right'];let stage=0,samples=[],cal={},lane=null,done=false,readSince=null,selectionArmed=false,message='Kalibrasi posisi netral.';
return {kind:'pose',title:'Jalur Jawaban',itemKey:'hewan-mamalia-1',
instruction:'Kalibrasi netral, kiri, kanan. Baca soal. Keluar dari posisi netral dahulu, lalu pilih A/B/C dan tahan.',
update(o){if(done)return;const x=shoulderX(o);
if(x===null){hold.reset();samples=[];lane=null;message='Bahu tidak terlihat; pemilihan dijeda.';return;}
if(stage<3){
const key=stages[stage];message=`Kalibrasi ${['posisi netral','condong kiri dengan nyaman','condong kanan dengan nyaman'][stage]}; tahan stabil.`;
samples.push(x);if(samples.length>12)samples.shift();
const eligible=stage===0||(stage===1&&x<cal.neutral-.06)||(stage===2&&x>cal.neutral+.06);
const stable=eligible&&samples.length>=8&&Math.max(...samples)-Math.min(...samples)<.035;
if(hold.step(stable?key:null,o.now)){cal[key]=samples.reduce((a,b)=>a+b,0)/samples.length;stage++;hold.reset();samples=[];
if(stage===3){if(cal.neutral-cal.left<.06||cal.right-cal.neutral<.06){stage=0;message='Rentang belum cukup. Ulang kalibrasi dengan kamera tetap.';}else readSince=o.now;}}
return;
}
if(o.now-readSince<2500){message='Baca soal: hewan mana yang mamalia? Tunggu hingga pilihan aktif.';hold.reset();return;}
const t=x<cal.neutral?-(cal.neutral-x)/(cal.neutral-cal.left):(x-cal.neutral)/(cal.right-cal.neutral);
if(!selectionArmed){message='Pilihan siap. Bergeser keluar dari netral dahulu, lalu arahkan ke jawaban.';if(Math.abs(t)>.4)selectionArmed=true;hold.reset();return;}
// Dead bands between three lanes prevent immediate accidental selections.
lane=t<-.55?0:t>.55?2:Math.abs(t)<.2?1:null;
message='A: ikan | B: kucing | C: ayam. Tahan pada satu pilihan 0,9 detik.';
if(hold.step(lane,o.now)){done=true;return{answer:{optionId:['a','b','c'][lane]},score:lane===1?100:0,feedback:lane===1?'Benar. Kucing termasuk mamalia.':'Belum tepat. Kucing adalah mamalia; ikan dan ayam bukan mamalia.'};}
},view(){return{lane,message};}
};
}
Langkah 7. Simpan pilihan akademik, bukan koordinat mentah
Ketika dwell selesai, jawaban lokal berisi optionId a, b, atau c. Penjelasan menyebut kucing sebagai mamalia. Skor tidak ditentukan oleh kecepatan condong, jauh pergeseran, atau confidence pose. Kalibrasi dan kegagalan tracking merupakan informasi kontrol yang berbeda dari jawaban soal.
Jika beberapa soal digabung, buat tahap membaca dan aktivasi baru untuk setiap soal. Jangan membawa kandidat jalur atau timer dari soal sebelumnya. Simpan hasil per item agar guru dapat melihat konsep yang perlu diulang. Modul akun dan percobaan tetap dipakai bersama dengan game lain ketika sudah terhubung server.
Langkah 8. Jalankan skenario lengkap dengan urutan yang sama
Pilih Jalur Jawaban dan tekan Mulai. Ikuti pesan netral, kiri, lalu kanan. Tunggu soal aktif. Keluar dari netral, lalu kembali ke tengah untuk memilih kucing. Tahan sampai hasil keluar. Periksa satu hasil tersimpan dan kamera berhenti. Pada pengulangan, pilih ikan atau ayam dan baca penjelasan koreksinya.
Selanjutnya, uji tetap diam di tengah setelah kalibrasi. Game tidak boleh langsung menyimpan jawaban. Uji bergerak dari kiri menuju kanan tanpa menahan tengah; B tidak boleh diterima karena sekadar dilewati. Uji menutup salah satu bahu; timer harus batal, bukan terus menghitung dengan koordinat lama.
Langkah 9. Tangani masalah kalibrasi terlebih dahulu
| Gejala | Bagian yang diperiksa | Tindakan |
|---|---|---|
| Arah kiri menjadi kanan | Pencerminan x | Terapkan satu transformasi yang konsisten |
| Kalibrasi langsung selesai tanpa bergerak | Syarat arah tiap tahap | Wajibkan posisi kiri/kanan berbeda dari netral |
| Jawaban B muncul ketika diam | Gate aktivasi | Wajibkan keluar dari netral sebelum pemilihan |
| B terpilih saat menyeberang | Dwell dan dead band | Jangan menerima sentuhan jalur sesaat |
| Gerak kecil berpindah ekstrem | Rentang kalibrasi | Tolak rentang terlalu sempit dan ulangi |
Langkah 10. Tambahkan kuis dan pengalaman kelas
Pisahkan array soal dari mekanik kalibrasi. Kalibrasi dapat digunakan selama kamera dan posisi pengguna tidak berubah, tetapi sediakan cara mengulang ketika perangkat bergeser. Beri teks pilihan yang cukup besar dan jangan mengandalkan warna sebagai satu-satunya penanda. Phaser boleh menggantikan renderer Canvas jika membutuhkan avatar atau lintasan animasi; evaluator jawaban dan state fase tetap dipertahankan.
Prompt pengembangan: “Tambahkan lima soal ke lane tanpa mengulang kalibrasi setiap soal. Setiap item harus mempunyai waktu baca, aktivasi keluar netral, dwell baru, dan feedback. Jika tracking hilang, batalkan timer. Buat tes bahwa jawaban soal pertama tidak otomatis menjawab soal kedua.”
Bab selesai apabila: rentang kiri dan kanan benar, diam tidak menjawab, perpindahan sesaat tidak diterima, tracking hilang membatalkan dwell, dan pilihan yang tersimpan sesuai jalur yang dikonfirmasi.
5. Tutorial Berburu Benda: dari label model sampai latihan kosakata

Konsep berburu benda menghubungkan kosakata dengan objek yang ditunjukkan ke kamera. Ilustrasi ImageGen.
Hasil bab ini adalah latihan “Show a cup”. Peserta menunjukkan satu cangkir, model membaca objek, game menunggu label stabil, lalu menampilkan arti kata dan menyimpan hasil target deteksi. Benda disiapkan di meja agar peserta tidak perlu berjalan sambil melihat layar ponsel. Gambar ilustrasi memberikan gambaran konsep, sedangkan prototipe menampilkan video dan daftar label pada panel terpisah.
Langkah 1. Pilih target yang didukung model
Mulai dengan satu target cup. Jangan meminta objek khusus yang belum termasuk label model. COCO-SSD menghasilkan bounding box, label kelas, dan skor deteksi; ia dapat diberi input elemen video. Daftar kelas pada repository resminya mencakup label yang dapat diperiksa sebelum menyusun materi. TensorFlow (diakses 2026/10), COCO-SSD README TensorFlow (diakses 2026/10), classes.ts
Tuliskan pemetaan materi: label model cup, kata Inggris cup, arti Indonesia cangkir, instruksi Show a cup, dan penjelasan setelah target diterima. Nama materi yang tampil tidak harus sama persis dengan label internal, tetapi hubungan keduanya harus eksplisit. Misalnya jangan menyamakan semua wadah minum dengan cup hanya karena terjemahannya terasa dekat.
Langkah 2. Siapkan runtime dan koneksi pemuatan
Buka src/detectors.js pada cabang objects. Adapter mengimpor TensorFlow.js, menunggu runtime siap, lalu memanggil coco.load({base: 'lite_mobilenet_v2'}). Import baru dilakukan ketika game objek dipilih. Contoh ini tidak memuat TensorFlow.js untuk sesi Jepit Jawaban yang hanya membutuhkan MediaPipe.
Pemuatan model objek dapat membutuhkan jaringan. Periksa Network jika halaman lama pada status memuat. Jangan menyatakan aplikasi sepenuhnya offline hanya karena inferensi berjalan di perangkat. Untuk penggunaan tanpa jaringan, pemasangan aset dan cache harus menjadi pekerjaan terpisah yang diuji pada lingkungan sasaran, termasuk lisensi dan lokasi model.
Langkah 3. Baca hasil deteksi sebelum membuat penilaian
Adapter menjalankan model.detect(video, 10, .5) dan mengembalikan array objek. Renderer menampilkan beberapa label beserta persentase skor agar pengembang dapat melihat keluaran. Pada prototipe ini, bounding box tidak digambar di atas video; daftar label sudah cukup untuk membuktikan hubungan model dengan target latihan.
Letakkan satu cangkir di depan kamera dan lihat apakah cup muncul. Jika model terus membaca benda lain, ubah jarak, sudut, atau latar. Catat kondisi tersebut sebagai batas model pada perangkat yang diuji. Jangan mengubah label salah menjadi benar hanya di tampilan demi membuat demonstrasi terlihat berhasil.
Langkah 4. Buat penerimaan yang konservatif dan mudah dijelaskan
Buka src/games/objects.js. Cari kandidat berlabel cup dengan skor setidaknya 0,65. Jika ada, beri kunci cup kepada Dwell(1500). Jika tidak ada, reset. Setelah target hadir dalam observasi berurutan selama 1,5 detik, game selesai.
Ambang skor dan waktu tahan adalah kebijakan contoh. Dwell pada label tidak menjamin objek fisik yang sama selalu terdeteksi; prototipe belum melacak identitas instance melalui bounding box. Untuk kelas kecil satu benda di meja, batas ini dapat dipahami. Jika tugas meminta melacak atau menghitung beberapa benda, diperlukan mekanisme berbeda dan uji tambahan.
Langkah 5. Pisahkan skor model dari hasil latihan
Nilai 100 pada game contoh berarti target cup diterima menurut aturan deteksi. Nilai tersebut tidak dihitung dengan mengalikan confidence menjadi angka akademik. Confidence yang berubah karena pencahayaan bukan ukuran langsung kemampuan bahasa peserta. Umpan balik menyatakan cup berarti cangkir dan menjelaskan makna keberhasilan.
Untuk mengukur kosakata, tambahkan tugas lanjutan dengan pilihan makna atau penggunaan kata. Simpan hasil pemahaman itu sebagai item lain. Menunjukkan benda yang diminta dapat menjadi bagian latihan, tetapi produk tidak boleh melaporkan penguasaan kosakata menyeluruh hanya dari satu label yang dikenali kamera.
import {Dwell} from '../core.js';
export function createGame(){
const hold=new Dwell(1500);let done=false,boxes=[],message='Tunjukkan satu cangkir. Target label model: cup.';
return {kind:'objects',title:'Berburu Benda',itemKey:'cup-1',
instruction:'Siapkan cangkir di meja. Tunjukkan kepada kamera sampai label cup stabil 1,5 detik.',
update(o){if(done)return;boxes=o.objects??[];const hit=boxes.find(p=>p.class==='cup'&&p.score>=.65);
message=hit?'Cangkir terlihat. Tahan tetap.':'Target belum stabil. Tunjukkan satu cangkir dengan jelas.';
if(hold.step(hit?'cup':null,o.now)){done=true;return{answer:{detectedLabel:'cup'},score:100,feedback:'Cup berarti cangkir. Target deteksi tercapai. Confidence tidak dipakai sebagai nilai penguasaan kosakata.'};}
},view(){return{boxes,message};}
};
}
Langkah 6. Tentukan kapan satu ronde berakhir
Setelah dwell diterima, state done mencegah keluaran berikutnya menjadi hasil baru. Controller menyimpan satu event dan menghentikan kamera. Ini membuat satu cangkir yang tetap berada di meja tidak menghasilkan nilai terus-menerus. Untuk versi satu target, tombol Mulai digunakan sebagai pengulangan eksplisit.
Jika mengembangkan banyak ronde otomatis, tambahkan fase target diterima, umpan balik, target dikeluarkan, lalu ronde baru. Ketika dua ronde berturut-turut meminta cup, gate keluarkan benda menjadi penting. Sekadar mengganti nomor soal tanpa membersihkan state dapat membuat benda lama menjawab tugas berikutnya.
Langkah 7. Uji target benar dan contoh negatif
Pilih Berburu Benda, tekan Mulai, dan tunggu model siap. Tunjukkan cangkir dengan posisi cukup jelas. Periksa pesan tahan, lalu hasil akhir, satu riwayat baru, dan kamera berhenti. Catat perangkat dan keadaan cahaya agar pengujian berikutnya dapat dibandingkan.
Ulangi dengan botol, tangan kosong, dan latar meja. Tidak ada target yang seharusnya diterima hanya karena benda tersebut umum berada di meja. Tunjukkan cangkir sesaat kurang dari waktu tahan, hilangkan, lalu tunjukkan lagi; timer harus dimulai ulang. Contoh negatif memberi informasi yang tidak didapat dari pengujian sukses saja.
Langkah 8. Simpan ringkasan yang diperlukan
Jawaban lokal menyimpan detectedLabel: 'cup'. Contoh tidak menyimpan foto maupun video. Jika laporan teknis membutuhkan ringkasan kesulitan, tambahkan jumlah target hilang atau durasi mencari secara terbatas, dengan tujuan yang jelas. Jangan menyimpan seluruh keluaran setiap frame hanya karena mudah dilakukan.
Pada server, item target dipilih dari versi materi yang ditugaskan, bukan dari label yang dikirim klien tanpa konteks. Browser dapat dimodifikasi, sehingga latihan deteksi lokal tidak boleh diposisikan sebagai ujian berpengawasan yang membuktikan keaslian benda. Gunakan data sesuai tujuan produk dan komunikasikan batas tersebut kepada guru.
Langkah 9. Gunakan diagnosis yang sesuai
| Gejala | Bagian yang diperiksa | Tindakan |
|---|---|---|
| Model tidak selesai dimuat | Jaringan dan aset model | Periksa request dan coba lagi setelah koneksi tersedia |
| Target selalu gagal | Label materi dan keluaran model | Pastikan target termasuk daftar kelas |
| Target diterima terlalu cepat | Timer dan reset | Syaratkan observasi berurutan sesuai dwell |
| Banyak hasil dari benda yang sama | State done atau fase ronde | Kunci satu hasil dan minta benda keluar sebelum ronde baru |
| Skor berubah mengikuti cahaya | Pemakaian confidence | Pisahkan confidence dari nilai kosakata |
Langkah 10. Tambahkan label atau model kustom secara sadar
Untuk benda lain yang sudah didukung, ubah data target dan penjelasannya. Uji label tersebut dengan contoh positif dan negatif. Untuk benda khusus yang tidak didukung, pilih model lain atau latih model sesuai kebutuhan. Classifier satu objek dominan berbeda dari detector yang melokalisasi beberapa objek; jangan mengganti keduanya tanpa menyesuaikan aturan dan UI.
Prompt pengembangan: “Pisahkan target cup ke katalog materi. Tambahkan bottle dan book hanya setelah label terverifikasi. Setiap ronde harus memiliki label internal, kata tampil, arti, dwell, fase keluarkan benda, dan satu event hasil. Buat tes label salah serta target yang muncul terputus. Jangan menyimpan frame atau menjadikan confidence sebagai nilai akademik.”
Bab selesai apabila: model siap, label cup dapat diamati, contoh negatif tidak langsung diterima, dwell terputus mengulang timer, dan satu target menghasilkan satu record yang maknanya jelas.
6. Tutorial Kode Gestur: dari gerakan tangan sampai robot mencapai tujuan

Konsep Kode Gestur mengubah gerakan tangan menjadi urutan instruksi untuk robot. Ilustrasi ImageGen.
Hasil bab ini adalah puzzle grid dengan urutan maju dan putar kanan. Peserta menyusun program melalui kategori gestur, memperbaiki antrean, lalu menjalankannya. Interpreter menghitung lintasan robot dan menentukan apakah tujuan tercapai. Input kamera dan aturan puzzle dipisahkan agar kesalahan membaca tangan tidak bercampur dengan kesalahan algoritme.
Langkah 1. Buat puzzle yang dapat diselesaikan di atas kertas
Contoh memakai grid 4 × 4. Robot mulai di kolom 0, baris 3, menghadap atas. Tujuan berada di kolom 2, baris 1, dan penghalang di kolom 1, baris 2. Kolom bertambah ke kanan dan baris bertambah ke bawah. Urutan solusi contoh adalah F F R F F: maju dua kali, putar kanan, lalu maju dua kali.
Gambar atau tulis posisi tersebut sebelum membuat gesture. Pastikan solusi tidak menabrak penghalang dan tidak keluar papan. Jika puzzle belum mempunyai solusi yang jelas, kualitas detector tidak akan memperbaikinya. Batas program contoh adalah 24 perintah agar eksekusinya terbatas dan mudah diperiksa.
Langkah 2. Pilih kategori gestur yang tersedia
Game menggunakan Gesture Recognizer dengan kategori bawaan. Tutorial memilih Pointing_Up, Victory, Closed_Fist, Thumb_Down, Thumb_Up, dan Open_Palm. Daftar kategori didokumentasikan oleh Google; pemetaan kategori ke perintah robot adalah keputusan desain tutorial. Google (2026/10), Gesture Recognizer, Canned gestures
| Gestur | Tindakan game | Arti untuk pengguna |
|---|---|---|
| Telapak terbuka | Menyiapkan penerimaan | Lakukan di antara dua perintah |
| Telunjuk ke atas | Tambah F | Robot maju satu sel saat program dijalankan |
| Dua jari berbentuk V | Tambah R | Robot berputar 90 derajat ke kanan |
| Kepalan | Hapus perintah terakhir | Koreksi satu langkah |
| Jempol ke bawah | Kosongkan program | Susun ulang dari awal |
| Jempol ke atas | Jalankan program | Nilai urutan yang sudah disusun |
Tidak ada perintah kiri langsung pada prototipe. Jika diperlukan, tiga kali putar kanan setara perubahan arah kiri pada interpreter ini. Untuk materi pemula, jangan menambah banyak gestur sebelum pengguna memahami satu perintah dan kondisi netral.
Langkah 3. Siapkan adapter dan keluaran gestur
Buka src/detectors.js pada cabang gesture. Adapter memuat gesture_recognizer.task dan menggunakan recognizeForVideo(video, timestamp). Hasil kategori teratas untuk tangan pertama dikirim ke modul game. API web yang digunakan perlu sesuai versi paket yang dikunci. Google (diakses 2026/10), GestureRecognizer JavaScript API
Prototipe membatasi satu tangan untuk menyederhanakan identitas pemain. Jika kelak menerima dua tangan, tentukan apakah kedua tangan mempunyai peran atau hanya satu yang aktif. Jangan menggabungkan kategori tangan berbeda pada timer yang sama tanpa aturan karena satu pemain dapat menambah perintah yang tidak dimaksudkan.
Langkah 4. Terapkan kondisi netral sebelum menerima perintah
Menahan telunjuk selama beberapa detik tidak boleh menambahkan F setiap frame. Contoh meminta Open_Palm stabil 0,35 detik untuk mengaktifkan penerimaan. Setelah satu tindakan diterima, penerimaan terkunci. Peserta harus membuka telapak lagi sebelum perintah berikutnya. Menghilangkan tangan saja tidak membuka kunci itu.
Perintah perlu mempunyai skor kategori setidaknya 0,65 dan bertahan 0,55 detik pada contoh. Jika kategori berubah sebelum waktunya, timer tindakan direset. Gunakan indikator pesan siap atau buka telapak agar peserta memahami mengapa gestur yang ditahan tidak terus menambah perintah. Ini aturan interaksi, bukan kerusakan model.
Langkah 5. Pisahkan menyusun dan menjalankan program
Saat F atau R diterima, tambahkan simbol ke array program. Kepalan menghapus elemen terakhir, dan jempol bawah mengosongkan array. Robot belum bergerak pada tahap ini. UI menampilkan antrean sehingga peserta dapat memperkirakan lintasan dan melakukan debugging sebelum menjalankan.
Jempol atas menyalin program untuk dievaluasi. Dengan pemisahan ini, urutan tidak berubah di tengah eksekusi karena kamera masih membaca tangan. Contoh menutup sesi setelah satu eksekusi; tombol Mulai menjadi kesempatan mencoba ulang. Untuk produk yang mengizinkan koreksi dalam satu sesi puzzle, pisahkan status evaluasi dan status edit secara eksplisit.
Langkah 6. Jalankan interpreter yang terbatas
Fungsi runRobot() hanya mengenali F dan R. F menghitung sel berikutnya dari arah robot, lalu memeriksa batas dan penghalang. R mengubah arah dengan modulo empat. Perintah yang tidak dikenal ditolak. Tidak ada eval() atau eksekusi kode JavaScript yang berasal dari gesture.
Interpreter mengembalikan lintasan, status tujuan, dan alasan berhenti. Renderer menggambar garis lintasan pada grid setelah evaluasi; prototipe belum menyediakan animasi langkah demi langkah. Penyempurnaan animasi dapat ditambahkan kemudian tanpa mengubah hasil interpreter.
import {Dwell,runRobot} from '../core.js';
export const puzzle={size:4,start:{x:0,y:3,dir:0},goal:{x:2,y:1},walls:[{x:1,y:2}]};
export function createGame(){
const stable=new Dwell(550),neutral=new Dwell(350);let armed=false,program=[],result=null,done=false,message='Tampilkan telapak terbuka untuk siap.';
const actions={Pointing_Up:'F',Victory:'R',Closed_Fist:'undo',Thumb_Down:'clear',Thumb_Up:'run'};
return{kind:'gesture',title:'Kode Gestur',itemKey:'robot-grid-1',
instruction:'Telapak terbuka di antara perintah. Telunjuk: maju; V: kanan; kepal: hapus; jempol bawah: kosongkan; jempol atas: jalankan.',
update(o){if(done)return;const g=o.gesture?.score>=.65?o.gesture.categoryName:null;
if(g==='Open_Palm'){
stable.reset();if(neutral.step('neutral',o.now)){armed=true;message='Siap menerima satu perintah.';}return;
}neutral.reset();
if(!armed||!actions[g]){stable.reset();return;}
if(stable.step(g,o.now)){
armed=false;const a=actions[g];message='Buka telapak sebelum perintah berikutnya.';
if(a==='undo')program.pop();else if(a==='clear')program=[];
else if(a==='run'){
result=runRobot(program,puzzle);done=true;return{answer:{program:[...program],reachedGoal:result.success},score:result.success?100:0,feedback:result.reason+'. Urutan contoh: F F R F F.'};
}else if(program.length<24)program.push(a);else message='Batas 24 perintah. Hapus atau jalankan.';
}
},view(){return{program,puzzle,result,message};}
};
}
Langkah 7. Buat penjelasan yang membantu debugging
Jika robot menabrak, jelaskan bahwa langkah menyentuh batas atau penghalang. Jika semua perintah habis sebelum tujuan, tampilkan keadaan itu dengan bahasa sederhana. Jika berhasil, sebut bahwa urutan mencapai tujuan. Skor contoh 100 atau 0 berasal dari hasil puzzle, bukan confidence tangan.
Simpan array perintah dan reachedGoal sebagai ringkasan jawaban. Array kecil tersebut cukup untuk mereproduksi logika puzzle. Untuk platform kelas, server dapat menjalankan interpreter yang sama terhadap versi puzzle yang ditugaskan. Ini lebih bermakna daripada menerima klaim klien bahwa “robot berhasil” tanpa memeriksa urutannya.
Langkah 8. Uji solusi lengkap melalui kamera
Pilih Kode Gestur, mulai kamera, dan tampilkan telapak terbuka sampai siap. Tunjukkan telunjuk untuk F, buka telapak, lalu ulangi F. Tambahkan R memakai V, kemudian dua F lagi dengan kondisi netral di antaranya. Periksa antrean F F R F F sebelum menampilkan jempol atas.
Hasil yang diharapkan adalah lintasan mencapai tujuan, penjelasan berhasil, satu record lokal, dan kamera berhenti. Pada sesi baru, coba F R F untuk menabrak penghalang. Coba pula menjalankan program kosong. Kondisi salah harus mempunyai hasil yang dapat dijelaskan, bukan membuat halaman macet.
Langkah 9. Uji latch dan perbaikan antrean
| Gejala | Bagian yang diperiksa | Tindakan |
|---|---|---|
| Menahan telunjuk menambah banyak F | Kunci penerimaan | Terima sekali, wajibkan Open_Palm sebelum berikutnya |
| Gestur mudah tertukar | Skor, waktu tahan, panduan posisi | Periksa kategori aktual dan jangan menerima satu frame |
| Robot bergerak saat menyusun | Pemisahan editor dan interpreter | Evaluasi hanya setelah perintah run |
| Program panjang membekukan UI | Batas jumlah perintah | Batasi antrean dan langkah interpreter |
| Nilai berbeda pada program yang sama | Ketergantungan pada kamera di evaluator | Gunakan fungsi deterministik dari program dan puzzle |
Langkah 10. Tambahkan level dari data, lalu evaluasi pembelajaran
Pindahkan ukuran grid, awal, tujuan, penghalang, dan batas program ke data puzzle berversi. Validasi bahwa posisi awal serta tujuan berada di dalam papan dan tidak menimpa penghalang. Buat solusi acuan untuk setiap level, tetapi jangan mengirim solusi sebagai data publik jika tujuan tugas mengharuskan peserta menyusunnya sendiri.
Prompt pengembangan: “Tambahkan tiga puzzle dari data dan animasi lintasan yang mengikuti hasil runRobot. Pertahankan daftar perintah F/R, batas langkah, penolakan perintah asing, serta Open_Palm di antara input. Sertakan fixture solusi, tabrakan, program kosong, dan program terlalu panjang. Pisahkan nilai puzzle dari confidence gesture.”
Bab selesai apabila: satu gesture menghasilkan satu perintah, koreksi antrean bekerja, solusi acuan mencapai tujuan, tabrakan ditolak, dan program yang disimpan dapat dievaluasi ulang tanpa kamera.
Menyelesaikan proyek: dari enam prototipe ke aplikasi yang bisa diserahkan
Sesudah satu game dapat dimainkan, pekerjaan berikutnya bukan langsung menambah dekorasi. Pastikan orang lain dapat memasang, menjalankan, mengulang, dan memahami hasilnya. Bagian ini menghubungkan keenam tutorial dengan pembangunan platform. Ia juga menjelaskan titik ketika contoh lokal selesai dan ketika backend perlu mulai dikerjakan.
Langkah 1. Jalankan verifikasi proyek dari folder yang benar
Setelah keenam modul game selesai ditulis, buat file tests/core.test.js dengan kode berikut. Pengujian ini memakai modul yang sama dengan permainan, tanpa membutuhkan kamera.
import test from 'node:test';
import assert from 'node:assert/strict';
import {angle,Pinch,Dwell,traceScore,sampleTriangle,runRobot} from '../src/core.js';
import {createGame as pinchGame} from '../src/games/pinch.js';
import {createGame as angleGame} from '../src/games/angle.js';
import {createGame as laneGame} from '../src/games/lane.js';
import {createGame as objectGame} from '../src/games/objects.js';
import {createGame as codeGame,puzzle} from '../src/games/code.js';
test('angle uses a valid plane and rejects zero-length vectors',()=>{
assert.equal(angle({x:0,y:0},{x:1,y:0},{x:1,y:1}),90);
assert.equal(angle({x:0,y:0},{x:0,y:0},{x:1,y:1}),null);
});
test('pinch requires open hand after loss and has separate release threshold',()=>{
const p=new Pinch();assert.equal(p.step(.2),'idle');p.step(.8);
assert.equal(p.step(.2),'start');assert.equal(p.step(.4),'hold');
assert.equal(p.step(.7),'end');assert.equal(p.step(undefined),'lost');
assert.equal(p.step(.2),'idle');
});
test('dwell emits once and resets after missing input',()=>{
const d=new Dwell(100);assert.equal(d.step('a',0),false);assert.equal(d.step('a',100),true);
assert.equal(d.step('a',200),false);d.step(null,300);assert.equal(d.step('a',400),false);
});
test('trace scoring requires coverage and is not improved by repeated stationary points',()=>{
const t=sampleTriangle();assert.ok(traceScore([...t,t[0]],t).score>90);
assert.equal(traceScore(Array(100).fill(t[0]),t).score,0);
});
test('robot evaluates known solution, rejects walls and invalid instructions',()=>{
assert.equal(runRobot(['F','F','R','F','F'],puzzle).success,true);
assert.equal(runRobot(['F','R','F'],puzzle).success,false);
assert.equal(runRobot(['execute-code'],puzzle).success,false);
});
function hand(x,y,ratio){const p=Array.from({length:21},()=>({x:.5,y:.5}));p[0]={x:.4,y:.5};p[9]={x:.5,y:.5};p[8]={x:1-x,y};p[4]={x:1-x+.1*ratio,y};return{hand:p,width:1000,height:1000};}
test('pinch drag submits only on explicit release, not tracking loss',()=>{
const g=pinchGame();g.update(hand(.2,.3,.8));g.update(hand(.2,.3,.2));
g.update(hand(.5,.76,.2));const r=g.update(hand(.5,.76,.8));assert.equal(r.score,100);
assert.equal(g.update(hand(.5,.76,.8)),undefined);
const h=pinchGame();h.update(hand(.2,.3,.8));h.update(hand(.2,.3,.2));assert.equal(h.update({}),undefined);
});
function poseX(x){const p=Array.from({length:33},()=>({x:1-x,y:.5,visibility:1}));return{pose:p,width:1000,height:1000};}
test('lane calibrates and does not answer automatically at neutral',()=>{
const g=laneGame();let now=0;
for(const x of [.5,.3,.7])for(let i=0;i<30;i++){g.update({...poseX(x),now});now+=100;}
now+=3000;for(let i=0;i<15;i++){assert.equal(g.update({...poseX(.5),now}),undefined);now+=100;}
g.update({...poseX(.3),now});now+=100;
let r;for(let i=0;i<11;i++){r=g.update({...poseX(.5),now})||r;now+=100;}assert.equal(r.score,100);
});
test('arm target requires a fresh continuous hold after tracking loss',()=>{
const g=angleGame(),o=poseX(.5);o.pose[12]={x:.2,y:.2,visibility:1};o.pose[14]={x:.4,y:.2,visibility:1};o.pose[16]={x:.4,y:.4,visibility:1};
g.update({...o,now:0});g.update({now:1000});assert.equal(g.update({...o,now:1200}),undefined);assert.equal(g.update({...o,now:2400}).score,100);
});
test('object acceptance resets after an unsupported or missing target',()=>{
const g=objectGame(),cup={objects:[{class:'cup',score:.9}]};g.update({...cup,now:0});g.update({objects:[],now:1000});
assert.equal(g.update({...cup,now:1600}),undefined);assert.equal(g.update({...cup,now:3100}).score,100);
});
test('gesture command stays latched until open palm, loss alone does not rearm',()=>{
const g=codeGame();const send=(name,now)=>g.update({now,gesture:{categoryName:name,score:.9}});
send('Open_Palm',0);send('Open_Palm',400);send('Pointing_Up',500);send('Pointing_Up',1100);
send('Pointing_Up',1800);g.update({now:1900});send('Pointing_Up',2500);send('Pointing_Up',3100);
assert.deepEqual(g.view().program,['F']);send('Open_Palm',3200);send('Open_Palm',3600);send('Pointing_Up',3700);send('Pointing_Up',4300);
assert.deepEqual(g.view().program,['F','F']);
});
Pada terminal dalam folder game-kamera buatan Anda, jalankan perintah berikut satu per satu:
npm test
npm run assets
npm run build
npm run preview
Tes memakai fixture sintetis untuk perilaku yang tidak memerlukan kamera. Saat revisi ini disusun, sepuluh tes logika lulus dan build Vite berhasil. Tes mencakup sudut tidak valid, pinch yang harus dibuka setelah kehilangan tangan, dwell satu kali, cakupan lintasan, interpreter robot, drag yang dinilai saat lepas, kalibrasi jalur, reset target sudut, reset label objek, dan kunci perintah gestur.
Hasil tersebut membuktikan perilaku logika pada fixture serta keberhasilan membangun berkas frontend. Ia belum membuktikan kualitas detector melalui kamera fisik, keberhasilan setiap model pada semua perangkat, atau kesiapan produk kelas. Gunakan matriks manual setelahnya. Build juga memberi peringatan ukuran chunk TensorFlow.js; catat untuk pengukuran pemuatan, bukan menghapus peringatan tanpa menilai dampak pada perangkat sasaran.
Langkah 2. Lakukan pemeriksaan manual keenam game
| Game | Skenario berhasil | Skenario gagal yang wajib dicoba | Bukti hasil |
|---|---|---|---|
| Jepit Jawaban | 2/4 dilepas di kotak | Tangan hilang saat drag; lepas di luar | Satu optionId, feedback pecahan |
| Gambar Bentuk | Tiga sisi dijiplak | Titik diam; stroke terputus | Ketepatan dan cakupan terpisah |
| Sudut Lengan | Target ditahan | Satu titik tertutup; keluar toleransi | Target dan sudut proyeksi |
| Jalur Jawaban | Kalibrasi, aktifkan, pilih B | Diam netral; menyeberang tanpa tahan | Pilihan yang dikonfirmasi |
| Berburu Benda | Label cup stabil | Benda lain; cup hanya sesaat | Label target diterima sekali |
| Kode Gestur | F F R F F dijalankan | Menahan gesture; tabrakan | Program dan hasil interpreter |
Untuk setiap baris, catat browser, sistem operasi, kamera, kondisi cahaya, tanggal, serta versi aplikasi. Gunakan status lulus, gagal, atau belum diuji. Jangan mengubah belum diuji menjadi lulus hanya karena modul serupa bekerja di laptop lain. Catat juga apakah tombol berhenti mematikan track dan apakah riwayat tampil setelah halaman dimuat ulang pada browser yang sama.
Langkah 3. Tentukan materi mana yang akan masuk database
Pindahkan data lokal ke entitas materi berversi. Satu identitas aktivitas dapat mempunyai beberapa revisi. Penugasan kelas menunjuk revisi tertentu agar hasil lama tetap mempunyai konteks. Contoh SQL yang ditulis langsung pada bagian database memberi fondasi relasi; ia belum menyertakan semua kolom operasional maupun penerapan otorisasi server.
Berikut pemetaan yang perlu dibuat editor materi dan evaluator. Kolom rubrik merupakan rancangan untuk platform, bukan field yang seluruhnya sudah tersedia pada prototipe.
| Game | Konfigurasi publik untuk memainkan | Rubrik atau aturan evaluasi | Bukti jawaban yang sesuai tujuan |
|---|---|---|---|
| Jepit Jawaban | Soal, ID dan label opsi, area drop | Opsi benar, penjelasan opsi | optionId |
| Gambar Bentuk | Template dan instruksi stroke | Versi evaluator, toleransi, batas minimum | Sampel lintasan terbatas bila evaluator server diperlukan |
| Sudut Lengan | Target, sisi, waktu tahan | Toleransi latihan dan jenis hasil | Ringkasan latihan gerak; item konsep disimpan terpisah |
| Jalur Jawaban | Soal, opsi, instruksi kalibrasi | Opsi benar dan penjelasan | optionId |
| Berburu Benda | Kosakata, label target, instruksi | Gate target; rubrik soal konsep bila ditambahkan | Label latihan; jawaban konsep terpisah |
| Kode Gestur | Grid, posisi awal, tujuan, penghalang | Interpreter dan batas langkah | Array program |
Jangan langsung menyalin answer.precision dari prototipe sebagai nilai tepercaya server. Jika server perlu menghitung ulang lintasan, kirim sampel jalur yang dibatasi dan validasi ukurannya. Jika hanya laporan latihan lokal, nyatakan asal metriknya. Demikian pula, server dapat mengevaluasi program robot dari array instruksi, tetapi ringkasan sudut atau label kamera tidak dengan sendirinya membuktikan kejadian fisik di depan kamera.
Langkah 4. Ganti percobaan lokal dengan percobaan dari server
Pada prototipe, attemptId dibuat lokal untuk mengenali satu hasil. Pada platform, frontend harus memulai melalui POST /api/assignments/:id/attempts. Server memeriksa akun, kelas, jadwal, kuota, dan versi materi, kemudian mengembalikan attempt ID yang sah serta item publik. Jangan mengirim attempt ID lokal seolah-olah sudah tercatat di database sekolah.
Setelah respons mulai diterima, frontend menyimpan konteks sesi: attempt ID server, daftar item, versi, dan kebijakan mengulang. Nilai itemKey lokal dipetakan ke UUID item milik versi tersebut. UI memakai DTO publik tanpa seluruh record database. Identitas organisasi dan pemilik attempt tetap diperiksa server pada setiap operasi.
Langkah 5. Bentuk event respons dengan identitas yang stabil
Saat game selesai, buat satu event ID dan pertahankan ID tersebut ketika melakukan retry. Jangan membuat UUID baru pada setiap kegagalan jaringan karena server kemudian tidak dapat mengenali kiriman ulang. Contoh bentuk respons untuk Jepit Jawaban:
{
"eventId": "7e8d2204-2fe1-4d5e-b566-6c4797d292b8",
"itemId": "6f4b4dd6-9c1d-4fab-a71e-c6de8ef17803",
"tryNo": 1,
"answer": {"optionId": "a"},
"inputMode": "camera",
"helpCount": 0,
"clientElapsedMs": 14320
}
Kirim ke POST /api/attempts/:id/responses. Skor lokal dan feedback dari controller contoh tidak menjadi sumber kebenaran server. Server menggunakan versi materi attempt untuk menilai. Untuk menampilkan feedback dari server, perbarui UI setelah respons diterima; jika belum terkirim, beri label hasil lokal atau menunggu sinkronisasi sesuai desain produk.
Langkah 6. Terapkan transaksi respons dan finalisasi
Urutan service respons yang disarankan adalah memverifikasi identitas, membuka transaksi, mengunci attempt yang relevan, memeriksa status, memastikan item milik versi, memeriksa event duplikat, mengevaluasi jawaban, menyimpan, lalu mengembalikan hasil konsisten. Semua endpoint yang menulis hasil atau melakukan submit harus mengikuti urutan kunci yang sama untuk mencegah kondisi balapan yang tidak ditangani.
Sketsa berikut adalah alur service yang perlu diimplementasikan pada backend pilihan, bukan implementasi backend NestJS lengkap:
Mulai transaksi
Baca attempt dan kunci barisnya
Periksa pemilik dan organisasi
Jika eventId sudah ada:
cocokkan attempt, item, tryNo, dan payload
kembalikan hasil lama atau konflik
Jika attempt tidak aktif: tolak jawaban baru
Pastikan item merupakan anggota versi attempt
Validasi bentuk jawaban untuk jenis game
Hitung hasil dengan evaluator versi materi
Simpan respons dengan unique constraint
Akhiri transaksi
Sketsa ini memuat urutan operasi, bukan diagram arsitektur atau klaim bahwa autentikasi sudah dibuat. PostgreSQL menyediakan constraint dan kontrol transaksi yang menjadi referensi implementasinya. PostgreSQL (diakses 2026/10), Constraints PostgreSQL (diakses 2026/10), Transaction isolation
Submit memakai transaksi yang mengunci attempt, menghitung ringkasan dari respons yang diterima, dan mengubah status. Jika suatu event lama dikirim ulang sesudah submit, tentukan bahwa hasil lama dapat dikembalikan setelah otorisasi serta kecocokan payload diverifikasi. Jawaban baru pada attempt yang sudah selesai tetap ditolak. Kontrak ini perlu diuji secara paralel, bukan hanya dengan klik satu tombol secara berurutan.
Langkah 7. Pilih mode offline sesuai kebutuhan sebenarnya
localStorage pada proyek yang Anda buat menyimpan hasil pada perangkat. Ia belum merupakan antrean sinkronisasi, tidak terikat akun, dan tidak memulihkan tugas dari server. Untuk kelas yang harus menahan putus koneksi, rancang antrean IndexedDB dengan identitas pengguna, organisasi, attempt, versi, event ID, serta status kiriman. Kemampuan penyimpanan tersebut didokumentasikan terpisah pada IndexedDB. MDN (diakses 2026/10), IndexedDB API
Tentukan tindakan pada logout dan perangkat bersama. Data akun lama tidak boleh dikirim sebagai akun baru. Tampilkan tersimpan lokal, sedang dikirim, terkirim, atau ditolak secara jelas. Jangan menyembunyikan penolakan server di balik pesan sukses karena murid sudah melihat animasi kemenangan. Pengalaman belajar dan konsistensi data harus diselesaikan bersama.
Langkah 8. Buat laporan guru yang sesuai jenis aktivitas
Jepit dan Jalur dapat melaporkan jawaban konsep. Gambar Bentuk melaporkan latihan lintasan. Sudut Lengan melaporkan keberhasilan target gerak dan, bila nanti ditambahkan, item konsepnya sendiri. Berburu Benda membedakan target deteksi dan soal kosakata. Kode Gestur melaporkan urutan serta keberhasilan puzzle. Jangan merata-ratakan semuanya menjadi satu angka kemampuan tanpa rubrik yang dijelaskan.
Laporan perlu menunjukkan jumlah percobaan, bantuan, status selesai, serta gangguan teknis secara terpisah. Pilih apakah ringkasan memakai percobaan pertama atau terbaik dan nyatakan alasannya. Guru harus dapat memeriksa konteks versi materi ketika meninjau hasil. Hak akses mengikuti kelas, bukan hanya keberadaan tautan laporan.
Langkah 9. Terbitkan frontend dengan aset yang utuh
Sebelum build, jalankan script assets dan pastikan tiga model berada di public. Vite menyertakan aset publik ke keluaran distribusi menurut pola proyeknya; periksa hasil build secara nyata. Deploy isi dist pada hosting HTTPS yang sesuai. Buka aplikasi dari domain sebenarnya dan periksa request model, WASM, serta perilaku izin kamera. Langkah build lokal tidak otomatis menguji konfigurasi server produksi. Vite (2026/10), Guide
Untuk API terpisah, tetapkan origin yang diizinkan, mekanisme login, konfigurasi cookie atau token, dan lingkungan database. Berkas SQL perlu dimasukkan ke sistem migrasi proyek serta diuji, bukan dijalankan sembarang pada database yang sudah memiliki tabel. Lakukan backup dan latihan restore sebelum perubahan data penting.
Langkah 10. Serahkan dengan bukti dan batas yang jelas
Serah terima minimum mencakup cara menjalankan, lokasi model, versi dependensi, hasil tes otomatis, matriks uji kamera, daftar masalah, prosedur berhenti, serta cara menghapus hasil lokal. Untuk platform kelas, tambahkan pemilik akun pengelola, runbook backup, pemulihan, dan jalur dukungan. Jangan meminta guru menebak apakah kegagalan terjadi pada jaringan, kamera, atau jawaban murid.
Kode dasar yang ditulis di artikel ini sudah melewati tes logika dan build pada verifikasi sebelumnya. Pengujian kamera fisik serta kelengkapan model pada perangkat pembaca tetap harus dilakukan mengikuti langkah tiap bab. Setelah bukti tersebut terkumpul, pengembangan platform dapat dilanjutkan menggunakan rincian perencanaan, database, API, prompt, dan operasi pada bagian berikut.
Perencanaan produk: menulis spesifikasi sebelum memilih teknologi
Sebelum membuat repository, tentukan masalah belajar yang ingin diselesaikan. Kamera adalah metode interaksi, sedangkan produk pembelajarannya perlu memiliki sasaran yang dapat diperiksa. “Peserta dapat mengenali pecahan senilai dari tiga pilihan” memberikan arah lebih jelas daripada “membuat website belajar yang canggih”.
Tuliskan spesifikasi singkat yang memuat pengguna, lingkungan penggunaan, kompetensi, aktivitas, ukuran keberhasilan, dan batas versi awal. Dokumen ini tidak perlu panjang, tetapi setiap kalimat harus membantu keputusan. Jika peserta memakai ponsel bersama, misalnya, mekanisme akun dan pergantian sesi akan berbeda dari kelas yang setiap siswanya memiliki laptop.
Tentukan tiga peran pengguna
Untuk rancangan platform ini, peserta berfokus pada membuka tugas, mengatur kamera, bermain, dan memahami feedback. Guru berfokus pada memilih materi, membagikan tugas, meninjau jawaban, serta mengetahui bagian yang perlu diajarkan kembali. Pengelola berfokus pada keanggotaan, penerbitan konten, konfigurasi, dan operasional.
Jangan memaksa ketiga peran memakai halaman yang sama. Layar peserta sebaiknya menampilkan instruksi singkat dan satu tindakan berikutnya. Layar guru memerlukan konteks kelas dan penjelasan laporan. Pengelola memerlukan kontrol perubahan yang tidak perlu terlihat ketika peserta sedang menggambar bentuk.
Peran tidak selalu melekat secara global pada seseorang. Orang yang menjadi guru di satu organisasi bisa menjadi peserta di organisasi lain. Karena itu, perencanaan hak akses sebaiknya mempertimbangkan konteks organisasi dan kelas, bukan sekadar sebuah kolom role yang berlaku untuk seluruh aplikasi.
Susun alur peserta dari awal sampai selesai
Alur peserta yang diusulkan dimulai dari membuka tautan tugas, membaca tujuan singkat, melakukan pengecekan perangkat, memberi izin kamera, mengikuti kalibrasi, menyelesaikan satu latihan contoh, lalu masuk ke materi. Setelah selesai, peserta melihat hasil, alasan jawaban, dan kesempatan mengulang bagian yang belum dipahami.
Latihan contoh harus mudah dan tidak memengaruhi nilai materi. Fungsinya memastikan peserta memahami kontrol. Jika pengguna tidak dapat mengambil kartu saat tutorial, aplikasi belum boleh menganggap kesulitan berikutnya sebagai masalah pemahaman matematika.
Setiap layar perlu mempunyai jalan keluar. Ketika kamera ditolak, tampilkan petunjuk dan opsi input alternatif. Ketika model belum selesai dimuat, tampilkan status yang masuk akal. Ketika peserta ingin berhenti, simpan progres yang diperlukan dan hentikan kamera. Alur gagal merupakan bagian produk, bukan pekerjaan tambahan menjelang rilis.
Bedakan MVP, pilot, dan produk kelas
MVP merupakan versi minimum untuk membuktikan satu aktivitas dapat digunakan. Pada tahap ini, materi lokal, satu model, dan hasil sederhana sudah cukup. Pilot berarti beberapa pengguna nyata mencoba aktivitas dalam kondisi yang mewakili tujuan produk. Produk kelas memerlukan akun, tugas, laporan, dukungan pengguna, dan pengelolaan data yang lebih tertib.
Ketiga tahap tersebut memiliki definisi selesai berbeda. MVP belum harus menyediakan dashboard organisasi. Pilot belum berarti semua browser didukung. Produk kelas tidak boleh mengandalkan asumsi bahwa pengguna akan memperbaiki sendiri kamera yang gagal.
Untuk enam game, mulailah dari satu fondasi kamera dan satu game yang lengkap. Setelah fondasi lolos uji, tambahkan game lain sebagai modul. Pendekatan ini membuat temuan pada kalibrasi dan kualitas input dapat diperbaiki di satu tempat.
Buat user story yang dapat diuji
User story perlu menyebut tindakan dan hasil yang dapat diamati. Contohnya: “Sebagai peserta, saya dapat mengambil kartu jawaban dengan pinch dan melepasnya ke kotak tujuan tanpa jawaban dikirim ketika tangan keluar frame.” Kalimat tersebut langsung menghasilkan skenario pengujian.
Untuk guru: “Saya dapat melihat jawaban pertama peserta dan membedakannya dari jawaban setelah bantuan.” Untuk pengelola: “Saya dapat menerbitkan revisi soal tanpa mengubah arti hasil pengerjaan yang sudah tersimpan.” Kedua kebutuhan ini memengaruhi struktur database sejak awal.
Tambahkan kriteria penerimaan negatif. Fitur dianggap benar bukan hanya ketika jawaban berhasil dikirim, tetapi juga ketika sistem menolak jawaban untuk tugas milik orang lain, tidak menambahkan perintah ganda, dan tidak kehilangan progres akibat pengiriman ulang.
Prioritaskan berdasarkan risiko
Kerjakan ketidakpastian terbesar lebih dahulu. Jika targetnya HP kelas menengah, uji pemuatan model dan latensi sebelum mendesain animasi rumit. Jika materi memerlukan pengenalan benda khusus, uji kategori tersebut sebelum membangun katalog ratusan soal.
Buat daftar risiko dengan pemilik dan tindakan. Contoh risiko teknis adalah koordinat pointer salah setelah orientasi berubah. Risiko konten adalah dua pilihan sama-sama benar. Risiko operasional adalah guru tidak memahami perbedaan belum mengerjakan dan gagal sinkronisasi. Masing-masing membutuhkan tindakan berbeda.
Prioritas yang disarankan adalah validitas materi, keandalan input, penyimpanan hasil, kemudian pengayaan visual. Urutan ini bukan larangan membuat desain menarik. Tujuannya memastikan animasi mendukung pengalaman yang sudah memiliki dasar yang benar.
Memilih framework melalui keputusan yang terdokumentasi
Jalur praktik dalam panduan ini memakai JavaScript biasa dan Vite. Untuk platform kelas, rancangan acuan menggunakan React, TypeScript, dan Vite untuk antarmuka; modul deteksi berjalan di browser; backend dapat memakai NestJS; database menggunakan PostgreSQL. Pilihan ini merupakan usulan arsitektur, bukan satu-satunya kombinasi yang dapat bekerja.
Keputusan stack sebaiknya ditulis dalam catatan singkat: masalah yang dihadapi, pilihan yang dibandingkan, alasan pilihan, serta kondisi yang dapat membuat pilihan berubah. Catatan semacam ini membantu ketika proyek bertambah besar atau anggota tim berganti.
React dan Vite untuk portal aktivitas
React sesuai sebagai lapisan menu, pengaturan, hasil, dan dashboard pada rancangan ini. Vite menjadi alat pengembangan serta build. Rendering game tidak harus dilakukan melalui pembaruan state React setiap frame; Canvas atau Phaser dapat mengelola loop visualnya sendiri.
Hindari memasukkan seluruh landmark ke state global yang memicu render UI terus-menerus. Status yang perlu tampil, seperti game dijeda atau soal berikutnya, berbeda dari data frame yang berubah sangat cepat. Pisahkan keduanya melalui adapter dan event agar dashboard tidak ikut bekerja pada setiap inferensi.
Gunakan TypeScript untuk kontrak antarmodul. Tipe tidak menggantikan validasi data dari jaringan, tetapi membantu membuat perbedaan antara frame, observasi, tindakan, dan jawaban terlihat dalam kode. Jika sebuah fungsi menerima semua jenis objek tanpa batas, kesalahan arsitektur lebih sulit ditemukan.
Kapan mempertimbangkan Next.js
Next.js dapat dipertimbangkan bila portal juga mempunyai halaman publik, katalog materi, serta kebutuhan rendering server yang menyatu dengan aplikasi. Dokumentasi Server and Client Components membedakan kode server dan komponen yang memerlukan interaktivitas atau API browser.
Pada rancangan kamera, akses media dan loop interaksi ditempatkan pada sisi klien. Halaman informasi atau pengambilan data awal dapat mengikuti kemampuan server framework. Jangan menganggap pemilihan Next.js berarti semua pemrosesan kamera harus pindah ke server.
Jika tim sudah nyaman dengan Vite dan API terpisah, tidak perlu mengganti stack semata-mata untuk mendapatkan nama framework lain. Nilai keputusan ditentukan oleh struktur produk, keahlian tim, dan kemudahan pemeliharaan.
Kapan mempertimbangkan Laravel
Laravel relevan ketika organisasi sudah mempunyai akun, administrasi, atau layanan yang dibangun dengan PHP. Modul game dapat tetap berupa frontend JavaScript dan menggunakan API dari backend yang sudah ada.
Dokumentasi Laravel Sanctum menjelaskan pilihan autentikasi untuk SPA dan token API. Gunakan pendekatan autentikasi yang sesuai topologi aplikasi, bukan menyalin token ke penyimpanan browser tanpa mempertimbangkan alur login, masa berlaku, dan pencabutan.
Jika Laravel dipilih, tentukan siapa pemilik session, endpoint tugas, dan validasi jawaban. Hindari dua sistem akun yang berjalan sendiri-sendiri hanya karena frontend dan backend dibuat pada waktu berbeda.
NestJS sebagai contoh backend terstruktur
Untuk tim TypeScript, NestJS dapat menjadi pilihan backend pada blueprint ini. Modul dapat dipisahkan menjadi identitas, kelas, katalog aktivitas, tugas, pengerjaan, dan laporan. Dokumentasi authentication serta validation menjadi referensi implementasi mekanisme dasar.
Framework tidak otomatis menentukan otorisasi bisnis. Endpoint tetap perlu memastikan guru berhak atas kelas yang diminta dan peserta berhak atas sesi pengerjaan. Validasi bentuk payload juga berbeda dari validasi bahwa jawaban memang milik soal dalam versi tugas tersebut.
Canvas, Phaser, dan library deteksi
Canvas cukup untuk gambar jalur, busur sudut, dan kartu sederhana. Phaser membantu ketika terdapat scene, karakter, level, dan interaksi objek yang lebih banyak. Gunakan satu pemilik loop game agar React dan engine tidak saling memperbarui posisi objek yang sama.
MediaPipe menjadi jalur tangan, pose, dan kategori gestur. TensorFlow.js digunakan untuk model objek atau classifier yang membutuhkannya. Jangan memuat dua runtime besar pada awal halaman jika peserta hanya membuka game tangan.
Tulis matriks dependensi per game. Dengan begitu, pemuatan bersifat terarah dan perubahan library dapat diuji pada modul yang memang bergantung padanya.
Mendesain database dari hubungan data, bukan dari tampilan
Database perlu menggambarkan hubungan antara peserta, materi, penugasan, dan bukti pengerjaan. Sebuah tabel score dengan nama pengguna dan angka akhir tidak cukup ketika guru ingin mengetahui soal mana yang salah atau ketika materi direvisi.
Blueprint berikut merupakan rancangan aplikasi. PostgreSQL digunakan sebagai contoh mesin database. Dokumentasi constraints menjelaskan primary key, unique constraint, foreign key, dan check constraint yang dapat digunakan untuk menjaga konsistensi data.
Organisasi, pengguna, dan keanggotaan
Tabel organizations menyimpan institusi atau ruang kerja. Tabel users menyimpan identitas akun minimum dan referensi ke penyedia autentikasi. Tabel organization_memberships menghubungkan keduanya serta menyimpan peran pada organisasi.
Jangan menyimpan password mentah. Pada rancangan yang memakai penyedia identitas, database aplikasi cukup menyimpan pasangan provider dan subject yang unik. Detail kredensial mengikuti sistem autentikasi yang dipilih.
Keanggotaan organisasi perlu mempunyai status. Pengguna yang keluar dari sekolah tidak selalu harus dihapus dari seluruh riwayat hasil secara langsung, tetapi akses aktifnya harus berhenti sesuai kebijakan produk. Pisahkan keputusan retensi catatan dengan keputusan boleh login atau membaca data.
Kelas dan keanggotaan kelas
Tabel classrooms menyimpan nama kelas dan organisasi pemiliknya. Tabel classroom_memberships menentukan pengguna yang menjadi guru atau peserta pada kelas. Satu pengguna dapat tergabung dalam lebih dari satu kelas tanpa membuat akun baru.
Hubungan ini penting untuk otorisasi. Memiliki peran guru dalam suatu organisasi tidak selalu berarti boleh melihat semua kelas. Tentukan apakah model aksesnya guru kelas, guru seluruh organisasi, atau kombinasi dengan pengelola.
Kelas yang diarsipkan sebaiknya tidak menerima tugas baru, tetapi laporan lama tetap dapat dibaca oleh pengguna berwenang. Pilihan mengarsipkan memberi jalur operasional yang lebih aman daripada menghapus kelas beserta hasilnya secara tidak sengaja.
Aktivitas dan versi aktivitas
Tabel activities menjadi identitas logis sebuah materi, misalnya “Pecahan Senilai Dasar”. Tabel activity_versions menyimpan revisi yang spesifik: jenis game, petunjuk, konfigurasi input, model profile, dan nomor versi.
Bedakan draft dengan published. Draft dapat diedit. Versi yang sudah diterbitkan dan dipakai oleh tugas harus diperlakukan sebagai snapshot tetap. Koreksi materi dilakukan dengan membuat versi baru, bukan mengubah diam-diam kunci jawaban pada versi lama.
Tugas menunjuk versi, bukan sekadar identitas aktivitas. Dengan demikian, peserta yang mengerjakan pada waktu berbeda tetap memakai materi yang ditetapkan guru. Jika guru ingin mengganti versi pada tugas aktif, keputusan migrasinya perlu jelas.
Soal, opsi, dan kunci jawaban
Tabel activity_items menyimpan soal pada versi tertentu, urutan, tag konsep, serta konfigurasi yang diperlukan game. Payload tampilan dapat berupa JSON untuk perbedaan jenis game, tetapi identitas, relasi versi, dan urutan sebaiknya tetap eksplisit.
Kunci dan aturan penilaian dapat diletakkan pada kolom atau tabel yang hanya dibaca service penilaian. Jangan mengirim seluruh record database langsung sebagai respons API. Gunakan objek respons yang sengaja memilih field yang boleh dilihat peserta.
Untuk pilihan ganda, opsi memiliki ID stabil yang berbeda dari label tampilannya. Jawaban peserta menyimpan ID opsi. Hal ini mempermudah perubahan urutan tampilan tanpa mengubah makna jawaban.
Untuk game gambar atau sudut, konfigurasi soal memuat target dan rubrik, bukan hanya satu answerId. Jelaskan versi rubrik karena perubahan toleransi atau bobot dapat mengubah interpretasi hasil.
Penugasan dan sesi pengerjaan
Tabel assignments menghubungkan kelas dengan versi aktivitas. Simpan waktu buka, batas waktu bila ada, jumlah percobaan yang diizinkan, serta status publikasi. Batas waktu dapat kosong untuk latihan mandiri.
Tabel attempts menyimpan satu sesi pengerjaan peserta pada tugas. Field penting meliputi pemilik, versi aktivitas, waktu mulai, status, input mode, dan ringkasan hasil yang sudah disahkan server.
Jangan menentukan attempt dari kombinasi pengguna dan aktivitas saja. Peserta dapat mengulang, guru dapat membuat tugas baru, dan materi dapat mempunyai versi baru. Identitas attempt yang terpisah membuat riwayat tersebut tidak saling menimpa.
Status inti pada contoh SQL adalah active, submitted, dan abandoned. Status invalidated dapat ditambahkan jika kebijakan koreksi hasil membutuhkannya. Transisi harus diatur service. Browser tidak boleh mengubah status sesuka hati melalui field yang langsung dipetakan ke database.
Respons dan event teknis
Tabel responses menyimpan jawaban terhadap item, nomor percobaan pada item, hasil evaluasi, dan penggunaan bantuan. Tabel ingestion_events atau kunci idempotensi menyimpan identitas kiriman sehingga retry tidak menggandakan respons.
Pisahkan event teknis dari bukti jawaban. Kehilangan tracking dapat diakumulasikan menjadi durasi atau jumlah kejadian tanpa menyimpan seluruh landmark. Rancangan ini menjaga laporan tetap fokus pada pembelajaran.
Untuk setiap respons, simpan item dan versi yang dinilai. Respons tidak boleh menunjuk item dari materi lain hanya karena ID tersebut valid. Gunakan relasi gabungan dan validasi service untuk menjaga kesesuaian tersebut.
Model dan aset
Tabel model_releases mencatat task, versi aset, checksum, sumber, dan status kelayakan pada perangkat uji. Tabel media_assets mencatat gambar materi atau audio yang memang menjadi bagian konten. Keduanya tidak perlu mencampur video kamera peserta.
Versi aplikasi, model, dan konfigurasi kalibrasi membantu penyelidikan masalah. Jika aktivasi palsu meningkat setelah pembaruan, tim dapat membandingkan sesi menurut kombinasi versi. Data tersebut lebih berguna daripada log yang hanya menyatakan “game gagal”.
Ringkasan kamus data
| Entitas | Identitas dan relasi | Field operasional penting |
|---|---|---|
| organizations | id | name, status |
| users | id, provider + subject | display_name, account_status |
| organization_memberships | organization_id + user_id | role, membership_status |
| classrooms | organization_id + id | title, archived_at |
| classroom_memberships | organization_id + classroom_id + user_id | classroom_role |
| activities | organization_id + id | title, created_by |
| activity_versions | organization_id + activity_id + revision | status, config, published_at |
| activity_items | organization_id + version_id + id | ordinal, public_payload, private_rubric |
| assignments | organization_id + id | classroom_id, version_id, dates |
| attempts | organization_id + id | assignment_id, user_id, version_id, status |
| responses | organization_id + event_id | attempt_id, item_id, try_no, answer, evaluation |
| attempt_technical_summaries | organization_id + attempt_id | lost_tracking_ms, error_counts, device_profile |
Nama tabel di atas merupakan rancangan. Sesuaikan konvensi proyek, tetapi pertahankan kejelasan hubungan dan kepemilikan datanya.
Constraint, index, dan transaksi
Gunakan foreign key untuk hubungan yang wajib valid, unique constraint untuk identitas gabungan, dan check untuk nilai yang masuk akal. Misalnya nomor percobaan harus positif dan waktu penutupan tidak lebih awal daripada pembukaan jika keduanya tersedia.
Check bukan tempat memeriksa semua aturan lintas tabel. Pemeriksaan keanggotaan aktif dan hak guru tetap membutuhkan service atau mekanisme otorisasi yang sesuai. Database menjaga struktur, sementara aturan produk membutuhkan konteks.
Index mengikuti pola query. Laporan kelas biasanya memfilter organization_id, classroom_id, assignment_id, dan status. Riwayat peserta sering memfilter pemilik dan waktu mulai. Jangan membuat index pada setiap kolom tanpa melihat kebutuhan pembacaan serta biaya penulisannya.
Simpan respons dan perbarui ringkasan attempt dalam satu transaksi jika keduanya harus konsisten. Ketika beberapa request dapat menutup atau menambah jawaban pada sesi yang sama, tentukan strategi penguncian atau concurrency control. Dokumentasi transaction isolation PostgreSQL menjadi dasar memahami perilaku transaksi; keputusan detailnya perlu diuji.
Pemisahan data antarorganisasi
Setiap tabel milik organisasi perlu mempunyai organization_id yang ditentukan oleh server dari konteks pengguna, bukan dipercaya begitu saja dari browser. Query harus memeriksa keanggotaan dan cakupan kelas.
PostgreSQL menyediakan row security policies. Fitur tersebut dapat menjadi lapisan tambahan bila tim mampu mengelola konteks pengguna dan connection pooling dengan benar. Mengaktifkan RLS saja belum merupakan rancangan otorisasi lengkap.
Jika memilih RLS, uji peran database yang digunakan aplikasi, perilaku pemilik tabel, serta pengecualian yang berlaku. Jika tidak memakai RLS, disiplin service dan pengujian isolasi harus dinyatakan jelas. Jangan mengklaim database aman untuk akses browser langsung hanya karena tabel memiliki organization_id.
Backup, penghapusan, dan perubahan skema
Tetapkan kebijakan retensi untuk akun, hasil belajar, dan ringkasan teknis. Durasi penyimpanan mengikuti konteks institusi; artikel ini tidak menetapkan angka hukum tertentu. Penghapusan akun perlu mempertimbangkan hubungan dengan laporan kelas dan ketentuan yang berlaku.
Backup belum terbukti berguna sampai restore pernah diuji. Jalankan pemulihan pada lingkungan terpisah dan periksa bahwa versi konten, respons, serta relasinya tetap konsisten. Catat waktu pemulihan agar rencana operasional tidak hanya berupa asumsi.
Perubahan skema mengikuti migration yang direview. Untuk perubahan besar, tambahkan struktur baru, lakukan backfill terukur, verifikasi, kemudian hentikan struktur lama. Hindari perubahan langsung yang membuat versi aplikasi sebelumnya tidak dapat berjalan saat rollback diperlukan.
Sebagai bacaan pendamping, lihat 50 tools website tanpa database dan server mahal. Kebutuhan penyimpanan hasil kelas pada rancangan ini tetap perlu dinilai tersendiri.
Arsitektur yang bisa dipakai bersama oleh keenam game
Membangun enam layanan kamera terpisah akan menyulitkan pemeliharaan. Pilihan yang lebih teratur adalah satu platform dengan layanan kamera bersama dan adapter deteksi sesuai aktivitas.
Alurnya dimulai dari kamera, dilanjutkan penjadwal frame, model deteksi, pemeriksaan kualitas, penerjemah interaksi, lalu aturan permainan. Hasil belajar masuk penyimpanan lokal atau backend bila dibutuhkan.
| Komponen | Tugas | Contoh keluaran |
|---|---|---|
| CameraManager | Membuka, mengganti, dan menghentikan kamera | Frame serta dimensi video |
| FrameScheduler | Memilih frame baru dan mencegah penumpukan | Frame dengan timestamp |
| VisionAdapter | Menjalankan model sesuai game | Landmark atau kategori |
| QualityGate | Memeriksa validitas dan keterlambatan hasil | Input valid atau alasan jeda |
| InteractionAdapter | Mengubah hasil deteksi menjadi tindakan | Grab, release, jalur, perintah |
| GameEngine | Mengelola ronde serta jawaban | State dan hasil |
| Renderer | Menampilkan aktivitas dan feedback | Canvas atau scene Phaser |
| ProgressStore | Menyimpan progres dan antrean sinkronisasi | Catatan jawaban |
Bagian yang paling sering terlupakan adalah penerjemah interaksi. Model mungkin mengetahui lokasi jari, tetapi aplikasi tetap harus menentukan kapan kartu diambil. Model mungkin mengeluarkan label gestur, tetapi aplikasi harus mencegah perintah ganda.
Gunakan kontrak tindakan yang sederhana, misalnya pointer, grab, release, lane, command, object, dan tracking-lost. Dengan kontrak tersebut, aturan soal tidak perlu mengetahui detail keluaran setiap library.
Satu sesi sebaiknya memuat model yang diperlukan saja. Saat berpindah dari game tangan ke game objek, tutup sesi lama dan inisialisasi adapter baru. Ini mempermudah pengendalian resource serta mencegah hasil deteksi dari aktivitas sebelumnya masuk ke game berikutnya.
Menulis skema SQL dari artikel
SQL berikut memperlihatkan contoh konkret inti relasional. Salin ke sistem migrasi proyek atau file SQL yang Anda buat sendiri untuk percobaan. Contoh memakai UUID yang dibuat aplikasi, foreign key gabungan, constraint respons unik, dan indeks beberapa jalur laporan. SQL ini merupakan rancangan yang belum dijalankan pada server PostgreSQL, bukan migrasi produksi siap pakai. Jalankan dahulu pada database pengujian kosong dan sesuaikan dengan migrator proyek.
Contoh SQL sengaja mencakup fondasi, bukan semua field usulan dalam kamus data. Registry model dan aset, kuota percobaan, status akun global, ringkasan nilai final, serta kebijakan retensi perlu ditambahkan sesuai kebutuhan. Nama activity_items mewakili entitas item dan attempt_technical_summaries mewakili ringkasan teknis. Status awal attempt pada SQL adalah active. API menggunakan camelCase, sedangkan kolom database menggunakan snake_case melalui pemetaan DTO.
Tidak ada konfigurasi login, GRANT, atau RLS otomatis dalam contoh SQL ini. Terapkan otorisasi aplikasi, pembatasan pengguna database, dan pengujian akses sebelum memakai data nyata. Foreign key membatasi hubungan data, tetapi tidak menentukan siapa yang boleh membacanya. Aturan versi terbit tidak boleh diedit dan finalisasi percobaan tetap perlu diimplementasikan dalam service dan transaksi; pertimbangkan proteksi tambahan di database sesuai model operasional.
-- Skema acuan, bukan migrasi produksi yang telah diuji pada server PostgreSQL.
-- UUID disediakan aplikasi. Jalankan pada database kosong untuk eksperimen.
-- Otorisasi, imutabilitas versi terbit, keanggotaan kelas saat mulai,
-- dan transisi status tetap harus diperiksa service/transaksi aplikasi.
-- Tidak menyimpan frame kamera, password, atau token sesi.
BEGIN;
CREATE TABLE organizations (
id uuid PRIMARY KEY,
name text NOT NULL CHECK (length(trim(name)) > 0),
created_at timestamptz NOT NULL DEFAULT now()
);
CREATE TABLE users (
id uuid PRIMARY KEY,
auth_provider text NOT NULL,
auth_subject text NOT NULL,
display_name text NOT NULL,
created_at timestamptz NOT NULL DEFAULT now(),
UNIQUE (auth_provider, auth_subject)
);
CREATE TABLE organization_memberships (
organization_id uuid NOT NULL REFERENCES organizations(id),
user_id uuid NOT NULL REFERENCES users(id),
role text NOT NULL CHECK (role IN ('student','teacher','admin')),
status text NOT NULL DEFAULT 'active' CHECK (status IN ('active','disabled')),
PRIMARY KEY (organization_id, user_id)
);
CREATE TABLE classrooms (
organization_id uuid NOT NULL REFERENCES organizations(id),
id uuid NOT NULL,
name text NOT NULL,
archived_at timestamptz,
PRIMARY KEY (organization_id, id)
);
CREATE TABLE classroom_memberships (
organization_id uuid NOT NULL,
classroom_id uuid NOT NULL,
user_id uuid NOT NULL,
role text NOT NULL CHECK (role IN ('student','teacher')),
PRIMARY KEY (organization_id, classroom_id, user_id),
FOREIGN KEY (organization_id, classroom_id)
REFERENCES classrooms(organization_id, id),
FOREIGN KEY (organization_id, user_id)
REFERENCES organization_memberships(organization_id, user_id)
);
CREATE TABLE activities (
organization_id uuid NOT NULL REFERENCES organizations(id),
id uuid NOT NULL,
title text NOT NULL,
created_by uuid NOT NULL,
PRIMARY KEY (organization_id, id),
FOREIGN KEY (organization_id, created_by)
REFERENCES organization_memberships(organization_id, user_id)
);
CREATE TABLE activity_versions (
organization_id uuid NOT NULL,
id uuid NOT NULL,
activity_id uuid NOT NULL,
revision integer NOT NULL CHECK (revision > 0),
game_type text NOT NULL CHECK (game_type IN
('pinch','draw_shape','arm_angle','answer_lane','object_hunt','gesture_code')),
status text NOT NULL DEFAULT 'draft' CHECK (status IN ('draft','published','retired')),
config jsonb NOT NULL DEFAULT '{}'::jsonb CHECK (jsonb_typeof(config) = 'object'),
published_at timestamptz,
PRIMARY KEY (organization_id, id),
UNIQUE (organization_id, activity_id, revision),
FOREIGN KEY (organization_id, activity_id) REFERENCES activities(organization_id, id),
CHECK (status = 'draft' OR published_at IS NOT NULL)
);
CREATE TABLE activity_items (
organization_id uuid NOT NULL,
version_id uuid NOT NULL,
id uuid NOT NULL,
ordinal integer NOT NULL CHECK (ordinal > 0),
public_payload jsonb NOT NULL CHECK (jsonb_typeof(public_payload) = 'object'),
private_rubric jsonb NOT NULL CHECK (jsonb_typeof(private_rubric) = 'object'),
PRIMARY KEY (organization_id, version_id, id),
UNIQUE (organization_id, version_id, ordinal),
FOREIGN KEY (organization_id, version_id) REFERENCES activity_versions(organization_id, id)
);
CREATE TABLE assignments (
organization_id uuid NOT NULL,
id uuid NOT NULL,
classroom_id uuid NOT NULL,
version_id uuid NOT NULL,
opens_at timestamptz,
closes_at timestamptz,
PRIMARY KEY (organization_id, id),
UNIQUE (organization_id, id, version_id),
FOREIGN KEY (organization_id, classroom_id) REFERENCES classrooms(organization_id, id),
FOREIGN KEY (organization_id, version_id) REFERENCES activity_versions(organization_id, id),
CHECK (closes_at IS NULL OR opens_at IS NULL OR closes_at > opens_at)
);
CREATE TABLE attempts (
organization_id uuid NOT NULL,
id uuid NOT NULL,
assignment_id uuid NOT NULL,
version_id uuid NOT NULL,
user_id uuid NOT NULL,
status text NOT NULL DEFAULT 'active' CHECK (status IN ('active','submitted','abandoned')),
input_mode text NOT NULL CHECK (input_mode IN ('camera','assisted')),
started_at timestamptz NOT NULL DEFAULT now(),
submitted_at timestamptz,
PRIMARY KEY (organization_id, id),
UNIQUE (organization_id, id, version_id),
FOREIGN KEY (organization_id, assignment_id, version_id)
REFERENCES assignments(organization_id, id, version_id),
FOREIGN KEY (organization_id, user_id)
REFERENCES organization_memberships(organization_id, user_id),
CHECK ((status = 'submitted') = (submitted_at IS NOT NULL)),
CHECK (submitted_at IS NULL OR submitted_at >= started_at)
);
CREATE TABLE responses (
organization_id uuid NOT NULL,
event_id uuid NOT NULL,
attempt_id uuid NOT NULL,
version_id uuid NOT NULL,
item_id uuid NOT NULL,
try_no integer NOT NULL CHECK (try_no > 0),
answer jsonb NOT NULL CHECK (jsonb_typeof(answer) = 'object'),
evaluation jsonb NOT NULL CHECK (jsonb_typeof(evaluation) = 'object'),
help_count integer NOT NULL DEFAULT 0 CHECK (help_count >= 0),
client_elapsed_ms bigint CHECK (client_elapsed_ms >= 0),
received_at timestamptz NOT NULL DEFAULT now(),
PRIMARY KEY (organization_id, event_id),
UNIQUE (organization_id, attempt_id, item_id, try_no),
FOREIGN KEY (organization_id, attempt_id, version_id)
REFERENCES attempts(organization_id, id, version_id),
FOREIGN KEY (organization_id, version_id, item_id)
REFERENCES activity_items(organization_id, version_id, id)
);
CREATE TABLE attempt_technical_summaries (
organization_id uuid NOT NULL,
attempt_id uuid NOT NULL,
app_version text NOT NULL,
model_version text NOT NULL,
tracking_loss_count integer NOT NULL DEFAULT 0 CHECK (tracking_loss_count >= 0),
inference_median_ms numeric CHECK (inference_median_ms >= 0),
PRIMARY KEY (organization_id, attempt_id),
FOREIGN KEY (organization_id, attempt_id) REFERENCES attempts(organization_id, id)
);
CREATE INDEX classroom_memberships_by_user
ON classroom_memberships(organization_id, user_id);
CREATE INDEX assignments_by_class
ON assignments(organization_id, classroom_id);
CREATE INDEX attempts_by_student
ON attempts(organization_id, user_id, started_at DESC);
CREATE INDEX attempts_by_assignment
ON attempts(organization_id, assignment_id, status);
CREATE INDEX responses_by_item
ON responses(organization_id, version_id, item_id);
COMMIT;
-- Default FK NO ACTION disengaja: penghapusan harus melalui prosedur retensi.
-- Tidak ada GRANT/RLS: tambahkan dan uji berdasarkan model koneksi produksi.
-- private_rubric wajib dikeluarkan dari DTO publik, bukan hanya disembunyikan UI.
-- ON CONFLICT bukan otorisasi; bandingkan identitas event dan payload di service.
-- Imutabilitas versi: service menolak mutasi versi terbit; pertimbangkan trigger
-- dan privilege database sebagai defense in depth, dengan tes tersendiri.
Merancang API, autentikasi, dan sinkronisasi hasil
API menjadi batas antara input yang dikendalikan browser dan data yang diterima sistem. Perlakukan semua payload klien sebagai data yang perlu diperiksa. Bahkan ketika UI hanya menampilkan satu tombol, pengguna atau gangguan jaringan dapat menghasilkan request yang tidak sesuai alur normal.
Rancangan API sebaiknya dimulai dari operasi bisnis, bukan CRUD setiap tabel. Peserta membutuhkan operasi memulai pengerjaan dan mengirim jawaban. Guru membutuhkan operasi menerbitkan tugas dan membaca laporan. Membuka endpoint bebas untuk mengubah semua kolom attempts akan menyulitkan pengendalian status.
Endpoint yang disarankan
| Metode dan endpoint | Pengguna | Tindakan service |
|---|---|---|
| GET /api/me | Pengguna masuk | Mengembalikan konteks akun dan keanggotaan |
| GET /api/classrooms | Guru atau peserta | Mengembalikan kelas yang boleh diakses |
| GET /api/assignments/:id | Anggota kelas | Memeriksa akses lalu mengirim konfigurasi tugas |
| POST /api/assignments/:id/attempts | Peserta | Memeriksa jadwal dan kuota percobaan |
| GET /api/attempts/:id | Pemilik atau guru berwenang | Mengirim state pengerjaan yang sesuai peran |
| POST /api/attempts/:id/responses | Pemilik | Memvalidasi item, idempotensi, dan jawaban |
| POST /api/attempts/:id/submit | Pemilik | Mengunci sesi dan menghitung ringkasan |
| POST /api/activities/:id/versions | Editor materi | Membuat draft revisi |
| POST /api/versions/:id/publish | Penerbit materi | Memvalidasi konten lalu menerbitkan snapshot |
| GET /api/assignments/:id/report | Guru kelas | Mengagregasi hasil tanpa membocorkan kelas lain |
Contoh endpoint merupakan usulan kontrak. Gunakan penamaan konsisten dengan proyek, tetapi jangan menghilangkan pemeriksaan akses hanya karena rutenya terlihat seperti operasi baca.
Contoh payload jawaban
Payload jawaban perlu menyatakan eventId, itemId, nomor percobaan, jawaban, serta metadata minimum. Timestamp klien dapat disimpan sebagai informasi, tetapi server menentukan waktu penerimaan dan tidak menganggap jam perangkat selalu benar.
{
"eventId": "7e8d2204-2fe1-4d5e-b566-6c4797d292b8",
"itemId": "6f4b4dd6-9c1d-4fab-a71e-c6de8ef17803",
"tryNo": 1,
"answer": {"optionId": "a"},
"inputMode": "camera",
"helpCount": 0,
"clientElapsedMs": 14320
}
Server mengambil userId dari identitas yang terverifikasi dan mengambil versi dari attempt. Klien tidak menentukan skor, pemilik sesi, atau organisasi yang dipercaya service. Jika aplikasi menerima field tambahan, gunakan kebijakan eksplisit untuk menolak atau membuangnya.
Respons server dapat menyatakan accepted, duplicate, atau rejected beserta kode yang dapat ditangani UI. Jangan mengirim detail internal database atau kunci soal lain sebagai pesan error.
Urutan validasi pada service
Pertama, verifikasi sesi pengguna. Kedua, cari attempt dalam cakupan organisasi yang diizinkan. Ketiga, pastikan pengguna adalah pemilik atau memiliki kewenangan khusus yang relevan. Keempat, periksa bahwa attempt masih menerima jawaban.
Kelima, pastikan item termasuk dalam versi yang dipakai attempt dan nomor percobaannya sesuai kebijakan. Keenam, validasi bentuk answer menurut jenis game. Ketujuh, tangani eventId sebelum menghitung nilai. Kedelapan, simpan respons dan perubahan ringkasan secara konsisten.
Urutan dapat disesuaikan demi efisiensi, tetapi tidak boleh menyebabkan item dari kelas lain dinilai hanya karena bentuk payload benar. Validasi tipe, relasi data, status, dan hak akses adalah pemeriksaan yang berbeda.
Idempotensi dan jawaban ganda
Jaringan seluler dapat terputus setelah server menerima jawaban tetapi sebelum browser menerima respons. Browser yang tidak mengetahui hasil akan mencoba lagi. Jika setiap kiriman dianggap baru, satu tindakan menghasilkan beberapa nilai.
Gunakan eventId yang dibuat sekali ketika tindakan diterima game, lalu dipakai kembali pada retry. Database menyimpan kunci unik yang sesuai cakupan. Jika eventId sudah ada dan isi payload sama, kembalikan hasil sebelumnya.
Jika eventId sama tetapi isi berbeda, perlakukan sebagai konflik, bukan pembaruan diam-diam. Simpan fingerprint dari payload yang dinormalisasi bila diperlukan. Dokumentasi INSERT dan ON CONFLICT menjelaskan mekanisme database yang dapat membantu, tetapi pemeriksaan kesamaan makna request tetap perlu dirancang.
Idempotensi eventId tidak menggantikan aturan bisnis. Dua eventId berbeda untuk item dan tryNo yang sama juga perlu ditangani melalui constraint atau logika service. Tentukan apakah hanya satu jawaban diperbolehkan atau apakah revisi harus menaikkan tryNo.
Menangani dua tab dan submit bersamaan
Peserta dapat membuka tugas pada dua tab. Jika kedua tab mengirim jawaban, service harus tetap menghasilkan state yang konsisten. UI dapat memperingatkan bahwa sesi sudah aktif di tempat lain, tetapi server tetap menjadi penjaga terakhir.
Saat submit bersamaan dengan respons terakhir, tentukan aturan urutan. Salah satu rancangan adalah mengunci row attempt selama transaksi singkat, memeriksa status, kemudian menyimpan respons atau menutup sesi. Request yang datang setelah submitted tidak diterima sebagai jawaban baru.
Jangan membuat halaman hasil hanya berdasarkan angka lokal sebelum server mengonfirmasi. Untuk mode offline, tampilkan bahwa hasil tersimpan di perangkat dan menunggu sinkronisasi. Label ini membantu peserta membedakan selesai bermain dengan selesai dikirim.
Antrean offline di perangkat
IndexedDB menyediakan penyimpanan terstruktur pada browser. Dalam rancangan ini, antrean dapat menyimpan eventId, attemptId, payload, jumlah retry, dan status pengiriman.
Kirim ulang dengan jeda bertahap dan batas yang jelas. Tidak semua error layak diulang: kegagalan jaringan dapat dicoba lagi, sedangkan attempt yang ditutup membutuhkan tindakan pengguna atau kebijakan rekonsiliasi.
Saat logout pada perangkat bersama, jangan mengirim event peserta lama memakai akun peserta baru. Ikat antrean ke identitas yang membuatnya. Beri proses eksplisit untuk menyelesaikan sinkronisasi atau membuang data lokal sesuai kebijakan sebelum berganti pengguna.
Penyimpanan browser bukan backup permanen. Jelaskan batas mode offline dan uji kondisi ruang penyimpanan penuh atau data situs dibersihkan. Jika tugas penting, sistem perlu memberi indikator bahwa hasil benar-benar diterima server.
Autentikasi dan sesi
Pilih satu pola yang sesuai arsitektur: session cookie untuk aplikasi web yang dikelola bersama, atau token API untuk kebutuhan yang memang memerlukannya. Untuk cookie, rancang perlindungan CSRF sesuai framework dan konfigurasi lintas origin. Untuk token, rancang masa berlaku, pembaruan, pencabutan, dan penyimpanan.
Jangan mencampur kedua pendekatan tanpa alasan. Frontend yang mendapatkan identitas dari satu sistem sementara backend mempercayai field userId di body akan menghasilkan celah desain.
Pisahkan endpoint peserta, editor materi, dan pengelola berdasarkan kebijakan akses yang dapat diuji. Menyembunyikan tombol administrasi bukan pengganti pemeriksaan backend. Setiap operasi harus bekerja benar walaupun dipanggil tanpa melalui UI resmi.
Implementasi frontend dan kamera sebagai modul yang terpisah
Frontend membutuhkan dua jenis state: state produk yang berubah pada peristiwa bermakna dan state frame yang bergerak cepat. State produk meliputi tugas aktif, item saat ini, hasil, serta izin kamera. State frame meliputi titik tangan, posisi tubuh, dan kategori prediksi terbaru.
Jangan menjadikan keduanya satu objek besar yang disimpan ulang pada setiap frame. Pisahkan loop deteksi, adapter interaksi, dan event yang mengubah game. Dengan begitu, perubahan UI seperti membuka pengaturan tidak harus menginisialisasi ulang kamera.
Lifecycle sesi
Setiap sesi memiliki sessionId yang baru. Ketika pengguna mengganti game atau menutup kamera, sesi lama dibatalkan. Hasil worker yang datang setelah pembatalan harus diabaikan meskipun prediksinya terlihat valid.
Urutan yang disarankan adalah memuat konfigurasi, meminta izin setelah tindakan pengguna, menunggu video siap, memuat model, melakukan warm-up, kalibrasi, tutorial kontrol, kemudian bermain. Kesalahan pada satu langkah membawa UI ke keadaan yang dapat dipulihkan.
Saat cleanup, hentikan track media, batalkan penjadwalan frame, tutup resource model, dan kosongkan tindakan yang belum dikonfirmasi. Hal ini penting agar satu kunjungan ulang tidak menghasilkan dua loop yang sama-sama memproses kamera.
Protokol worker
Pesan worker perlu mempunyai jenis yang jelas seperti init, ready, infer, result, error, dan dispose. Sertakan sessionId, frameId, dan timestamp asal. Worker tidak perlu mengetahui kunci soal atau identitas peserta.
Maksimum satu frame diproses pada satu waktu sebagai kebijakan awal. Setelah result atau error diterima, scheduler boleh mengirim frame berikutnya. Jika worker berhenti merespons, tampilkan pemulihan dan hentikan pengiriman tambahan.
Transfer image harus memperhatikan kepemilikan resource. Lepaskan objek sementara setelah dipakai dan jangan menyimpan riwayat bitmap tanpa kebutuhan. Kenaikan memori yang lambat bisa tidak terlihat pada demo pendek tetapi merusak sesi panjang.
Pemetaan koordinat yang konsisten
Simpan transformasi dari sumber video ke area tampilan. Perubahan orientasi, crop, mirror, dan device pixel ratio perlu ditangani secara sengaja. Koordinat CSS dan piksel backing Canvas bukan selalu satuan yang sama.
Untuk Canvas beresolusi tinggi, atur ukuran backing berdasarkan device pixel ratio, tetapi pertahankan sistem hit testing dalam satuan logis yang konsisten. Jika renderer memakai piksel fisik sementara area klik memakai CSS pixel, pointer dapat meleset.
Tambahkan tampilan diagnostik khusus pengembang yang menunjukkan area sumber, crop, dan titik hasil. Matikan tampilan tersebut pada alur peserta. Tujuannya membantu menemukan masalah koordinat tanpa membebani pengguna dengan detail implementasi.
QualityGate dan kestabilan
QualityGate memeriksa apakah observasi masih cukup baru, target valid, dan kondisi model memenuhi kebutuhan interaksi. Nilai confidence tangan kiri/kanan tidak sama dengan keyakinan bahwa setiap titik jari akurat. Gunakan field sesuai maknanya.
Saat tracking buruk, aplikasi sebaiknya mengurangi tindakan, bukan menebak lebih agresif. Pointer dapat dibekukan atau disembunyikan, sedangkan jawaban tetap belum dikirim. Pesan “Posisikan tangan di tengah” lebih membantu daripada skor terus turun.
Kalibrasi menyimpan rentang input untuk sesi, tetapi jangan menganggap kalibrasi selalu berlaku setelah kamera berubah. Perubahan kamera, dimensi, atau posisi perangkat dapat memerlukan pengulangan singkat.
Pengelolaan materi dan dashboard guru
Editor materi perlu menyediakan validasi sebelum publikasi. Soal pilihan ganda harus mempunyai opsi yang cukup dan kunci yang mengacu pada opsi yang ada. Game bentuk memerlukan template yang valid. Game objek memerlukan label yang tersedia pada model profile.
Gunakan preview yang memakai engine yang sama dengan peserta. Editor yang menampilkan pratinjau berbeda dari game sesungguhnya dapat menyembunyikan masalah layout atau konfigurasi. Preview tidak perlu menyimpan hasil ke laporan kelas.
Pisahkan pembuat dan penerbit materi jika institusi memerlukan review. Setelah materi diterbitkan, simpan siapa yang menyetujui dan kapan. Catatan ini membantu koreksi ketika guru menemukan pertanyaan ambigu.
Dashboard guru sebaiknya menjawab pertanyaan praktis: siapa belum mulai, siapa sudah selesai, konsep apa yang sering salah, dan apakah banyak peserta mengalami gangguan teknis. Jangan menggabungkan semua keadaan menjadi satu warna merah.
Tampilkan jumlah data yang mendasari persentase. Nilai 100% dari satu soal berbeda konteks dengan 100% dari dua puluh soal. Tampilkan pula apakah yang diringkas jawaban pertama atau jawaban terbaik setelah mengulang.
Untuk ekspor, gunakan identitas yang sesuai kebutuhan penerima. Laporan publik tidak memerlukan nama peserta. Jika hasil dibagikan kepada guru lain, cek cakupan akses dan tujuan pembagian, bukan hanya menyediakan tombol unduh tanpa pembatasan.
Cara membuat game tetap nyaman di HP
Mulai dari resolusi kamera yang wajar dan tingkatkan hanya bila diperlukan. Sebagai titik awal eksperimen, 640 × 480 dapat dicoba, tetapi hasil akhirnya harus ditentukan melalui uji deteksi dan performa.
Kurangi efek visual ketika perangkat bekerja berat. Bayangan kompleks dan partikel yang berlebihan tidak membantu jika pointer terlambat mengikuti tangan. Ukur latensi keseluruhan, bukan hanya waktu menjalankan model.
Perhatikan perubahan orientasi. Pemetaan koordinat harus diperbarui ketika layar berpindah dari portrait ke landscape atau ukuran area video berubah. Preview yang dicerminkan juga memerlukan transformasi yang konsisten.
Jangan menganggap posisi x/y model langsung sama dengan posisi CSS. Jika video menggunakan cover, sebagian gambar dapat terpotong. Jika menggunakan contain, ada area kosong di sisi tertentu. Pointer perlu memperhitungkan skala dan offset tersebut.
Uji sesi yang cukup panjang untuk melihat perubahan performa. Aplikasi yang baik selama 30 detik belum tentu nyaman setelah 15 menit. Catat penurunan frame rate, penggunaan memori, dan kondisi perangkat selama pilot.
Roadmap pembangunan yang bisa diterjemahkan menjadi tiket
Roadmap berikut adalah usulan urutan kerja, bukan estimasi kontrak. Waktu nyata bergantung pada pengalaman tim, kesiapan materi, perangkat sasaran, dan kebutuhan institusi. Untuk tim kecil, lebih berguna membagi pekerjaan menjadi hasil yang dapat didemonstrasikan daripada menjanjikan semua game selesai pada tanggal tertentu. Satu iterasi dianggap selesai apabila pengembang lain dapat menjalankannya dari dokumentasi, guru dapat mencoba alurnya, dan masalah yang masih diketahui tercatat.
Iterasi 1: buktikan interaksi, tanpa akun kelas
Bangun satu halaman izin kamera, satu detector, dan satu miniaktivitas Jepit Jawaban. Gunakan tiga soal tetap agar perhatian tidak terpecah pada editor. Tampilkan indikator tangan ditemukan, kalibrasi, dan cara mengulang. Sediakan tombol berhenti yang benar-benar menghentikan track kamera. Catat perangkat, browser, resolusi input, waktu inferensi, dan pengalaman pengguna. Jangan menyimpulkan kelayakan dari laptop pengembang saja. Demonstrasi harus mencakup kamera ditolak, tangan keluar bingkai, dan halaman ditinggalkan.
Hasil iterasi ini adalah keputusan apakah gesture dapat diandalkan pada perangkat sasaran. Jika belum, kurangi kompleksitas tampilan atau ganti gesture sebelum membangun dashboard. Tiket kecil dapat berupa controller kamera, adapter landmark, filter jitter, mesin pinch, tutorial, dan pencatatan metrik lokal. Setiap tiket mempunyai contoh input dan kondisi selesai. Kualitas pedagogis awal dinilai guru melalui kecocokan instruksi, jumlah pilihan, dan kejelasan umpan balik.
Iterasi 2: bentuk satu alur belajar yang tersimpan
Tambahkan identitas pengguna, satu organisasi, satu kelas, materi berversi, penugasan, percobaan, dan respons. Guru menerbitkan aktivitas lalu menugaskannya. Murid membuka tugas, menyelesaikan aktivitas, dan melihat ringkasan. Guru melihat hasil yang sama. Simulasikan pengiriman ulang setelah koneksi putus sehingga tim membuktikan satu jawaban tidak berubah menjadi dua. Periksa bahwa pengguna kelas lain tidak dapat membuka data hanya dengan menebak ID.
Pisahkan tiket database dan kontrak API, tetapi sepakati contoh payload sebelum keduanya dikerjakan. Backend boleh memakai jawaban dummy untuk pengujian; frontend boleh memakai server mock. Ketika integrasi dilakukan, gantikan mock secara eksplisit dan tandai fitur yang masih berupa simulasi. Jangan menyebut laporan lengkap apabila sebagian nilai masih berasal dari data hardcode.
Iterasi 3: tambah variasi melalui kontrak yang sama
Tambahkan Gambar Bentuk dan Sudut dengan Lengan terlebih dahulu apabila tujuan produk dominan matematika. Tambahkan Berburu Benda lebih awal jika kebutuhan utama kosakata. Prioritas tidak harus mengikuti nomor artikel. Setiap game mendaftarkan jenis konten, validator, renderer, evaluator, dan tutorialnya. Modul akun, kelas, percobaan, dan laporan tetap digunakan bersama. Perubahan kontrak respons harus menjaga kompatibilitas dengan versi aktivitas yang sudah diterbitkan.
Jalur Jawaban memerlukan uji ruang gerak dan mode duduk. Kode Gestur memerlukan pengujian interpreter terpisah dari pengenalan tangan. Berburu Benda memerlukan pemeriksaan apakah label model mendukung materi. Karena risiko ketiganya berbeda, jangan memakai satu checklist “kamera menyala” untuk menyatakan semua selesai. Setelah satu game ditambahkan, lakukan regresi singkat terhadap alur lama yang memakai komponen bersama.
Iterasi 4: pilot kelas dan kesiapan operasional
Pilot sebaiknya mempunyai tujuan terbatas, misalnya menguji apakah guru dapat memulai sesi tanpa bantuan teknis dan apakah murid memahami instruksi. Kumpulkan catatan kesulitan, waktu penyelesaian, jumlah bantuan, serta masalah perangkat. Pisahkan observasi kegunaan dari klaim peningkatan hasil belajar. Bukti bahwa peserta menikmati permainan belum membuktikan penguasaan materi meningkat. Jika ingin menilai hasil belajar, siapkan instrumen dan rancangan evaluasi yang sesuai bersama pendidik.
Tetapkan kriteria lanjut sebelum pilot: tidak ada akses lintas organisasi, tidak ada kehilangan hasil pada skenario retry yang diuji, kamera berhenti sesuai kontrol, materi telah ditinjau, dan perangkat sasaran memenuhi pengalaman minimum yang disepakati. Hasil pilot dapat menghasilkan keputusan menunda satu game. Keputusan itu lebih sehat daripada memaksa semua model interaksi masuk ke peluncuran pertama ketika satu di antaranya belum stabil.
Deployment, observabilitas, dan biaya operasional
Pisahkan lingkungan dan konfigurasi
Gunakan lingkungan pengembangan, staging, dan produksi dengan database serta kredensial berbeda. Staging berisi data sintetis; jangan menyalin daftar murid nyata hanya untuk memeriksa warna tombol. Konfigurasi publik frontend boleh berisi base URL API dan versi model. Rahasia backend, kredensial database, dan kunci layanan tidak boleh dimasukkan ke bundle browser. Nama variabel yang diawali prefix publik harus diperiksa dalam review karena nilainya biasanya memang dimaksudkan tersedia di klien.
Produksi perlu HTTPS untuk alur kamera normal. Tentukan domain frontend dan API, kebijakan origin yang diizinkan, pengaturan cookie, dan jalur penyajian model. Uji konfigurasi yang benar-benar akan digunakan, termasuk apabila aplikasi ditanam dalam iframe milik sekolah. Jangan menganggap perilaku localhost mewakili domain sekolah. Catatan deployment harus mencantumkan siapa yang memiliki domain, siapa yang dapat mengganti konfigurasi, dan bagaimana memulihkan layanan ketika pemilik utama tidak tersedia.
Perlakukan model sebagai aset berversi
Simpan manifest model yang memuat nama, versi paket runtime, lokasi berkas, ukuran, hash, lisensi, dan game yang menggunakan model tersebut. Kunci dependensi dalam lockfile. Hindari mengganti model melalui URL yang selalu menunjuk berkas terbaru tanpa pengujian. Pergantian model dapat mengubah label atau kestabilan prediksi meskipun antarmuka aplikasi tidak berubah. Cache aset memakai nama berversi, lalu tampilkan status pemuatan yang dapat dipahami pengguna.
Model sebaiknya dimuat ketika game terkait dibuka, bukan seluruhnya pada halaman pertama. Tetapkan timeout dan pilihan mencoba lagi. Jika unduhan gagal, jelaskan bahwa komponen pengenalan belum siap, bukan bahwa jawaban murid salah. Ukur ukuran transfer dan durasi pemuatan di jaringan sasaran. Target angkanya ditentukan dari pengukuran produk sendiri; artikel ini tidak menjanjikan ukuran atau kecepatan universal untuk semua kombinasi model dan perangkat.
Rilis database dan aplikasi secara terencana
Migrasi produksi harus ditinjau bersama rencana rollback. Penambahan kolom nullable umumnya lebih mudah dipisahkan dari penghapusan kolom yang masih dipakai versi aplikasi lama. Untuk perubahan besar, gunakan tahap tambah struktur, pindahkan data, alihkan pembacaan, baru hapus struktur lama setelah aman. Strategi ini adalah usulan operasional; detail SQL tetap perlu diuji pada versi PostgreSQL dan volume data yang digunakan.
Sebelum rilis, pastikan backup dapat dipulihkan, bukan sekadar bahwa tugas backup berstatus sukses. Dokumentasikan target kehilangan data yang dapat diterima dan target waktu pemulihan bersama pengelola. Lakukan latihan restore ke lingkungan terisolasi dengan data yang sesuai kebijakan. Rollback frontend tidak otomatis mengembalikan skema database. Karena itu, simpan catatan pasangan versi aplikasi, skema, dan model untuk setiap rilis.
Pantau masalah tanpa mengumpulkan rekaman kamera
Gunakan log terstruktur untuk request ID, jenis endpoint, status, durasi, kode kesalahan, dan versi aplikasi. Jangan menulis token autentikasi, cookie, isi kamera, atau seluruh payload murid ke log umum. Untuk masalah detector, ringkasan seperti durasi pemuatan model, median inferensi, jumlah kehilangan tracking, dan jenis perangkat dapat lebih berguna daripada rekaman. Pengumpulan ringkasan tetap perlu dibatasi tujuan, masa simpan, dan aksesnya.
Dashboard operasional dan dashboard guru melayani pertanyaan berbeda. Tim operasional perlu tahu apakah API gagal, aset model tidak terunduh, atau antrean sinkronisasi macet. Guru perlu tahu materi mana yang sudah dicoba dan bantuan apa yang diperlukan. Jangan memasukkan angka teknis yang tidak membantu keputusan mengajar ke laporan akademik. Sebaliknya, jangan membuka nama seluruh murid kepada setiap anggota tim yang hanya menangani performa server.
Buat estimasi biaya dengan variabel yang bisa diganti
Rumus sederhana biaya bulanan adalah hosting frontend ditambah API, database, penyimpanan, transfer aset, backup, dan observabilitas. Inferensi lokal mengurangi kebutuhan komputasi server untuk setiap frame, tetapi tidak membuat produk bebas biaya. Unduhan model, database hasil belajar, dukungan perangkat, dan waktu pengelolaan tetap perlu dihitung. Gunakan tarif aktual penyedia saat memilih layanan, karena angka harga cepat berubah.
Sebagai latihan perencanaan, jika satu sesi menghasilkan 20 event jawaban dan ada 1.000 sesi per bulan, sistem menerima sekitar 20.000 event jawaban, di luar event lifecycle. Angka ini contoh aritmetika, bukan proyeksi pengguna. Hitung ukuran baris aktual dengan indeks dan overhead database sebelum memperkirakan kapasitas. Bedakan pengguna terdaftar, pengguna aktif, sesi bersamaan, dan total respons; empat ukuran itu memengaruhi komponen biaya secara berbeda.
Rencana pengujian yang menghasilkan bukti
Uji logika sebelum menguji kamera langsung
Evaluator jawaban, penghitung sudut, penyederhanaan lintasan, dan interpreter robot dapat diuji menggunakan fixture sintetis. Contoh sudut memakai titik yang menghasilkan 90 derajat, garis lurus, dan vektor dengan panjang mendekati nol. Contoh lintasan memeriksa koordinat berulang, garis terlalu pendek, dan urutan titik terbalik sesuai rubrik. Interpreter memeriksa tabrakan, batas langkah, urutan kosong, serta reset. Fixture tidak perlu berisi foto murid.
Mesin gesture diuji sebagai rangkaian waktu. Berikan landmark yang mendekati ambang, menyeberanginya, lalu bergetar di sekitarnya. Pastikan hysteresis mencegah satu gerakan menjadi banyak event. Berikan hasil detector yang datang terlambat setelah pergantian sesi, dan pastikan hasil itu ditolak. Pengujian ini berbeda dari menulis tes yang hanya menyalin baris implementasi; tujuannya membuktikan perilaku yang berisiko gagal di perangkat nyata.
Uji API dengan identitas dan kondisi yang berbeda
Minimal sediakan akun murid A, murid B satu kelas, guru kelas tersebut, guru kelas lain, dan pengguna organisasi lain. Coba membaca tugas, mengirim jawaban, mengubah materi, dan mengekspor hasil dengan tiap identitas. Hasil yang diharapkan harus tercatat sebelum tes dijalankan. ID acak yang sulit ditebak tidak menggantikan pemeriksaan kepemilikan. Periksa juga akun yang sudah dinonaktifkan dan keanggotaan yang dicabut ketika sesi masih terbuka.
Untuk idempotensi, kirim event yang sama dua kali, kirim event ID sama dengan payload berbeda, dan kirim dua event berbeda untuk kombinasi item serta nomor percobaan yang sama. Uji permintaan paralel, bukan hanya berurutan. Untuk finalisasi, kirim jawaban bersamaan dengan submit. Sistem harus menghasilkan status yang konsisten sesuai kontrak. Jika ditemukan kondisi balapan, perbaiki transaksi atau constraint; jangan hanya menyembunyikan tombol lebih cepat.
Uji perangkat berdasarkan matriks, bukan kebiasaan tim
Buat matriks perangkat sasaran yang memuat sistem operasi, browser dan versinya, orientasi, kamera depan atau belakang, serta kelas kemampuan perangkat. Tambahkan kondisi cahaya normal, redup, latar ramai, tangan sebagian keluar bingkai, dan pengguna berganti. Tidak semua kombinasi harus diuji pada setiap commit. Pilih smoke test kecil untuk perubahan rutin dan matriks lebih luas untuk rilis yang mengubah pipeline kamera atau model.
Catat hasil sebagai lulus, gagal, atau belum diuji. Jangan memberi label kompatibel hanya karena halaman dapat dimuat. Pengalaman minimum mencakup izin kamera, pemuatan model, respons gesture, penyelesaian aktivitas, dan penyimpanan hasil. Pengujian otomatis melalui mock membantu regresi UI, tetapi tidak membuktikan kualitas kamera fisik. Jadwalkan pengujian manual pada perangkat sasaran dengan prosedur yang sama agar hasil antarversi dapat dibandingkan.
Bedakan kriteria teknis dan kriteria pembelajaran
Kriteria teknis meliputi penghentian kamera, isolasi data, stabilitas event, dan pemulihan koneksi. Kriteria pembelajaran meliputi ketepatan materi, kejelasan instruksi, kecocokan tingkat kesulitan, serta umpan balik yang membantu murid memperbaiki jawaban. Guru perlu memeriksa rubrik yang dipakai evaluator. Angka confidence model tidak boleh menjadi nilai pengetahuan, sebab model menilai kecocokan input dengan representasinya, bukan pemahaman peserta didik.
Laporan uji akhir sebaiknya memuat versi aplikasi, versi model, tanggal, perangkat, skenario, hasil, dan masalah yang belum selesai. Cantumkan batas kesimpulan: misalnya game telah diuji pada dua perangkat tertentu, bukan semua ponsel. Lampirkan keputusan rilis beserta alasan. Jika ada fitur yang belum diuji, tulis secara eksplisit supaya tim berikutnya tidak salah menganggap prototipe sebagai produk kelas yang telah tervalidasi.
Kumpulan prompt untuk pengembangan platform kelas
Prompt berikut melanjutkan prototipe ke platform kelas berbasis React dan TypeScript. Untuk mengubah proyek JavaScript yang sedang Anda bangun secara langsung, sesuaikan konteks stack dan gunakan prompt spesifik pada akhir setiap tutorial game. Prompt berikut adalah instruksi kerja untuk asisten coding atau anggota tim. Prompt bukan pengganti keputusan produk, review kode, atau pengujian perangkat. Jalankan bertahap dengan konteks repositori yang nyata. Ganti bagian dalam tanda kurung siku sebelum digunakan. Jangan menempelkan token, password, data pribadi murid, atau rekaman kamera ke layanan AI hanya untuk membuat contoh lebih realistis. Gunakan identitas dan fixture sintetis.
Prompt 0: konteks induk proyek
Salin dan sesuaikan: Saya membangun web pembelajaran dengan enam game kamera: Jepit Jawaban, Gambar Bentuk, Sudut Lengan, Jalur Jawaban, Berburu Benda, dan Kode Gestur. Sasaran pengguna [usia/jenjang], perangkat [daftar], konteks [rumah/kelas], dan tim [kapasitas]. Acuan stack adalah React, TypeScript, Vite, NestJS, PostgreSQL, serta detector lokal MediaPipe atau TensorFlow.js sesuai tugas. Gameplay utama menggunakan kamera; kontrol akun dan navigasi tetap memakai UI biasa. Jangan mengunggah frame. Bedakan rancangan, kode yang dibuat, dan fitur yang sudah diuji. Mulai dengan membaca struktur repositori serta instruksi yang berlaku, lalu laporkan asumsi dan risiko yang memengaruhi pekerjaan.
Prompt 1: PRD dan batas MVP
Salin dan sesuaikan: Berdasarkan konteks induk, tulis PRD untuk MVP satu game Jepit Jawaban dengan satu alur guru menugaskan dan murid menyelesaikan. Uraikan masalah pengguna, tujuan belajar, persona, alur utama, kondisi gagal, batas cakupan, dan pertanyaan terbuka. Turunkan kebutuhan menjadi user story dengan kriteria penerimaan yang dapat diamati. Pisahkan metrik kegunaan, kualitas deteksi, dan hasil belajar. Jangan mengklaim efektivitas pedagogis tanpa bukti. Akhiri dengan urutan keputusan yang perlu diselesaikan sebelum desain database, termasuk aturan percobaan ulang, penilaian, dan siapa yang dapat melihat hasil.
Prompt 2: keputusan framework dan arsitektur
Salin dan sesuaikan: Buat ADR yang membandingkan React Vite dengan Next.js untuk frontend kamera serta NestJS dengan Laravel untuk backend tim [keahlian]. Nilai kebutuhan SEO halaman publik, autentikasi, operasi kelas, deployment, pengujian, dan pemeliharaan. Tetapkan satu pilihan acuan dengan alasan serta kondisi yang membuat keputusan perlu ditinjau ulang. Gambarkan batas UI, controller kamera, adapter detector, mesin game, API, dan database. Tandai komponen yang hanya berjalan di browser. Rujuk dokumentasi resmi untuk klaim kemampuan framework dan jangan menyebut versi terbaru tanpa verifikasi.
Prompt 3: skema data dan migrasi
Salin dan sesuaikan: Rancang migrasi PostgreSQL untuk organisasi, identitas pengguna, keanggotaan, kelas, materi, versi materi, item, penugasan, percobaan, dan respons. Materi terbit harus mempunyai versi yang dapat dipertahankan; tugas menunjuk versi tertentu. Gunakan foreign key komposit untuk mencegah hubungan lintas organisasi dan respons ke item versi yang salah. Jelaskan primary key, unique constraint, indeks, kebijakan penghapusan, dan aturan yang tetap harus diperiksa aplikasi. Sertakan seed sintetis dan skenario validasi negatif. Jangan menganggap skema saja sudah menyediakan otorisasi endpoint atau imutabilitas seluruh konten.
Prompt 4: autentikasi dan otorisasi
Salin dan sesuaikan: Implementasikan autentikasi sesuai mekanisme proyek yang sudah ada, lalu tambahkan otorisasi tingkat organisasi, kelas, dan pemilik percobaan. Jangan mengganti sistem login tanpa menjelaskan dampaknya. Buat matriks izin murid, guru, dan admin. Identitas pengguna serta organisasi aktif harus diverifikasi server, bukan dipercaya dari payload. Sertakan tes akses milik sendiri, milik teman, kelas lain, organisasi lain, dan keanggotaan dicabut. Jika memakai cookie, jelaskan pengaturan keamanan dan perlindungan CSRF yang relevan. Gunakan data sintetis dan jangan mencetak kredensial dalam log.
Prompt 5: controller kamera
Salin dan sesuaikan: Buat controller kamera dengan state idle, requesting, ready, running, paused, error, dan stopped. Kamera diminta setelah aksi pengguna. Tangani izin ditolak, perangkat tidak tersedia, stream terputus, perubahan halaman, dan tombol berhenti. Semua track harus dihentikan pada cleanup. Pisahkan state UI dari data frame; jangan memperbarui React state untuk setiap landmark. Sertakan session ID agar hasil inferensi lama diabaikan. Tambahkan petunjuk pengguna yang singkat serta tes lifecycle dengan stream mock. Nyatakan bahwa tes mock belum membuktikan kompatibilitas kamera fisik.
Prompt 6: adapter detector dan worker
Salin dan sesuaikan: Periksa kemampuan runtime detector [pilihan dan versi terkunci] sebelum memindahkan inferensi ke worker. Definisikan pesan init, ready, infer, result, error, dan dispose. Terapkan satu inferensi aktif, frame ID, session ID, timeout yang wajar, serta pelepasan sumber daya frame. Jika jalur worker tidak didukung kombinasi browser dan runtime sasaran, tawarkan fallback yang terukur. Jangan mengarang API. Buat adapter agar logika game menerima landmark ternormalisasi tanpa bergantung pada paket detector tertentu, lalu uji hasil terlambat dan pergantian sesi.
Prompt 7: implementasi Jepit Jawaban
Salin dan sesuaikan: Bangun game Jepit Jawaban dengan koordinat kursor dari ujung telunjuk dan jarak ibu jari yang dinormalisasi terhadap ukuran tangan. Gunakan hysteresis, dwell, serta state hover, grabbed, dragging, dropped, dan cooldown. Tentukan perilaku saat tracking hilang ketika kartu sedang dibawa. Bedakan salah meletakkan kartu dan input belum terbaca. Buat hitbox yang terlihat pada mode debug, tetapi tidak mengganggu murid. Tambahkan fixture gerakan dengan jitter untuk memastikan satu cubitan tidak menggandakan jawaban. Rubrik akademik harus berasal dari versi soal, bukan confidence detector.
Prompt 8: implementasi Gambar Bentuk
Salin dan sesuaikan: Bangun aktivitas menggambar bentuk menggunakan lintasan jari dengan gesture mulai dan selesai yang jelas. Simpan titik hanya ketika sesi menggambar aktif, batasi jumlah titik, dan normalisasikan skala serta posisi. Buat evaluator sederhana untuk [bentuk yang dipilih] dengan rubrik terbuka mengenai penutupan, jumlah sudut, dan toleransi. Hindari klaim pengenalan semua gambar. Sediakan umpan balik spesifik ketika lintasan terlalu pendek atau belum selesai. Uji garis berulang, titik identik, arah lintasan terbalik, dan bentuk dengan ukuran berbeda menggunakan fixture sintetis.
Prompt 9: implementasi Sudut Lengan
Salin dan sesuaikan: Implementasikan perhitungan sudut siku dari bahu, siku, dan pergelangan dalam bidang citra. Tangani vektor nol, landmark tidak cukup terlihat, dan nilai cosine di luar rentang akibat pembulatan. Jelaskan keterbatasan proyeksi dua dimensi kepada pengembang. Tampilkan target dengan toleransi dan waktu tahan yang dapat dikonfigurasi dalam materi. Sediakan alternatif gerak yang sesuai ruang pengguna. Uji sudut 90 derajat, lurus, dan input tidak valid. Jangan menyajikan hasil sebagai pengukuran klinis atau diagnosis gerak tubuh.
Prompt 10: implementasi Jalur Jawaban
Salin dan sesuaikan: Buat kuis tiga jalur yang memilih jawaban dari posisi tubuh setelah kalibrasi. Definisikan fase membaca soal, memilih, mengunci, dan umpan balik agar murid tidak menjawab saat baru mencari posisi. Gunakan area netral dan waktu tahan. Kalibrasi harus menyesuaikan lebar gerak pengguna, termasuk mode duduk jika disepakati dalam PRD. Hindari lompatan sebagai syarat. Uji perpindahan cepat, tubuh keluar bingkai, dan perubahan orientasi perangkat. Pisahkan kegagalan tracking dari kesalahan memilih jawaban dalam event hasil.
Prompt 11: implementasi Berburu Benda
Salin dan sesuaikan: Buat aktivitas kosakata yang meminta pengguna menunjukkan benda kepada kamera. Baca daftar label model yang benar-benar digunakan dan buat pemetaan eksplisit ke kosakata pelajaran. Jangan menerima target yang tidak didukung model tanpa strategi alternatif. Terapkan kestabilan prediksi selama beberapa frame dan konfirmasi yang jelas. Buat contoh negatif serta penjelasan ketika tidak ada objek yang cocok. Simpan label jawaban dan ringkasan percobaan, tanpa menyimpan foto. Pisahkan nilai pengenalan objek dari evaluasi apakah pengguna memahami kata yang ditanyakan.
Prompt 12: implementasi Kode Gestur
Salin dan sesuaikan: Buat game menyusun instruksi robot pada grid melalui gesture tangan. Pisahkan pengenalan gesture, editor antrean perintah, dan interpreter deterministik. Pengguna harus dapat menambah, menghapus, mengulang, dan menjalankan program dengan konfirmasi yang tidak mudah terpencet berulang. Terapkan batas panjang program dan jumlah langkah eksekusi. Jangan memakai eval untuk menjalankan perintah. Buat fixture robot mencapai tujuan, menabrak penghalang, program kosong, dan batas langkah. Nilai urutan instruksi dari kondisi puzzle, bukan dari kestabilan tangan pengguna.
Prompt 13: API jawaban dan idempotensi
Salin dan sesuaikan: Implementasikan endpoint pengiriman respons dengan event ID dari klien, nomor percobaan per item, dan transaksi server. Pengiriman ulang payload sama harus mengembalikan hasil yang konsisten; event ID sama dengan payload berbeda harus menghasilkan konflik yang terdokumentasi. Gunakan unique constraint untuk mencegah duplikasi saat request paralel. Server menentukan pengguna, organisasi, versi soal, serta skor. Tambahkan tes respons terhadap percobaan selesai, item dari versi lain, dan submit yang berbarengan dengan jawaban. Jelaskan keputusan penguncian dan batas transaksi.
Prompt 14: sinkronisasi offline
Salin dan sesuaikan: Rancang antrean IndexedDB untuk event jawaban yang belum terkirim. Ikat antrean ke identitas pengguna, organisasi, percobaan, dan versi materi. Tampilkan status tersimpan lokal, sedang mengirim, tersinkron, atau perlu tindakan. Gunakan retry dengan backoff dan bedakan kegagalan jaringan dari penolakan permanen server. Tentukan tindakan saat logout atau pergantian akun pada perangkat bersama. Jangan mengirim event akun lama sebagai akun baru. Uji koneksi putus, browser ditutup, event duplikat, dan percobaan yang telah difinalisasi dari tab lain.
Prompt 15: editor materi dan dashboard
Salin dan sesuaikan: Buat alur guru menyusun draft, memvalidasi, mempratinjau, menerbitkan versi, dan menugaskannya kepada kelas. Perubahan materi terbit harus menghasilkan versi baru. Gunakan engine yang sama untuk preview dan sesi murid agar perilakunya konsisten. Dashboard harus membedakan belum mulai, sedang mencoba, selesai, dan terkendala teknis. Jelaskan denominator setiap persentase dan aturan memilih percobaan pertama atau terbaik. Batasi ekspor menurut izin kelas. Jangan menampilkan confidence model sebagai indikator kemampuan akademik murid.
Prompt 16: pengujian dan audit implementasi
Salin dan sesuaikan: Tinjau perubahan repositori terhadap PRD dan kontrak API. Susun tes berdasarkan risiko: akses lintas organisasi, event ganda, cleanup kamera, hasil inferensi terlambat, evaluasi rubrik, dan pemulihan offline. Jalankan tes yang tersedia dan laporkan perintah serta hasil sebenarnya. Pisahkan temuan terverifikasi, dugaan yang perlu reproduksi, dan skenario yang belum diuji. Jangan menyatakan semua perangkat kompatibel dari tes mock. Usulkan perbaikan kecil yang dapat ditinjau dan jelaskan dampaknya terhadap data atau pengalaman pengguna sebelum perubahan besar.
Prompt 17: deployment dan serah terima
Salin dan sesuaikan: Buat runbook deployment untuk lingkungan [penyedia pilihan] tanpa memasukkan rahasia. Dokumentasikan variabel konfigurasi, build, migrasi, penyajian aset model, health check, backup, restore, dan rollback. Pisahkan langkah otomatis dari langkah operator. Verifikasi domain HTTPS dan konfigurasi kamera pada origin produksi yang sebenarnya. Sertakan daftar versi aplikasi, skema, runtime, dan model. Buat checklist serah terima kepada guru serta pengelola teknis. Nyatakan batas pengujian dan fitur yang belum selesai; jangan memberi label siap produksi apabila bukti operasionalnya belum tersedia.
Cara menilai hasil dari prompt
Hasil yang baik bukan sekadar banyak file. Cocokkan hasil dengan kriteria penerimaan, jalankan dari lingkungan bersih, dan minta penjelasan perubahan terhadap kontrak data. Jika asisten menambahkan dependensi, tanyakan mengapa dependensi itu diperlukan dan bagaimana pemeliharaannya. Jika ada bagian yang disimulasikan, pastikan ditandai dalam README. Simpan keputusan penting sebagai ADR singkat supaya prompt tahap berikutnya tidak mengubah fondasi tanpa alasan.
Untuk revisi, gunakan prompt berbasis kegagalan konkret: “Pada fixture A, satu cubitan menghasilkan dua respons; berikut input dan hasil yang diharapkan.” Instruksi seperti ini lebih dapat diperiksa daripada “buat lebih canggih.” Setelah perbaikan, minta regresi yang relevan, bukan seluruh rangkaian tes tanpa tujuan. Ketika perilaku sudah memenuhi kebutuhan dan risikonya cukup terverifikasi, lanjutkan ke hasil pengguna berikutnya.
Privasi, aksesibilitas, dan publikasi
Rancang pemrosesan frame pada perangkat bila memungkinkan dan jangan merekam video secara default. Backend cukup menerima hasil belajar yang diperlukan. Periksa lalu lintas jaringan untuk memastikan implementasi sesuai klaim tersebut.
Sediakan indikator kamera aktif dan tombol berhenti. Hindari perekaman ulang sesi atau screenshot otomatis pada area kamera untuk analytics. Data diagnostik seperti waktu muat dan kejadian tracking hilang biasanya dapat dicatat tanpa menyimpan gambar.
Untuk aksesibilitas, sediakan mode duduk, target besar, waktu fleksibel, instruksi teks, serta input alternatif. Kalibrasi yang baik menyesuaikan aplikasi pada pengguna.
Ketika aplikasi sudah diterbitkan, bagikan tautan kelas dengan cara yang sederhana. Salah satu opsi adalah mengubah URL menjadi kode melalui generator QR Code Tumbas.in, lalu menaruhnya pada lembar aktivitas. Kode tersebut membantu membuka halaman; izin kamera tetap diminta oleh browser saat fitur digunakan.
Referensi nyata untuk mempelajari pola interaksi
Move Mirror menunjukkan pencocokan pose webcam dengan gambar menggunakan TensorFlow.js dan PoseNet. Halaman resminya menyatakan eksperimen tersebut sudah tidak aktif, sehingga cocok dipelajari sebagai contoh historis.
Emoji Scavenger Hunt merupakan contoh permainan pencarian benda melalui kamera dengan TensorFlow.js. Repository-nya diarsipkan pada 6 Mei 2026. Konsepnya tetap dapat dipelajari, tetapi stack lama tidak perlu disalin tanpa evaluasi.
Untuk fondasi baru, contoh resmi MediaPipe dan dokumentasi model yang dipilih lebih tepat dijadikan titik awal teknis. Referensi lama menunjukkan kemungkinan interaksi, sementara keputusan implementasi mengikuti kebutuhan perangkat dan produk yang dibangun sekarang.
FAQ tentang game pembelajaran berbasis kamera
Apakah harus memakai kamera khusus?
Rancangan dalam artikel ini ditujukan untuk webcam laptop atau kamera ponsel. Perangkat sasaran tetap perlu diuji karena pencahayaan, kualitas kamera, browser, dan kemampuan komputasi memengaruhi pengalaman.
Apakah harus menggunakan React?
Tidak. Vanilla JavaScript atau TypeScript dapat digunakan untuk prototipe. React menjadi pilihan ketika UI dan pengelolaan halaman bertambah. Teknologi deteksi kamera tidak mengharuskan satu framework UI tertentu.
Apakah semua game membutuhkan TensorFlow.js?
Tidak. Game tangan, tubuh, dan gestur pada rancangan ini menggunakan MediaPipe Tasks Vision. TensorFlow.js dipakai pada jalur COCO-SSD atau model ekspor tertentu seperti Teachable Machine.
Apakah game dapat dibuat tanpa backend?
Versi latihan lokal dapat dirancang tanpa backend. Backend diperlukan jika ada akun, penugasan kelas, sinkronisasi perangkat, atau laporan terpusat. Kebutuhan tersebut sebaiknya diputuskan setelah mekanik inti berfungsi.
Bisakah digunakan secara offline?
Bisa dirancang untuk kondisi tertentu setelah aplikasi dan model tersimpan pada perangkat. Namun, cache, pembaruan, dan kondisi instalasi pertama harus diuji sebelum fitur offline dijanjikan kepada pengguna.
Apakah gambar dari kamera harus dikirim ke server?
Tidak harus. Arsitektur artikel ini mengutamakan pemrosesan lokal. Implementasi tetap perlu diperiksa karena analytics, debugging, atau fitur tambahan dapat mengubah aliran data.
Apakah deteksi gestur bisa langsung menjadi aplikasi bahasa isyarat?
Tidak otomatis. Beberapa label gestur bawaan tidak mewakili bahasa isyarat lengkap. Produk semacam itu memerlukan tujuan, data, model, serta evaluasi yang sesuai.
Apakah Gambar Bentuk dapat membaca semua tulisan tangan?
Tidak pada rancangan awal ini. Aktivitasnya membandingkan lintasan jari dengan template. Pengakuan tulisan bebas merupakan pekerjaan berbeda yang membutuhkan metode dan pengujian tersendiri.
Mengapa gerakan terkadang terbaca berulang?
Model dapat mengeluarkan hasil yang sama pada banyak frame. Aplikasi perlu mengubah rangkaian hasil itu menjadi satu tindakan, lalu meminta kondisi netral sebelum menerima tindakan berikutnya.
Apakah tampilan harus 3D agar menarik?
Tidak. Kartu, garis, avatar 2D, dan feedback yang jelas dapat menjelaskan aktivitas dengan baik. Gunakan 3D ketika membantu konsep ruang atau materi yang dipelajari.
Game mana yang sebaiknya dibuat terlebih dahulu?
Jepit Jawaban menjadi titik awal yang masuk akal untuk proyek ini. Mekaniknya menguji banyak komponen dasar, sementara materi dan jumlah kartu dapat dibatasi.
Apa ukuran keberhasilan proyek?
Peserta dapat menggunakan kontrol dengan nyaman, memahami instruksi, mendapatkan feedback yang benar, dan menunjukkan pemahaman materi. Animasi yang menarik membantu pengalaman, tetapi tidak menggantikan pengukuran tersebut.
Mulai dari satu aktivitas yang selesai dengan baik
Web pembelajaran berbasis kamera menjadi berguna ketika gerakan memiliki hubungan jelas dengan materi. Menjepit kartu membantu memilih representasi, menggambar jalur membantu mengamati bentuk, dan menyusun gestur membantu memahami urutan instruksi.
Bangun satu aktivitas yang dapat diselesaikan dari awal sampai hasil. Perbaiki kalibrasi, penanganan tracking, dan penjelasan jawaban. Setelah fondasinya stabil, gunakan kembali komponen tersebut untuk menambah game tanpa mengulang seluruh pekerjaan kamera.
Pendekatan bertahap memberi ruang untuk menguji hal yang paling penting: apakah teknologi membantu peserta memahami pelajaran dan apakah pengalaman itu tetap nyaman pada perangkat yang benar-benar mereka gunakan.
Sumber dan dokumentasi
Dokumentasi diperiksa pada 15 Oktober 2026. Contoh arsitektur, pilihan stack, serta jadwal dalam artikel merupakan usulan implementasi, bukan benchmark perangkat.
Komentar (0)
Belum ada komentar. Jadilah yang pertama berkomentar!
Tinggalkan Komentar