Compare commits

..

2 Commits

Author SHA1 Message Date
3b019607ec add README 2026-04-29 23:52:04 +09:00
73130dd8ce optimazed camera controll again
Co-authored-by: Copilot <copilot@github.com>
2026-04-29 23:09:07 +09:00
9 changed files with 97 additions and 61 deletions

View File

@@ -1,10 +1,48 @@
# svelte-P5 Play # Nubzuki Jump
- Name: Tomas Horsky
A simple template to get you started with p5 svelte and the [p5play](https://p5play.org/index.html) library. - ID: 20256426
- Email: tomashorsky@kaist.ac.kr
- Git Repository: https://git.prototyping.id/20256426/NubzukiJump
- Demo Video:
## How to use ## How to use
```bash ```bash
npm install npm install
npm run dev npm run dev
``` ```
## Description of the game
<img src="./public/assets/nubzuki.png" width="140" align="right" style="margin-left: 30px;" />
Nubzuki Jump is a simple 2D platformer game where the player controls a character named Nubzuki. The goal is to jump on platforms and reach as high as possible without falling down. The game features four different types of platforms, which are introduced as the player progresses, the types of platforms are:
1. **Basic platform**
2. **Moving platform** a platform that moves horizontally
3. **Spring platform** boosts the player's jump
4. **One-time platform** disappears after one jump
The player can move only left and right, the jumping is done automatically whenever player lands on platform. Game is controlled either by using the **arrow keys**, or by enabling **camera control** and moving head left or right.
## Code Organization
The code is divided into two main parts, the Svelte app and the game logic inside of `src/game`. The Svelte app is responsible for layout of the page, displaying the leaderboard and handling the game state (start, end, restart). It also contains one component `CameraControl.svelte` which is responsible for enabling camera control and showing reading of head movement.
The game logic is divided into several files:
1. `game.js` - the main file which initializes the game and handles the game loop
2. `player.js` - contains the logic related to the player character, such as movement and player state
3. `platforms.js` - contains logic for creating and managing platforms
4. `platformTypes.js` - handles the creation and behavior of of platforms by type
5. `cameraControl.js` - contains the logic for enabling camera control and reading head movement
6. `constants.js` - contains constants for the game
How that different parts of the code are conected and are communicating can be seen in the following diagram:
<div style="text-align: left;">
<img src="./public/assets/organization_diagram.png"
alt="Organization diagram"
width="500">
</div>
## Issues
The issue I was not able to solve is that when using camera control, the game becomes slower and a little bit laggy. I improved it by lowering the camera resolution and reducing the detection frequency, however its still not perfect but playeble. Other then that, the colisons between player and edges of platform are not always perfect, but nothing terrible.
## Resources and Acknowledgements
- [P5play tutorial](https://p5play.org/learn/sprite) and [P5play documentation](https://p5play.org/docs) - to help me with p5play
- **Github copilot** - to help me write code faster
- **ChatGPT** - mainly for camera controls and for finding bugs in my code

Binary file not shown.

Before

Width:  |  Height:  |  Size: 131 KiB

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

View File

@@ -7,7 +7,7 @@
<title>Svelte app</title> <title>Svelte app</title>
<link rel='icon' type='image/png' href='/favicon.png'> <link rel='icon' type='image/png' href='/assets/nubzuki.png'>
<link rel='stylesheet' href='/global.css'> <link rel='stylesheet' href='/global.css'>
<link rel='stylesheet' href='/build/bundle.css'> <link rel='stylesheet' href='/build/bundle.css'>

View File

@@ -1,91 +1,89 @@
import { FaceDetector, FilesetResolver } from '@mediapipe/tasks-vision'; import { FaceLandmarker, FilesetResolver } from '@mediapipe/tasks-vision';
export const cameraInput = { export const cameraInput = {
left: false, left: false,
right: false, right: false,
active: false, active: false,
zone: 'center', zone: 'center'
x: 0.5
}; };
let video; let video;
let faceDetector; let faceLandmarker;
let lastTime = 0;
// 🔥 NARROW CENTER ZONE: const DETECTION_INTERVAL = 60;
// 0.48 and 0.52 means the center is only 4% of the screen width.
// Adjust these if it's still too hard to trigger movement.
const LEFT_THRESHOLD = 0.48;
const RIGHT_THRESHOLD = 0.52;
export async function initCameraControl() { export async function initCameraControl() {
if (cameraInput.active) return video; if (cameraInput.active)
return video;
video = document.createElement('video'); video = document.createElement('video');
video.autoplay = true; video.autoplay = true;
video.playsInline = true; video.playsInline = true;
video.muted = true; video.muted = true;
// Set low resolution and frame rate for better performance
const stream = await navigator.mediaDevices.getUserMedia({ const stream = await navigator.mediaDevices.getUserMedia({
video: { video: {
width: 160, width: { ideal: 160 },
height: 120, height: { ideal: 120 },
frameRate: { ideal: 20 } // Slightly higher for faster reaction frameRate: { ideal: 30 }
} }
}); });
video.srcObject = stream; video.srcObject = stream;
//load models for face landmark detection
const vision = await FilesetResolver.forVisionTasks( const vision = await FilesetResolver.forVisionTasks(
'https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision/wasm' 'https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision/wasm'
); );
faceLandmarker = await FaceLandmarker.createFromOptions(vision, {
faceDetector = await FaceDetector.createFromOptions(vision, {
baseOptions: { baseOptions: {
modelAssetPath: `https://storage.googleapis.com/mediapipe-models/face_detector/blaze_face_short_range/float16/1/blaze_face_short_range.task`, modelAssetPath:
delegate: 'GPU' 'https://storage.googleapis.com/mediapipe-models/face_landmarker/face_landmarker/float16/latest/face_landmarker.task',
}, },
runningMode: 'VIDEO' runningMode: 'VIDEO',
}); numFaces: 1
video.addEventListener('loadeddata', () => {
cameraInput.active = true;
startDetectionLoop();
}); });
cameraInput.active = true;
return video; return video;
} }
function startDetectionLoop() { export function updateCameraControl() {
const detect = (now) => { const now = performance.now();
const result = faceDetector.detectForVideo(video, now);
if (result.detections.length > 0) { // Limit detection frequency to improve performance
const box = result.detections[0].boundingBox; if (now - lastTime < DETECTION_INTERVAL)
// Calculate raw center point (0 to 1) return;
const rawX = (box.originX + box.width / 2) / 160; lastTime = now;
cameraInput.x = rawX; if (!faceLandmarker || !video || video.readyState < 2)
return;
// INSTANT LOGIC (No smoothing) const result = faceLandmarker.detectForVideo(video, now);
if (rawX < LEFT_THRESHOLD) {
updateZones(false, true, 'right'); // Mirrored: Face on left of cam = move right
} else if (rawX > RIGHT_THRESHOLD) {
updateZones(true, false, 'left'); // Mirrored: Face on right of cam = move left
} else {
updateZones(false, false, 'center');
}
} else {
updateZones(false, false, 'center');
}
video.requestVideoFrameCallback(detect); // If no face is detected, reset to center
}; if (!result.faceLandmarks.length) {
cameraInput.left = false;
cameraInput.right = false;
cameraInput.zone = 'center';
return;
}
video.requestVideoFrameCallback(detect); // look at the nose and determine zone
} const x = result.faceLandmarks[0][1].x;
if (x < 0.4) {
function updateZones(l, r, z) { cameraInput.left = false;
cameraInput.left = l; cameraInput.right = true;
cameraInput.right = r; cameraInput.zone = 'right';
cameraInput.zone = z; } else if (x > 0.6) {
cameraInput.left = true;
cameraInput.right = false;
cameraInput.zone = 'left';
} else {
cameraInput.left = false;
cameraInput.right = false;
cameraInput.zone = 'center';
}
} }

View File

@@ -4,14 +4,14 @@ import { cameraInput } from './cameraControl.js';
export function createPlayer() { export function createPlayer() {
const player = new Sprite(); const player = new Sprite();
player.scale = 0.20; player.scale = 0.13;
player.img = "assets/nubzuki.png"; player.img = "assets/nubzuki.png";
player.bounciness = 0; player.bounciness = 0;
player.rotationLock = true; player.rotationLock = true;
player.w = 20; player.w = 25;
player.h = 20; player.h = 20;
player.offset.y = 25; player.offset.y = 30;
player.elevation = 0; player.elevation = 0;
player.x = width / 2; player.x = width / 2;
player.y = height - 80; player.y = height - 80;