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;
let isDying = false;
let hitFlash = false;
let shake = 0;
let playerAnchor;
let currentModel = null;
let currentMixer = null;
let currentAction = null;
let currentState = "run";
let swapToken = 0;
const loader = new GLTFLoader();
const clock = new THREE.Clock();
const glbCache = new Map();
function normalizeModel(root) {
root.scale.setScalar(CONFIG.playerScale);
root.rotation.y = Math.PI;
root.traverse((obj) => {
if (obj.isMesh || obj.isSkinnedMesh) {
obj.castShadow = true;
obj.receiveShadow = true;
obj.frustumCulled = false;
}
if (obj.isBone) {
obj.frustumCulled = false;
}
});
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() { function init() {
// Creating the universe
scene = new THREE.Scene(); scene = new THREE.Scene();
//Setup the field of view, aspect ration, near plane, far plane
camera = new THREE.PerspectiveCamera(60, 1, 0.1, 1000); 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.position.set(0, 5, 10);
camera.lookAt(0, 0, -5); camera.lookAt(0, 0, -5);
renderer = new THREE.WebGLRenderer({canvas, antialias: true}); //antialias makes edges smooth renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
renderer.setPixelRatio(window.devicePixelRatio); renderer.setPixelRatio(window.devicePixelRatio);
//adding lights to the scene scene.add(new THREE.AmbientLight(0xffffff, 0.8));
scene.add(new THREE.AmbientLight(0xffffff, 0.8)); //soft general light
const sun = new THREE.DirectionalLight(0xffffff, 1); const sun = new THREE.DirectionalLight(0xffffff, 1);
sun.position.set(5, 10, 7); //light from the side sun.position.set(5, 10, 7);
scene.add(sun); scene.add(sun);
//build the ground
const floorGeo = new THREE.PlaneGeometry(100, 2000); const floorGeo = new THREE.PlaneGeometry(100, 2000);
floor = new THREE.Mesh(floorGeo, new THREE.MeshStandardMaterial({ color: 0x222222 })); floor = new THREE.Mesh(
floor.rotation.x = -Math.PI /2; //so it is horizontal plane floorGeo,
new THREE.MeshStandardMaterial({ color: 0x222222 })
);
floor.rotation.x = -Math.PI / 2;
scene.add(floor); scene.add(floor);
//to prevent game ratio from looking stretched when you resize the window playerAnchor = new THREE.Group();
scene.add(playerAnchor);
resizeObserver = new ResizeObserver(() => { resizeObserver = new ResizeObserver(() => {
const { width, height } = container.getBoundingClientRect(); const { width, height } = container.getBoundingClientRect();
renderer.setSize(width, height); renderer.setSize(width, height);
camera.aspect = width / height; camera.aspect = width / height;
camera.updateProjectionMatrix(); camera.updateProjectionMatrix();
}); });
resizeObserver.observe(container); resizeObserver.observe(container);
} }
function update() {
if(!isPlaying || gameOver) return; //if paused or lost, do nothing
score++; //points for surviving longer
//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;
//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;
}
//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;
})
.filter(obj => {
//delete obstacles behind us
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();
}
function spawn() { function spawn() {
// choose a random lane btw -2 and 2
const l = Math.floor(Math.random() * 5) - 2; const l = Math.floor(Math.random() * 5) - 2;
const mesh = new THREE.Mesh( const mesh = new THREE.Mesh(
new THREE.BoxGeometry(1.5, 1.5, 1.5), new THREE.BoxGeometry(1.5, 1.5, 1.5),
new THREE.MeshStandardMaterial({ color: 0x00fff2 }) new THREE.MeshStandardMaterial({ color: 0x00fff2 })
); );
mesh.position.set(l * CONFIG.lane, 0.75, -100); //start far mesh.position.set(l * CONFIG.lane, 0.75, -100);
scene.add(mesh); scene.add(mesh);
worldObjects.push({ mesh, lane: l }); worldObjects.push({ mesh, lane: l });
} }
function startGame(){ function update() {
//clear old obstacles const delta = clock.getDelta();
worldObjects.map(obj => scene.remove(obj.mesh));
worldObjects = [];
//creating the player CUBE for now if (currentMixer) {
if (player) scene.remove(player); currentMixer.update(delta);
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
score = 0; lane = 0; playerY = 0; currX = 0; isJumping = false;
startScreen = false; gameOver = false; isPlaying = true;
} }
onMount(() => { if (shake > 0.001) {
init(); //build the world shake *= 0.9;
camera.position.x = (Math.random() - 0.5) * shake;
} else {
shake = 0;
camera.position.x += (0 - camera.position.x) * 0.2;
}
//the loop of rendering, updating, repeating if (playerAnchor) {
const loop = () => { playerAnchor.position.set(currX, playerY, 0);
requestAnimationFrame(loop); }
update();
renderer.render(scene, camera);
};
loop();
//listening to keys
window.addEventListener("keydown", (e) => {
if (!isPlaying) return; 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();
}
}
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) => {
const keep = obj.mesh.position.z <= 15;
if (!keep) scene.remove(obj.mesh);
return keep;
});
if (score % 30 === 0) spawn();
}
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 handleKeyDown(e) {
if (!isPlaying || isDying) return;
if (e.key === "ArrowLeft" && lane > -2) lane--; if (e.key === "ArrowLeft" && lane > -2) lane--;
if (e.key === "ArrowRight" && lane < 2) lane++; if (e.key === "ArrowRight" && lane < 2) lane++;
if ((e.key === " " || e.key === "ArrowUp") && !isJumping) { if ((e.key === " " || e.key === "ArrowUp") && !isJumping) {
isJumping = true; isJumping = true;
jumpV = CONFIG.jump; jumpV = CONFIG.jump;
showJump();
}
} }
});
});
onDestroy(() => resizeObserver?.disconnect()); //disconnect resizeobserver so it doesn;t leak memory adn cpu onMount(() => {
init();
const loop = () => {
animationFrame = requestAnimationFrame(loop);
update();
renderer.render(scene, camera);
};
loop();
window.addEventListener("keydown", handleKeyDown);
return () => {
cancelAnimationFrame(animationFrame);
window.removeEventListener("keydown", handleKeyDown);
resizeObserver?.disconnect();
if (currentMixer) currentMixer.stopAllAction();
renderer?.dispose();
};
});
</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>