added character, jumping, running, cute falling animationgit add . love it so much

This commit is contained in:
Mak
2026-04-24 00:50:30 +09:00
parent df887cd7fc
commit e33f9a804f
5 changed files with 659 additions and 144 deletions

BIN
Falling Back Death.glb Normal file

Binary file not shown.

BIN
Jump.glb Normal file

Binary file not shown.

BIN
Running.glb Normal file

Binary file not shown.

View File

@@ -1,198 +1,428 @@
<script>
//@ts-nocheck - to stop annoying red lines of typescript check
import { onMount, onDestroy } from "svelte";
//onMount - do this the second the component appears on screen
//onDestroy - clean up after losing
// @ts-nocheck
import { onMount } from "svelte";
import * as THREE from "three";
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
import { clone as cloneSkeleton } from "three/examples/jsm/utils/SkeletonUtils.js";
//config for physics
const CONFIG = {
lane: 2.5, //distance between the lanes
jump: 0.35, //jump hight
grav: 0.015, //how fast to land after jump
speed: 0.3 //how fast world is moving towards us
//typically in infinite runner games the environment is moving, not the player as it may seem
};
lane: 2.5,
jump: 0.35,
grav: 0.015,
speed: 0.3,
gameOverDelay: 1400,
playerScale: 1.25
};
//variables to track the current state of the game
let score = 0;
let isPlaying = false;
let gameOver = false;
let startScreen = true;
let showGameOverModal = false;
//player position variables
let lane = 0; //target lane (-2 to 2 for 5 lanes)
let currX = 0; //actual X position used for smooth transition
let lane = 0;
let currX = 0;
let isJumping = false;
let jumpV = 0; //Jump velocity
let playerY = 0; //height off the ground
let jumpV = 0;
let playerY = 0;
//making the scene
let container, canvas, scene, camera, renderer, player, floor;
let container, canvas, scene, camera, renderer, floor;
let worldObjects = [];
let resizeObserver;
let animationFrame;
function init(){
// Creating the universe
scene = new THREE.Scene();
let isDying = false;
let hitFlash = false;
let shake = 0;
//Setup the field of view, aspect ration, near plane, far plane
camera = new THREE.PerspectiveCamera(60, 1, 0.1, 1000);
camera.position.set(0, 5, 10); //put the camera from the back of the player
camera.lookAt(0, 0, -5);
let playerAnchor;
let currentModel = null;
let currentMixer = null;
let currentAction = null;
let currentState = "run";
let swapToken = 0;
renderer = new THREE.WebGLRenderer({canvas, antialias: true}); //antialias makes edges smooth
renderer.setPixelRatio(window.devicePixelRatio);
const loader = new GLTFLoader();
const clock = new THREE.Clock();
const glbCache = new Map();
//adding lights to the scene
scene.add(new THREE.AmbientLight(0xffffff, 0.8)); //soft general light
const sun = new THREE.DirectionalLight(0xffffff, 1);
sun.position.set(5, 10, 7); //light from the side
scene.add(sun);
function normalizeModel(root) {
root.scale.setScalar(CONFIG.playerScale);
root.rotation.y = Math.PI;
//build the ground
const floorGeo = new THREE.PlaneGeometry(100, 2000);
floor = new THREE.Mesh(floorGeo, new THREE.MeshStandardMaterial({ color: 0x222222 }));
floor.rotation.x = -Math.PI /2; //so it is horizontal plane
scene.add(floor);
root.traverse((obj) => {
if (obj.isMesh || obj.isSkinnedMesh) {
obj.castShadow = true;
obj.receiveShadow = true;
obj.frustumCulled = false;
}
if (obj.isBone) {
obj.frustumCulled = false;
}
});
//to prevent game ratio from looking stretched when you resize the window
resizeObserver = new ResizeObserver(()=>{
const {width, height} = container.getBoundingClientRect();
renderer.setSize(width, height);
camera.aspect = width / height;
camera.updateProjectionMatrix();
});
resizeObserver.observe(container);
root.updateMatrixWorld(true);
const box = new THREE.Box3().setFromObject(root);
const center = new THREE.Vector3();
box.getCenter(center);
root.position.x -= center.x;
root.position.z -= center.z;
root.position.y -= box.min.y;
root.updateMatrixWorld(true);
return root;
}
async function getCachedGLTF(file) {
if (!glbCache.has(file)) {
const gltf = await loader.loadAsync(file);
glbCache.set(file, gltf);
}
return glbCache.get(file);
}
function disposeCurrentCharacter() {
if (currentMixer) {
currentMixer.stopAllAction();
}
if (currentModel && playerAnchor) {
playerAnchor.remove(currentModel);
}
currentModel = null;
currentMixer = null;
currentAction = null;
}
async function swapCharacter(file, stateName, { loop = true } = {}) {
const myToken = ++swapToken;
const source = await getCachedGLTF(file);
if (myToken !== swapToken) return;
const model = normalizeModel(cloneSkeleton(source.scene));
const mixer = new THREE.AnimationMixer(model);
let action = null;
if (source.animations && source.animations.length > 0) {
action = mixer.clipAction(source.animations[0]);
if (loop) {
action.setLoop(THREE.LoopRepeat, Infinity);
} else {
action.setLoop(THREE.LoopOnce, 1);
action.clampWhenFinished = true;
}
action.reset();
action.play();
}
disposeCurrentCharacter();
currentModel = model;
currentMixer = mixer;
currentAction = action;
currentState = stateName;
playerAnchor.add(currentModel);
}
function showRun() {
return swapCharacter("Running.glb", "run", { loop: true });
}
function showJump() {
return swapCharacter("Jumping.glb", "jump", { loop: false });
}
function showDeath() {
return swapCharacter("Falling Back Death.glb", "death", { loop: false });
}
function triggerGameOver() {
if (isDying || gameOver) return;
isPlaying = false;
gameOver = true;
isDying = true;
hitFlash = true;
shake = 0.35;
showDeath();
setTimeout(() => {
hitFlash = false;
}, 150);
setTimeout(() => {
showGameOverModal = true;
}, CONFIG.gameOverDelay);
}
function init() {
scene = new THREE.Scene();
camera = new THREE.PerspectiveCamera(60, 1, 0.1, 1000);
camera.position.set(0, 5, 10);
camera.lookAt(0, 0, -5);
renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
renderer.setPixelRatio(window.devicePixelRatio);
scene.add(new THREE.AmbientLight(0xffffff, 0.8));
const sun = new THREE.DirectionalLight(0xffffff, 1);
sun.position.set(5, 10, 7);
scene.add(sun);
const floorGeo = new THREE.PlaneGeometry(100, 2000);
floor = new THREE.Mesh(
floorGeo,
new THREE.MeshStandardMaterial({ color: 0x222222 })
);
floor.rotation.x = -Math.PI / 2;
scene.add(floor);
playerAnchor = new THREE.Group();
scene.add(playerAnchor);
resizeObserver = new ResizeObserver(() => {
const { width, height } = container.getBoundingClientRect();
renderer.setSize(width, height);
camera.aspect = width / height;
camera.updateProjectionMatrix();
});
resizeObserver.observe(container);
}
function spawn() {
const l = Math.floor(Math.random() * 5) - 2;
const mesh = new THREE.Mesh(
new THREE.BoxGeometry(1.5, 1.5, 1.5),
new THREE.MeshStandardMaterial({ color: 0x00fff2 })
);
mesh.position.set(l * CONFIG.lane, 0.75, -100);
scene.add(mesh);
worldObjects.push({ mesh, lane: l });
}
function update() {
if(!isPlaying || gameOver) return; //if paused or lost, do nothing
score++; //points for surviving longer
const delta = clock.getDelta();
//for smooth movement
//move 15% of the way every frame to switch to new lane
currX += (lane * CONFIG.lane - currX) * 0.15;
if (player) player.position.x = currX;
if (currentMixer) {
currentMixer.update(delta);
}
//Jumping math
if (isJumping) {
jumpV -= CONFIG.grav; //gravity pulls velocity down
playerY += jumpV; //Velocity moves the player
if (playerY <= 0){ //hit the ground
playerY = 0;
isJumping = false;
}
if(player) player.position.y = playerY;
if (shake > 0.001) {
shake *= 0.9;
camera.position.x = (Math.random() - 0.5) * shake;
} else {
shake = 0;
camera.position.x += (0 - camera.position.x) * 0.2;
}
if (playerAnchor) {
playerAnchor.position.set(currX, playerY, 0);
}
if (!isPlaying) return;
score++;
currX += (lane * CONFIG.lane - currX) * 0.15;
if (isJumping) {
jumpV -= CONFIG.grav;
playerY += jumpV;
if (playerY <= 0) {
playerY = 0;
isJumping = false;
if (!isDying) showRun();
}
//obstacle movement & collision
worldObjects = worldObjects
.map(obj => {
//move
obj.mesh.position.z += CONFIG.speed;
}
//detect collision
if ( //to see if we are close to the object
Math.abs(obj.mesh.position.z) < 0.8 &&
obj.lane === lane &&
playerY < 1
) {
isPlaying = false;
gameOver = true;
}
return obj;
worldObjects = worldObjects
.map((obj) => {
obj.mesh.position.z += CONFIG.speed;
if (
Math.abs(obj.mesh.position.z) < 0.8 &&
obj.lane === lane &&
playerY < 1
) {
triggerGameOver();
}
return obj;
})
.filter(obj => {
//delete obstacles behind us
const keep = obj.mesh.position.z <= 15;
if(!keep) scene.remove(obj.mesh);
return keep;
.filter((obj) => {
const keep = obj.mesh.position.z <= 15;
if (!keep) scene.remove(obj.mesh);
return keep;
});
// spawn new obstacles every 30 frames
if (score % 30 === 0) spawn();
if (score % 30 === 0) spawn();
}
function spawn(){
// choose a random lane btw -2 and 2
const l = Math.floor(Math.random() * 5) - 2;
const mesh = new THREE.Mesh(
new THREE.BoxGeometry(1.5, 1.5, 1.5),
new THREE.MeshStandardMaterial({ color: 0x00fff2 })
);
mesh.position.set(l * CONFIG.lane, 0.75, -100); //start far
scene.add(mesh);
worldObjects.push({ mesh, lane: l});
async function startGame() {
worldObjects.forEach((obj) => scene.remove(obj.mesh));
worldObjects = [];
score = 0;
isPlaying = true;
gameOver = false;
startScreen = false;
showGameOverModal = false;
lane = 0;
currX = 0;
isJumping = false;
jumpV = 0;
playerY = 0;
isDying = false;
hitFlash = false;
shake = 0;
if (playerAnchor) {
playerAnchor.position.set(0, 0, 0);
playerAnchor.rotation.set(0, 0, 0);
}
await showRun();
}
function startGame(){
//clear old obstacles
worldObjects.map(obj => scene.remove(obj.mesh));
worldObjects = [];
function handleKeyDown(e) {
if (!isPlaying || isDying) return;
//creating the player CUBE for now
if (player) scene.remove(player);
player = new THREE.Group();
player.add(new THREE.Mesh(new THREE.BoxGeometry(1, 1, 1), new THREE.MeshStandardMaterial({ color: 0xffffff })));
scene.add(player);
if (e.key === "ArrowLeft" && lane > -2) lane--;
if (e.key === "ArrowRight" && lane < 2) lane++;
//Resey all variables
score = 0; lane = 0; playerY = 0; currX = 0; isJumping = false;
startScreen = false; gameOver = false; isPlaying = true;
if ((e.key === " " || e.key === "ArrowUp") && !isJumping) {
isJumping = true;
jumpV = CONFIG.jump;
showJump();
}
}
onMount(() => {
init(); //build the world
init();
//the loop of rendering, updating, repeating
const loop = () => {
requestAnimationFrame(loop);
update();
renderer.render(scene, camera);
};
loop();
const loop = () => {
animationFrame = requestAnimationFrame(loop);
update();
renderer.render(scene, camera);
};
//listening to keys
window.addEventListener("keydown", (e) => {
if(!isPlaying) return;
if (e.key === "ArrowLeft" && lane > -2) lane--;
if (e.key === "ArrowRight" && lane < 2) lane ++;
if ((e.key === " " || e.key === "ArrowUp") && !isJumping) {
isJumping = true;
jumpV = CONFIG.jump;
}
});
loop();
window.addEventListener("keydown", handleKeyDown);
return () => {
cancelAnimationFrame(animationFrame);
window.removeEventListener("keydown", handleKeyDown);
resizeObserver?.disconnect();
if (currentMixer) currentMixer.stopAllAction();
renderer?.dispose();
};
});
onDestroy(() => resizeObserver?.disconnect()); //disconnect resizeobserver so it doesn;t leak memory adn cpu
</script>
<style>
/* Make the game full screen */
:global(body, html) { margin: 0; padding: 0; height: 100%; overflow: hidden; background: #000; }
#wrapper { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; }
canvas { width: 100% !important; height: 100% !important; display: block; }
/* The UI Overlay */
.ui { position: absolute; inset: 0; pointer-events: none; color: white; text-align: center; font-family: sans-serif; }
.modal { pointer-events: auto; background: rgba(0,0,0,0.9); padding: 40px; border-radius: 20px; margin-top: 20vh;}
button { padding: 15px 40px; background: #00fff2; border: none; cursor: pointer; font-weight: bold; }
:global(body, html) {
margin: 0;
padding: 0;
height: 100%;
overflow: hidden;
background: #000;
}
#wrapper {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
}
canvas {
width: 100% !important;
height: 100% !important;
display: block;
}
.ui {
position: absolute;
inset: 0;
pointer-events: none;
color: white;
text-align: center;
font-family: sans-serif;
}
.score {
font-size: 2rem;
margin-top: 20px;
}
.modal {
pointer-events: auto;
background: rgba(0, 0, 0, 0.9);
padding: 40px;
border-radius: 20px;
margin-top: 20vh;
border: 1px solid #00fff2;
display: inline-block;
min-width: 280px;
}
button {
padding: 15px 40px;
background: #00fff2;
border: none;
cursor: pointer;
font-weight: bold;
color: black;
text-transform: uppercase;
}
.flash {
position: absolute;
inset: 0;
pointer-events: none;
background: white;
opacity: 0;
animation: flash-hit 180ms ease-out forwards;
z-index: 10;
}
@keyframes flash-hit {
0% { opacity: 0.95; }
100% { opacity: 0; }
}
</style>
<div id="wrapper" bind:this={container}>
<canvas bind:this={canvas}></canvas>
{#if hitFlash}
<div class="flash"></div>
{/if}
<div class="ui">
<div style="font-size: 2rem; margin-top: 20px;">{score}</div>
{#if startScreen || gameOver}
<div class="score">{score}</div>
{#if startScreen || showGameOverModal}
<div class="modal">
<h1>{gameOver ? 'GAME OVER' : 'CUBE RUNNER'}</h1>
<p>{gameOver ? 'Final Score: ' + score : 'Arrows to Move • Space to Jump'}</p>
<button on:click={startGame}>{gameOver ? 'RETRY' : 'START'}</button>
<h1>{showGameOverModal ? "GAME OVER" : "RUNNER"}</h1>
<p>{showGameOverModal ? "Score: " + score : "Arrows to Move • Space to Jump"}</p>
<button on:click={startGame}>
{showGameOverModal ? "RETRY" : "START"}
</button>
</div>
{/if}
</div>
</div>
</div>

285
src/cool.svelte Normal file
View File

@@ -0,0 +1,285 @@
<script>
//@ts-nocheck
import { onMount, onDestroy } from "svelte";
import * as THREE from "three";
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
const CONFIG = {
lane: 2.5,
jump: 0.35,
grav: 0.015,
speed: 0.3
};
// UI State
let score = 0, isPlaying = false, gameOver = false, startScreen = true;
let hitFlash = false;
// Physics/Logic State
let lane = 0, currX = 0, isJumping = false, jumpV = 0, playerY = 0;
let isDying = false;
let shake = 0;
let fade = 1;
let container, canvas, scene, camera, renderer;
let worldObjects = [];
let resizeObserver;
// Character Refs
let playerGroup, mixer, currentAction, animations = [];
let clock = new THREE.Clock();
function playAnimation(name, loop = true) {
if (!mixer || !animations.length) return;
const clip = animations.find(a => a.name === name);
if (!clip) return;
const newAction = mixer.clipAction(clip);
if (currentAction === newAction) return;
if (currentAction) currentAction.fadeOut(0.2);
newAction.reset()
.setEffectiveTimeScale(1)
.setEffectiveWeight(1)
.fadeIn(0.2)
.play();
newAction.setLoop(loop ? THREE.LoopRepeat : THREE.LoopOnce);
newAction.clampWhenFinished = !loop;
currentAction = newAction;
}
function init() {
scene = new THREE.Scene();
camera = new THREE.PerspectiveCamera(60, 1, 0.1, 1000);
camera.position.set(0, 5, 10);
camera.lookAt(0, 0, -5);
renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
renderer.setPixelRatio(window.devicePixelRatio);
scene.add(new THREE.AmbientLight(0xffffff, 0.5));
const sun = new THREE.DirectionalLight(0x00FFD1, 1.2);
sun.position.set(5, 10, 7);
scene.add(sun);
const floorGeo = new THREE.PlaneGeometry(100, 2000);
const floor = new THREE.Mesh(floorGeo, new THREE.MeshStandardMaterial({ color: 0x0a0a0a, roughness: 0.2, metalness: 0.5 }));
floor.rotation.x = -Math.PI / 2;
scene.add(floor);
const loader = new GLTFLoader();
// Using the RobotExpressive model from your logic reference
loader.load('https://threejs.org/examples/models/gltf/RobotExpressive/RobotExpressive.glb', (gltf) => {
const model = gltf.scene;
model.scale.set(0.4, 0.4, 0.4);
model.rotation.y = Math.PI;
playerGroup = new THREE.Group();
playerGroup.add(model);
scene.add(playerGroup);
animations = gltf.animations;
mixer = new THREE.AnimationMixer(model);
playAnimation('Running');
});
resizeObserver = new ResizeObserver(() => {
const { width, height } = container.getBoundingClientRect();
renderer.setSize(width, height);
camera.aspect = width / height;
camera.updateProjectionMatrix();
});
resizeObserver.observe(container);
}
function update() {
const delta = clock.getDelta();
if (mixer) mixer.update(delta);
// 1. Camera Shake Logic
if (shake > 0) {
shake -= 0.05;
camera.position.x = (Math.random() - 0.5) * shake;
camera.position.y = 5 + (Math.random() - 0.5) * shake;
} else {
camera.position.x = 0;
camera.position.y = 5;
}
// 2. Death Fade Logic
if (isDying && fade > 0) {
fade -= 0.005;
playerGroup?.traverse((child) => {
if (child.isMesh) {
child.material.transparent = true;
child.material.opacity = fade;
}
});
}
// Stop physics if not playing or dying
if (!isPlaying || isDying || gameOver) return;
score++;
// Smooth movement
currX += (lane * CONFIG.lane - currX) * 0.15;
if (playerGroup) playerGroup.position.x = currX;
// Jump Physics
if (isJumping) {
jumpV -= CONFIG.grav;
playerY += jumpV;
if (playerY <= 0) {
playerY = 0;
isJumping = false;
}
if (playerGroup) playerGroup.position.y = playerY;
}
// Obstacles & Collision
worldObjects = worldObjects.map(obj => {
obj.mesh.position.z += CONFIG.speed;
if (Math.abs(obj.mesh.position.z) < 0.8 && obj.lane === lane && playerY < 1) {
// --- TRIGGER DEATH SEQUENCE ---
isDying = true;
shake = 0.5;
hitFlash = true;
playAnimation('Death', false);
setTimeout(() => { hitFlash = false; }, 150);
setTimeout(() => {
isPlaying = false;
gameOver = true;
}, 3000);
}
return obj;
}).filter(obj => {
const keep = obj.mesh.position.z <= 15;
if (!keep) scene.remove(obj.mesh);
return keep;
});
if (score % 30 === 0) spawn();
}
function spawn() {
const l = Math.floor(Math.random() * 5) - 2;
const mesh = new THREE.Mesh(
new THREE.BoxGeometry(1.5, 1.5, 1.5),
new THREE.MeshStandardMaterial({
color: 0xFF0055,
emissive: 0xFF0055,
emissiveIntensity: 0.5
})
);
mesh.position.set(l * CONFIG.lane, 0.75, -100);
scene.add(mesh);
worldObjects.push({ mesh, lane: l });
}
function startGame() {
worldObjects.forEach(obj => scene.remove(obj.mesh));
worldObjects = [];
score = 0; lane = 0; playerY = 0; currX = 0;
isJumping = false; isDying = false; gameOver = false;
fade = 1; shake = 0; startScreen = false; isPlaying = true;
if (playerGroup) {
playerGroup.position.set(0, 0, 0);
playerGroup.traverse(child => {
if (child.isMesh) child.material.opacity = 1;
});
playAnimation('Running');
}
}
onMount(() => {
init();
const loop = () => {
requestAnimationFrame(loop);
update();
renderer.render(scene, camera);
};
loop();
window.addEventListener("keydown", (e) => {
if (!isPlaying || isDying) return;
if (e.key === "ArrowLeft" && lane > -2) lane--;
if (e.key === "ArrowRight" && lane < 2) lane++;
if ((e.key === " " || e.key === "ArrowUp") && !isJumping) {
isJumping = true;
jumpV = CONFIG.jump;
playAnimation('Jump', false);
// Switch back to running after jump time (approx 800ms)
setTimeout(() => { if(!isDying) playAnimation('Running'); }, 800);
}
});
});
onDestroy(() => resizeObserver?.disconnect());
</script>
<style>
:global(body, html) { margin: 0; padding: 0; height: 100%; overflow: hidden; background: #050505; }
#wrapper { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; }
canvas { width: 100% !important; height: 100% !important; display: block; }
.flash { position: absolute; inset: 0; background: white; z-index: 100; pointer-events: none; }
.ui { position: absolute; inset: 0; pointer-events: none; color: white; text-align: center; font-family: sans-serif; z-index: 50; }
.modal {
pointer-events: auto;
background: rgba(0,0,0,0.95);
padding: 60px;
border: 1px solid #FF0055;
box-shadow: 0 0 50px rgba(255, 0, 85, 0.2);
margin-top: 10vh;
}
.score-hud { font-size: 5rem; font-weight: 900; color: #00FFD1; margin-top: 20px; font-style: italic; }
button {
padding: 20px 60px;
background: #FF0055;
border: none;
cursor: pointer;
font-weight: 900;
color: white;
text-transform: uppercase;
font-style: italic;
font-size: 1.5rem;
}
</style>
<div id="wrapper" bind:this={container}>
<canvas bind:this={canvas}></canvas>
{#if hitFlash}
<div class="flash"></div>
{/if}
<div class="ui">
{#if isPlaying && !gameOver}
<div class="score-hud">{score}</div>
{/if}
{#if startScreen || gameOver}
<div class="modal">
<h1 style="font-size: 4rem; font-style: italic; font-weight: 900;">
{gameOver ? 'SYSTEM FAILURE' : 'CYBER RUN'}
</h1>
<p style="letter-spacing: 5px; opacity: 0.6; margin-bottom: 40px;">
{gameOver ? 'FINAL SCORE: ' + score : 'NEURAL LINK READY'}
</p>
<button on:click={startGame}>{gameOver ? 'RE-INITIATE' : 'START SIMULATION'}</button>
</div>
{/if}
</div>
</div>