Initial game from internet: understood the logic and base, just made it svelte and 5 lanes
This commit is contained in:
198
src/App.svelte
Normal file
198
src/App.svelte
Normal file
@@ -0,0 +1,198 @@
|
||||
<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
|
||||
import * as THREE from "three";
|
||||
|
||||
//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
|
||||
};
|
||||
|
||||
//variables to track the current state of the game
|
||||
let score = 0;
|
||||
let isPlaying = false;
|
||||
let gameOver = false;
|
||||
let startScreen = true;
|
||||
|
||||
//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 isJumping = false;
|
||||
let jumpV = 0; //Jump velocity
|
||||
let playerY = 0; //height off the ground
|
||||
|
||||
//making the scene
|
||||
let container, canvas, scene, camera, renderer, player, floor;
|
||||
let worldObjects = [];
|
||||
let resizeObserver;
|
||||
|
||||
function init(){
|
||||
// Creating the universe
|
||||
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.position.set(0, 5, 10); //put the camera from the back of the player
|
||||
camera.lookAt(0, 0, -5);
|
||||
|
||||
renderer = new THREE.WebGLRenderer({canvas, antialias: true}); //antialias makes edges smooth
|
||||
renderer.setPixelRatio(window.devicePixelRatio);
|
||||
|
||||
//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);
|
||||
|
||||
//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);
|
||||
|
||||
//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);
|
||||
}
|
||||
|
||||
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(){
|
||||
// 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});
|
||||
}
|
||||
|
||||
function startGame(){
|
||||
//clear old obstacles
|
||||
worldObjects.map(obj => scene.remove(obj.mesh));
|
||||
worldObjects = [];
|
||||
|
||||
//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);
|
||||
|
||||
//Resey all variables
|
||||
score = 0; lane = 0; playerY = 0; currX = 0; isJumping = false;
|
||||
startScreen = false; gameOver = false; isPlaying = true;
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
init(); //build the world
|
||||
|
||||
//the loop of rendering, updating, repeating
|
||||
const loop = () => {
|
||||
requestAnimationFrame(loop);
|
||||
update();
|
||||
renderer.render(scene, camera);
|
||||
};
|
||||
loop();
|
||||
|
||||
//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;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
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; }
|
||||
</style>
|
||||
|
||||
<div id="wrapper" bind:this={container}>
|
||||
<canvas bind:this={canvas}></canvas>
|
||||
|
||||
<div class="ui">
|
||||
<div style="font-size: 2rem; margin-top: 20px;">{score}</div>
|
||||
|
||||
{#if startScreen || gameOver}
|
||||
<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>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
296
src/app.css
Normal file
296
src/app.css
Normal file
@@ -0,0 +1,296 @@
|
||||
:root {
|
||||
--text: #6b6375;
|
||||
--text-h: #08060d;
|
||||
--bg: #fff;
|
||||
--border: #e5e4e7;
|
||||
--code-bg: #f4f3ec;
|
||||
--accent: #aa3bff;
|
||||
--accent-bg: rgba(170, 59, 255, 0.1);
|
||||
--accent-border: rgba(170, 59, 255, 0.5);
|
||||
--social-bg: rgba(244, 243, 236, 0.5);
|
||||
--shadow:
|
||||
rgba(0, 0, 0, 0.1) 0 10px 15px -3px, rgba(0, 0, 0, 0.05) 0 4px 6px -2px;
|
||||
|
||||
--sans: system-ui, 'Segoe UI', Roboto, sans-serif;
|
||||
--heading: system-ui, 'Segoe UI', Roboto, sans-serif;
|
||||
--mono: ui-monospace, Consolas, monospace;
|
||||
|
||||
font: 18px/145% var(--sans);
|
||||
letter-spacing: 0.18px;
|
||||
color-scheme: light dark;
|
||||
color: var(--text);
|
||||
background: var(--bg);
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--text: #9ca3af;
|
||||
--text-h: #f3f4f6;
|
||||
--bg: #16171d;
|
||||
--border: #2e303a;
|
||||
--code-bg: #1f2028;
|
||||
--accent: #c084fc;
|
||||
--accent-bg: rgba(192, 132, 252, 0.15);
|
||||
--accent-border: rgba(192, 132, 252, 0.5);
|
||||
--social-bg: rgba(47, 48, 58, 0.5);
|
||||
--shadow:
|
||||
rgba(0, 0, 0, 0.4) 0 10px 15px -3px, rgba(0, 0, 0, 0.25) 0 4px 6px -2px;
|
||||
}
|
||||
|
||||
#social .button-icon {
|
||||
filter: invert(1) brightness(2);
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2 {
|
||||
font-family: var(--heading);
|
||||
font-weight: 500;
|
||||
color: var(--text-h);
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 56px;
|
||||
letter-spacing: -1.68px;
|
||||
margin: 32px 0;
|
||||
@media (max-width: 1024px) {
|
||||
font-size: 36px;
|
||||
margin: 20px 0;
|
||||
}
|
||||
}
|
||||
h2 {
|
||||
font-size: 24px;
|
||||
line-height: 118%;
|
||||
letter-spacing: -0.24px;
|
||||
margin: 0 0 8px;
|
||||
@media (max-width: 1024px) {
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
code,
|
||||
.counter {
|
||||
font-family: var(--mono);
|
||||
display: inline-flex;
|
||||
border-radius: 4px;
|
||||
color: var(--text-h);
|
||||
}
|
||||
|
||||
code {
|
||||
font-size: 15px;
|
||||
line-height: 135%;
|
||||
padding: 4px 8px;
|
||||
background: var(--code-bg);
|
||||
}
|
||||
|
||||
.counter {
|
||||
font-size: 16px;
|
||||
padding: 5px 10px;
|
||||
border-radius: 5px;
|
||||
color: var(--accent);
|
||||
background: var(--accent-bg);
|
||||
border: 2px solid transparent;
|
||||
transition: border-color 0.3s;
|
||||
margin-bottom: 24px;
|
||||
|
||||
&:hover {
|
||||
border-color: var(--accent-border);
|
||||
}
|
||||
&:focus-visible {
|
||||
outline: 2px solid var(--accent);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.hero {
|
||||
position: relative;
|
||||
|
||||
.base,
|
||||
.framework,
|
||||
.vite {
|
||||
inset-inline: 0;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.base {
|
||||
width: 170px;
|
||||
position: relative;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.framework,
|
||||
.vite {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.framework {
|
||||
z-index: 1;
|
||||
top: 34px;
|
||||
height: 28px;
|
||||
transform: perspective(2000px) rotateZ(300deg) rotateX(44deg) rotateY(39deg)
|
||||
scale(1.4);
|
||||
}
|
||||
|
||||
.vite {
|
||||
z-index: 0;
|
||||
top: 107px;
|
||||
height: 26px;
|
||||
width: auto;
|
||||
transform: perspective(2000px) rotateZ(300deg) rotateX(40deg) rotateY(39deg)
|
||||
scale(0.8);
|
||||
}
|
||||
}
|
||||
|
||||
#app {
|
||||
width: 1126px;
|
||||
max-width: 100%;
|
||||
margin: 0 auto;
|
||||
text-align: center;
|
||||
border-inline: 1px solid var(--border);
|
||||
min-height: 100svh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
#center {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 25px;
|
||||
place-content: center;
|
||||
place-items: center;
|
||||
flex-grow: 1;
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
padding: 32px 20px 24px;
|
||||
gap: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
#next-steps {
|
||||
display: flex;
|
||||
border-top: 1px solid var(--border);
|
||||
text-align: left;
|
||||
|
||||
& > div {
|
||||
flex: 1 1 0;
|
||||
padding: 32px;
|
||||
@media (max-width: 1024px) {
|
||||
padding: 24px 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.icon {
|
||||
margin-bottom: 16px;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
#docs {
|
||||
border-right: 1px solid var(--border);
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
border-right: none;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
}
|
||||
|
||||
#next-steps ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin: 32px 0 0;
|
||||
|
||||
.logo {
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--text-h);
|
||||
font-size: 16px;
|
||||
border-radius: 6px;
|
||||
background: var(--social-bg);
|
||||
display: flex;
|
||||
padding: 6px 12px;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
text-decoration: none;
|
||||
transition: box-shadow 0.3s;
|
||||
|
||||
&:hover {
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
.button-icon {
|
||||
height: 18px;
|
||||
width: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
margin-top: 20px;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
|
||||
li {
|
||||
flex: 1 1 calc(50% - 8px);
|
||||
}
|
||||
|
||||
a {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#spacer {
|
||||
height: 88px;
|
||||
border-top: 1px solid var(--border);
|
||||
@media (max-width: 1024px) {
|
||||
height: 48px;
|
||||
}
|
||||
}
|
||||
|
||||
.ticks {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
|
||||
&::before,
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -4.5px;
|
||||
border: 5px solid transparent;
|
||||
}
|
||||
|
||||
&::before {
|
||||
left: 0;
|
||||
border-left-color: var(--border);
|
||||
}
|
||||
&::after {
|
||||
right: 0;
|
||||
border-right-color: var(--border);
|
||||
}
|
||||
}
|
||||
BIN
src/assets/hero.png
Normal file
BIN
src/assets/hero.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
1
src/assets/svelte.svg
Normal file
1
src/assets/svelte.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="26.6" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 308"><path fill="#FF3E00" d="M239.682 40.707C211.113-.182 154.69-12.301 113.895 13.69L42.247 59.356a82.198 82.198 0 0 0-37.135 55.056a86.566 86.566 0 0 0 8.536 55.576a82.425 82.425 0 0 0-12.296 30.719a87.596 87.596 0 0 0 14.964 66.244c28.574 40.893 84.997 53.007 125.787 27.016l71.648-45.664a82.182 82.182 0 0 0 37.135-55.057a86.601 86.601 0 0 0-8.53-55.577a82.409 82.409 0 0 0 12.29-30.718a87.573 87.573 0 0 0-14.963-66.244"></path><path fill="#FFF" d="M106.889 270.841c-23.102 6.007-47.497-3.036-61.103-22.648a52.685 52.685 0 0 1-9.003-39.85a49.978 49.978 0 0 1 1.713-6.693l1.35-4.115l3.671 2.697a92.447 92.447 0 0 0 28.036 14.007l2.663.808l-.245 2.659a16.067 16.067 0 0 0 2.89 10.656a17.143 17.143 0 0 0 18.397 6.828a15.786 15.786 0 0 0 4.403-1.935l71.67-45.672a14.922 14.922 0 0 0 6.734-9.977a15.923 15.923 0 0 0-2.713-12.011a17.156 17.156 0 0 0-18.404-6.832a15.78 15.78 0 0 0-4.396 1.933l-27.35 17.434a52.298 52.298 0 0 1-14.553 6.391c-23.101 6.007-47.497-3.036-61.101-22.649a52.681 52.681 0 0 1-9.004-39.849a49.428 49.428 0 0 1 22.34-33.114l71.664-45.677a52.218 52.218 0 0 1 14.563-6.398c23.101-6.007 47.497 3.036 61.101 22.648a52.685 52.685 0 0 1 9.004 39.85a50.559 50.559 0 0 1-1.713 6.692l-1.35 4.116l-3.67-2.693a92.373 92.373 0 0 0-28.037-14.013l-2.664-.809l.246-2.658a16.099 16.099 0 0 0-2.89-10.656a17.143 17.143 0 0 0-18.398-6.828a15.786 15.786 0 0 0-4.402 1.935l-71.67 45.674a14.898 14.898 0 0 0-6.73 9.975a15.9 15.9 0 0 0 2.709 12.012a17.156 17.156 0 0 0 18.404 6.832a15.841 15.841 0 0 0 4.402-1.935l27.345-17.427a52.147 52.147 0 0 1 14.552-6.397c23.101-6.006 47.497 3.037 61.102 22.65a52.681 52.681 0 0 1 9.003 39.848a49.453 49.453 0 0 1-22.34 33.12l-71.664 45.673a52.218 52.218 0 0 1-14.563 6.398"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.9 KiB |
1
src/assets/vite.svg
Normal file
1
src/assets/vite.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 8.5 KiB |
5
src/lib/Counter.svelte
Normal file
5
src/lib/Counter.svelte
Normal file
@@ -0,0 +1,5 @@
|
||||
<script>
|
||||
let count = $state(0)
|
||||
</script>
|
||||
|
||||
<button class="counter" onclick={() => count++}>Count is {count}</button>
|
||||
9
src/main.js
Normal file
9
src/main.js
Normal file
@@ -0,0 +1,9 @@
|
||||
import { mount } from 'svelte'
|
||||
import './app.css'
|
||||
import App from './App.svelte'
|
||||
|
||||
const app = mount(App, {
|
||||
target: document.getElementById('app'),
|
||||
})
|
||||
|
||||
export default app
|
||||
Reference in New Issue
Block a user