Skip to content

Commit 34c1654

Browse files
committed
Added a sample game created with three.js 🎲
1 parent bf6106d commit 34c1654

12 files changed

Lines changed: 418 additions & 16 deletions

File tree

‎dist/index.html‎

Lines changed: 10 additions & 2 deletions
Large diffs are not rendered by default.

‎dist/play/2048/index.html‎

Lines changed: 8 additions & 2 deletions
Large diffs are not rendered by default.

‎dist/play/chess/index.html‎

Lines changed: 8 additions & 2 deletions
Large diffs are not rendered by default.

‎dist/play/memory-match/index.html‎

Lines changed: 8 additions & 2 deletions
Large diffs are not rendered by default.

‎dist/play/snake/index.html‎

Lines changed: 8 additions & 2 deletions
Large diffs are not rendered by default.

‎dist/play/space-shooter/index.html‎

Lines changed: 8 additions & 2 deletions
Large diffs are not rendered by default.

‎dist/play/tetris/index.html‎

Lines changed: 8 additions & 2 deletions
Large diffs are not rendered by default.

‎package-lock.json‎

Lines changed: 8 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎package.json‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
},
1111
"dependencies": {
1212
"astro": "^5.16.3",
13-
"lucide-astro": "^0.555.0"
13+
"lucide-astro": "^0.555.0",
14+
"three": "^0.181.2"
1415
}
1516
}

‎src/games/NeonRunner.astro‎

