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