Lines changed: 338 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,338 @@
1+
---
2+
3+
---
4+
5+
<div id="game-container">
6+
<div id="score">Score: 0</div>
7+
<div id="start-screen">
8+
<h2>Neon Runner</h2>
9+
<p>Tap or Press Space to Start</p>
10+
<p class="controls">Arrow Keys / Swipe to Move</p>
11+
</div>
12+
<div id="game-over-screen" class="hidden">
13+
<h2>Game Over</h2>
14+
<p id="final-score">Score: 0</p>
15+
<button id="restart-btn">Try Again</button>
16+
</div>
17+
</div>
18+
19+
<script>
20+
import * as THREE from "three";
21+
22+
// Game Configuration
23+
const CONFIG = {
24+
laneWidth: 3,
25+
speed: 0.5,
26+
speedIncrease: 0.0005,
27+
obstacleSpawnRate: 60, // Frames between spawns
28+
colors: {
29+
background: 0x111111,
30+
player: 0x00ffcc, // Neon Cyan
31+
obstacle: 0xff0066, // Neon Pink
32+
floor: 0x222222,
33+
grid: 0x444444,
34+
},
35+
};
36+
37+
// State
38+
let scene, camera, renderer;
39+
let player;
40+
let obstacles = [];
41+
let animationId;
42+
let score = 0;
43+
let isPlaying = false;
44+
let currentLane = 0; // -1, 0, 1
45+
let frameCount = 0;
46+
let gameSpeed = CONFIG.speed;
47+
48+
// DOM Elements
49+
const container = document.getElementById("game-container");
50+
const scoreEl = document.getElementById("score");
51+
const startScreen = document.getElementById("start-screen");
52+
const gameOverScreen = document.getElementById("game-over-screen");
53+
const finalScoreEl = document.getElementById("final-score");
54+
const restartBtn = document.getElementById("restart-btn");
55+
56+
function init() {
57+
// Scene Setup
58+
scene = new THREE.Scene();
59+
scene.background = new THREE.Color(CONFIG.colors.background);
60+
scene.fog = new THREE.Fog(CONFIG.colors.background, 10, 50);
61+
62+
// Camera
63+
camera = new THREE.PerspectiveCamera(
64+
60,
65+
container.clientWidth / container.clientHeight,
66+
0.1,
67+
100,
68+
);
69+
camera.position.set(0, 3, 6);
70+
camera.lookAt(0, 0, -5);
71+
72+
// Renderer
73+
renderer = new THREE.WebGLRenderer({ antialias: true });
74+
renderer.setSize(container.clientWidth, container.clientHeight);
75+
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
76+
container.appendChild(renderer.domElement);
77+
78+
// Lighting
79+
const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
80+
scene.add(ambientLight);
81+
82+
const dirLight = new THREE.DirectionalLight(0xffffff, 0.8);
83+
dirLight.position.set(5, 10, 7);
84+
scene.add(dirLight);
85+
86+
// Floor (Infinite Grid effect)
87+
const gridHelper = new THREE.GridHelper(
88+
100,
89+
100,
90+
CONFIG.colors.player,
91+
CONFIG.colors.grid,
92+
);
93+
gridHelper.position.y = -0.5;
94+
scene.add(gridHelper);
95+
96+
// Player
97+
const geometry = new THREE.BoxGeometry(1, 1, 1);
98+
const material = new THREE.MeshStandardMaterial({
99+
color: CONFIG.colors.player,
100+
emissive: CONFIG.colors.player,
101+
emissiveIntensity: 0.5,
102+
});
103+
player = new THREE.Mesh(geometry, material);
104+
player.position.y = 0;
105+
scene.add(player);
106+
107+
// Event Listeners
108+
window.addEventListener("resize", onWindowResize);
109+
document.addEventListener("keydown", onKeyDown);
110+
111+
// Touch Controls
112+
let touchStartX = 0;
113+
container.addEventListener(
114+
"touchstart",
115+
(e) => (touchStartX = e.changedTouches[0].screenX),
116+
);
117+
container.addEventListener("touchend", (e) => {
118+
const touchEndX = e.changedTouches[0].screenX;
119+
handleSwipe(touchStartX, touchEndX);
120+
});
121+
122+
// Start Game Listener
123+
container.addEventListener("click", () => {
124+
if (!isPlaying && gameOverScreen.classList.contains("hidden"))
125+
startGame();
126+
});
127+
document.addEventListener("keydown", (e) => {
128+
if (
129+
e.code === "Space" &&
130+
!isPlaying &&
131+
gameOverScreen.classList.contains("hidden")
132+
)
133+
startGame();
134+
});
135+
136+
restartBtn.addEventListener("click", resetGame);
137+
}
138+
139+
function startGame() {
140+
isPlaying = true;
141+
startScreen.classList.add("hidden");
142+
animate();
143+
}
144+
145+
function resetGame() {
146+
gameOverScreen.classList.add("hidden");
147+
startScreen.classList.remove("hidden");
148+
149+
// Reset State
150+
score = 0;
151+
gameSpeed = CONFIG.speed;
152+
currentLane = 0;
153+
player.position.x = 0;
154+
scoreEl.innerText = `Score: 0`;
155+
156+
// Clear Obstacles
157+
obstacles.forEach((obs) => scene.remove(obs));
158+
obstacles = [];
159+
160+
isPlaying = false;
161+
}
162+
163+
function gameOver() {
164+
isPlaying = false;
165+
cancelAnimationFrame(animationId);
166+
finalScoreEl.innerText = `Score: ${Math.floor(score)}`;
167+
gameOverScreen.classList.remove("hidden");
168+
}
169+
170+
function spawnObstacle() {
171+
const geometry = new THREE.BoxGeometry(1, 1, 1);
172+
const material = new THREE.MeshStandardMaterial({
173+
color: CONFIG.colors.obstacle,
174+
emissive: CONFIG.colors.obstacle,
175+
emissiveIntensity: 0.6,
176+
});
177+
const obstacle = new THREE.Mesh(geometry, material);
178+
179+
// Random Lane (-1, 0, 1)
180+
const lane = Math.floor(Math.random() * 3) - 1;
181+
obstacle.position.set(lane * CONFIG.laneWidth, 0, -50);
182+
183+
scene.add(obstacle);
184+
obstacles.push(obstacle);
185+
}
186+
187+
function handleSwipe(start, end) {
188+
if (!isPlaying) return;
189+
const threshold = 50;
190+
if (end < start - threshold) movePlayer(-1); // Left
191+
if (end > start + threshold) movePlayer(1); // Right
192+
}
193+
194+
function onKeyDown(event) {
195+
if (!isPlaying) return;
196+
if (event.key === "ArrowLeft") movePlayer(-1);
197+
if (event.key === "ArrowRight") movePlayer(1);
198+
}
199+
200+
function movePlayer(direction) {
201+
currentLane = Math.max(-1, Math.min(1, currentLane + direction));
202+
}
203+
204+
function onWindowResize() {
205+
if (!camera || !renderer) return;
206+
camera.aspect = container.clientWidth / container.clientHeight;
207+
camera.updateProjectionMatrix();
208+
renderer.setSize(container.clientWidth, container.clientHeight);
209+
}
210+
211+
function animate() {
212+
if (!isPlaying) return;
213+
animationId = requestAnimationFrame(animate);
214+
215+
// Update Player Position (Smooth Lerp)
216+
const targetX = currentLane * CONFIG.laneWidth;
217+
player.position.x += (targetX - player.position.x) * 0.1;
218+
219+
// Rotate Player
220+
player.rotation.x -= 0.05;
221+
player.rotation.y -= 0.02;
222+
223+
// Spawn Obstacles
224+
frameCount++;
225+
if (frameCount % CONFIG.obstacleSpawnRate === 0) {
226+
spawnObstacle();
227+
}
228+
229+
// Move Obstacles & Collision Detection
230+
for (let i = obstacles.length - 1; i >= 0; i--) {
231+
const obs = obstacles[i];
232+
obs.position.z += gameSpeed;
233+
234+
// Collision
235+
if (obs.position.z > -1 && obs.position.z < 1) {
236+
// Simple box collision check on X axis
237+
if (Math.abs(obs.position.x - player.position.x) < 0.8) {
238+
gameOver();
239+
}
240+
}
241+
242+
// Remove if passed
243+
if (obs.position.z > 5) {
244+
scene.remove(obs);
245+
obstacles.splice(i, 1);
246+
score += 10;
247+
scoreEl.innerText = `Score: ${score}`;
248+
249+
// Increase difficulty
250+
gameSpeed += CONFIG.speedIncrease;
251+
}
252+
}
253+
254+
renderer.render(scene, camera);
255+
}
256+
257+
// Initialize
258+
init();
259+
</script>
260+
261+
<style>
262+
#game-container {
263+
width: 100%;
264+
height: 600px;
265+
position: relative;
266+
overflow: hidden;
267+
border-radius: 12px;
268+
background: #000;
269+
}
270+
271+
#score {
272+
position: absolute;
273+
top: 20px;
274+
left: 20px;
275+
color: white;
276+
font-family: "Outfit", sans-serif;
277+
font-size: 1.5rem;
278+
font-weight: bold;
279+
z-index: 10;
280+
text-shadow: 0 0 10px #00ffcc;
281+
}
282+
283+
#start-screen,
284+
#game-over-screen {
285+
position: absolute;
286+
top: 50%;
287+
left: 50%;
288+
transform: translate(-50%, -50%);
289+
text-align: center;
290+
color: white;
291+
z-index: 20;
292+
background: rgba(0, 0, 0, 0.8);
293+
padding: 2rem;
294+
border-radius: 16px;
295+
border: 1px solid #333;
296+
backdrop-filter: blur(5px);
297+
}
298+
299+
.hidden {
300+
display: none;
301+
}
302+
303+
h2 {
304+
font-family: "Outfit", sans-serif;
305+
font-size: 2.5rem;
306+
margin-bottom: 0.5rem;
307+
background: linear-gradient(45deg, #00ffcc, #0099ff);
308+
-webkit-background-clip: text;
309+
-webkit-text-fill-color: transparent;
310+
}
311+
312+
p {
313+
font-family: "Inter", sans-serif;
314+
color: #ccc;
315+
margin-bottom: 1.5rem;
316+
}
317+
318+
.controls {
319+
font-size: 0.9rem;
320+
opacity: 0.7;
321+
}
322+
323+
button {
324+
background: linear-gradient(45deg, #00ffcc, #0099ff);
325+
border: none;
326+
padding: 0.8rem 2rem;
327+
border-radius: 50px;
328+
color: black;
329+
font-weight: bold;
330+
font-size: 1.1rem;
331+
cursor: pointer;
332+
transition: transform 0.2s;
333+
}
334+
335+
button:hover {
336+
transform: scale(1.05);
337+
}
338+
</style>

0 commit comments

Comments
 (0)