Skip to content

Instantly share code, notes, and snippets.

@juliendkim
Last active February 5, 2026 06:23
Show Gist options
  • Select an option

  • Save juliendkim/1aac383ddd539045e3f4be1a995a2d66 to your computer and use it in GitHub Desktop.

Select an option

Save juliendkim/1aac383ddd539045e3f4be1a995a2d66 to your computer and use it in GitHub Desktop.
[Game] Galaga like Shooter with React
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Galaga like Shooter</title>
<!-- React & ReactDOM -->
<script crossorigin src="https://unpkg.com/react@18/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
<!-- Babel for JSX -->
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<style>
body {
margin: 0;
background-color: #0a0a14;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
height: 100vh;
color: white;
font-family: Arial, sans-serif;
overflow: hidden;
touch-action: none;
}
#root {
position: relative;
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
}
canvas {
background-color: #0a0a14;
display: block;
}
.ui-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
z-index: 10;
}
.game-over-box {
background: rgba(0, 0, 0, 0.8);
padding: 40px;
border-radius: 10px;
text-align: center;
border: 1px solid #444;
pointer-events: auto;
}
h1 { margin: 0 0 20px 0; color: #ff3232; font-size: 48px; }
p { font-size: 24px; margin-bottom: 20px; }
.restart-btn {
padding: 10px 30px;
font-size: 20px;
background: #ff3232;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
}
.restart-btn:hover { background: #ff5050; }
.score-board {
position: absolute;
top: 20px;
left: 20px;
font-size: 24px;
font-weight: bold;
text-shadow: 2px 2px 2px black;
z-index: 5;
}
#mobile-controls {
position: absolute;
bottom: 30px;
left: 30px;
width: 15vw;
height: 15vw;
max-width: 120px;
max-height: 120px;
display: flex;
justify-content: center;
align-items: center;
pointer-events: none;
z-index: 20;
}
.joystick-zone {
width: 100%;
height: 100%;
background: rgba(255, 255, 255, 0.1);
border-radius: 50%;
position: relative;
pointer-events: auto;
touch-action: none;
display: flex;
justify-content: center;
align-items: center;
border: 2px solid rgba(255, 255, 255, 0.2);
}
.joystick-stick {
width: 40%;
height: 40%;
background: rgba(255, 255, 255, 0.3);
border-radius: 50%;
position: absolute;
pointer-events: none;
}
@media (min-width: 768px) {
#mobile-controls {
display: none;
}
}
</style>
</head>
<body>
<div id="root"></div>
<script type="text/babel">
const { useState, useEffect, useRef } = React;
// --- Game Logic Component ---
function Game() {
const canvasRef = useRef(null);
const joyStickRef = useRef(null);
// React state for UI overlays (Game Over, Score)
const [gameState, setGameState] = useState('playing');
const [score, setScore] = useState(0);
const [dimensions, setDimensions] = useState({
width: window.innerWidth,
height: window.innerHeight
});
// Mutable Game State (Refs)
const gameStateRef = useRef('playing');
const frameId = useRef(0);
const keys = useRef({});
const joyStart = useRef(null);
const mobileMoveX = useRef(0);
const player = useRef({
x: window.innerWidth / 2,
y: window.innerHeight - 100,
width: 40,
height: 30,
speed: 7,
lastShot: 0,
shootDelay: 250
});
const bullets = useRef([]);
const enemies = useRef([]);
const particles = useRef([]);
const stars = useRef([]);
const scoreRef = useRef(0);
const spawnTimer = useRef(0);
const spawnRate = useRef(60);
// Resize Handler
useEffect(() => {
const handleResize = () => {
const w = window.innerWidth;
const h = window.innerHeight;
setDimensions({ width: w, height: h });
player.current.y = h - 100;
if (player.current.x > w) player.current.x = w / 2;
};
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
// Sync Ref with State
useEffect(() => {
gameStateRef.current = gameState;
}, [gameState]);
// --- Initialization ---
const initGame = () => {
player.current.x = window.innerWidth / 2;
player.current.y = window.innerHeight - 100;
bullets.current = [];
enemies.current = [];
particles.current = [];
scoreRef.current = 0;
setScore(0);
spawnRate.current = 90;
spawnTimer.current = 0;
mobileMoveX.current = 0;
// Generate Stars
stars.current = [];
for(let i=0; i<100; i++) {
stars.current.push({
x: Math.random() * window.innerWidth,
y: Math.random() * window.innerHeight,
speed: 0.5 + Math.random() * 2.5,
size: 1 + Math.random(),
brightness: 100 + Math.random() * 155
});
}
setGameState('playing');
};
// --- Loop Management ---
useEffect(() => {
if (gameState === 'playing') {
frameId.current = requestAnimationFrame(gameLoop);
}
return () => cancelAnimationFrame(frameId.current);
}, [gameState, dimensions]);
useEffect(() => {
initGame();
const handleKeyDown = (e) => keys.current[e.code] = true;
const handleKeyUp = (e) => keys.current[e.code] = false;
window.addEventListener('keydown', handleKeyDown);
window.addEventListener('keyup', handleKeyUp);
return () => {
window.removeEventListener('keydown', handleKeyDown);
window.removeEventListener('keyup', handleKeyUp);
};
}, []);
// --- Helper Functions ---
const spawnBullet = () => {
bullets.current.push({
x: player.current.x,
y: player.current.y - 20,
width: 4,
height: 10,
color: '#FFFF00'
});
};
const spawnEnemy = () => {
const width = 30;
enemies.current.push({
x: width + Math.random() * (window.innerWidth - width * 2),
y: -30,
width: 30,
height: 30,
speed: 2 + Math.random() * 3,
color: '#FF3232'
});
};
const createExplosion = (x, y, color, count) => {
for(let i=0; i<count; i++) {
particles.current.push({
x: x,
y: y,
vx: (Math.random() - 0.5) * 6,
vy: (Math.random() - 0.5) * 6,
life: 20 + Math.random() * 20,
size: 2 + Math.random() * 3,
color: color
});
}
};
const checkRectCollision = (r1, r2) => {
return (
r1.x < r2.x + r2.width &&
r1.x + r1.width > r2.x &&
r1.y < r2.y + r2.height &&
r1.height + r1.y > r2.y
);
};
// --- Main Loop ---
const gameLoop = (time) => {
if (gameStateRef.current !== 'playing') return;
const ctx = canvasRef.current.getContext('2d');
const WIDTH = window.innerWidth;
const HEIGHT = window.innerHeight;
// 1. Update Logic
let moveX = 0;
if (keys.current['ArrowLeft']) moveX -= 1;
if (keys.current['ArrowRight']) moveX += 1;
if (Math.abs(mobileMoveX.current) > 0.05) {
moveX = mobileMoveX.current;
}
player.current.x += moveX * player.current.speed;
player.current.x = Math.max(player.current.width/2, Math.min(WIDTH - player.current.width/2, player.current.x));
// Shooting (Automatic)
if (time - player.current.lastShot > player.current.shootDelay) {
spawnBullet();
player.current.lastShot = time;
}
stars.current.forEach(star => {
star.y += star.speed;
if (star.y > HEIGHT) {
star.y = 0;
star.x = Math.random() * WIDTH;
}
});
for (let i = bullets.current.length - 1; i >= 0; i--) {
const b = bullets.current[i];
b.y -= 10;
if (b.y < -20) bullets.current.splice(i, 1);
}
spawnTimer.current++;
if (spawnTimer.current >= Math.floor(spawnRate.current)) {
spawnEnemy();
spawnTimer.current = 0;
if (spawnRate.current > 20) spawnRate.current -= 0.1;
}
const pRect = {
x: player.current.x - player.current.width/2 + 5,
y: player.current.y - player.current.height/2 + 5,
width: player.current.width - 10,
height: player.current.height - 10
};
for (let i = enemies.current.length - 1; i >= 0; i--) {
const e = enemies.current[i];
e.y += e.speed;
if (checkRectCollision(pRect, e)) {
createExplosion(player.current.x, player.current.y, '#00FFFF', 20);
createExplosion(e.x + 15, e.y + 15, '#FFA500', 20);
setGameState('gameover');
return;
}
const enemyHitbox = {
x: e.x - 5,
y: e.y - 5,
width: e.width + 10,
height: e.height + 10
};
let hit = false;
for (let j = bullets.current.length - 1; j >= 0; j--) {
const b = bullets.current[j];
if (checkRectCollision(b, enemyHitbox)) {
bullets.current.splice(j, 1);
createExplosion(e.x + 15, e.y + 15, '#FF3232', 10);
scoreRef.current += 10;
setScore(scoreRef.current);
enemies.current.splice(i, 1);
hit = true;
break;
}
}
if (!hit && e.y > HEIGHT + 50) {
enemies.current.splice(i, 1);
}
}
for (let i = particles.current.length - 1; i >= 0; i--) {
const p = particles.current[i];
p.x += p.vx;
p.y += p.vy;
p.life--;
p.size = Math.max(0, p.size - 0.1);
if (p.life <= 0) particles.current.splice(i, 1);
}
// 2. Draw
ctx.fillStyle = '#0a0a14';
ctx.fillRect(0, 0, WIDTH, HEIGHT);
stars.current.forEach(star => {
const c = Math.floor(star.brightness);
ctx.fillStyle = `rgb(${c},${c},${c})`;
ctx.fillRect(star.x, star.y, star.size, star.size);
});
ctx.fillStyle = '#FFFF00';
bullets.current.forEach(b => {
ctx.fillRect(b.x - b.width/2, b.y, b.width, b.height);
});
ctx.fillStyle = '#00FFFF';
ctx.beginPath();
ctx.moveTo(player.current.x, player.current.y - 20);
ctx.lineTo(player.current.x - 20, player.current.y + 15);
ctx.lineTo(player.current.x, player.current.y + 5);
ctx.lineTo(player.current.x + 20, player.current.y + 15);
ctx.closePath();
ctx.fill();
if (Math.random() > 0.5) {
ctx.fillStyle = '#FFA500';
ctx.beginPath();
ctx.moveTo(player.current.x - 5, player.current.y + 10);
ctx.lineTo(player.current.x, player.current.y + 25);
ctx.lineTo(player.current.x + 5, player.current.y + 10);
ctx.fill();
}
enemies.current.forEach(e => {
ctx.fillStyle = e.color;
ctx.fillRect(e.x, e.y, e.width, e.height);
ctx.fillStyle = '#0a0a14';
ctx.fillRect(e.x + 5, e.y + 5, 8, 8);
ctx.fillRect(e.x + 17, e.y + 5, 8, 8);
});
particles.current.forEach(p => {
ctx.fillStyle = p.color;
ctx.beginPath();
ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2);
ctx.fill();
});
frameId.current = requestAnimationFrame(gameLoop);
};
const handleRestart = () => {
initGame();
};
useEffect(() => {
const handleGlobalKey = (e) => {
if (gameState === 'gameover' && e.code === 'KeyR') {
handleRestart();
}
};
window.addEventListener('keydown', handleGlobalKey);
return () => window.removeEventListener('keydown', handleGlobalKey);
}, [gameState]);
const setKey = (key, val) => {
keys.current[key] = val;
};
const handleJoyStart = (clientX, clientY) => {
joyStart.current = { x: clientX, y: clientY };
};
const handleJoyMove = (clientX, clientY) => {
if (!joyStart.current) return;
const dx = clientX - joyStart.current.x;
const moveLimit = 40;
const clampedX = Math.max(-moveLimit, Math.min(moveLimit, dx));
if (joyStickRef.current) {
joyStickRef.current.style.transform = `translateX(${clampedX}px)`;
}
mobileMoveX.current = clampedX / moveLimit;
};
const handleJoyEnd = () => {
joyStart.current = null;
if (joyStickRef.current) {
joyStickRef.current.style.transform = `translateX(0px)`;
}
mobileMoveX.current = 0;
};
const [isMouseDragging, setIsMouseDragging] = useState(false);
useEffect(() => {
const onMouseMove = (e) => {
if (isMouseDragging) handleJoyMove(e.clientX, e.clientY);
};
const onMouseUp = () => {
if (isMouseDragging) {
setIsMouseDragging(false);
handleJoyEnd();
}
};
window.addEventListener('mousemove', onMouseMove);
window.addEventListener('mouseup', onMouseUp);
return () => {
window.removeEventListener('mousemove', onMouseMove);
window.removeEventListener('mouseup', onMouseUp);
};
}, [isMouseDragging]);
return (
<div style={{position: 'relative', width: '100%', height: '100%'}}>
<canvas
ref={canvasRef}
width={dimensions.width}
height={dimensions.height}
/>
<div className="score-board">Score: {score}</div>
{gameState === 'gameover' && (
<div className="ui-overlay">
<div className="game-over-box">
<h1>GAME OVER</h1>
<p>Final Score: {score}</p>
<button className="restart-btn" onClick={handleRestart}>Try Again (Press R)</button>
</div>
</div>
)}
<div id="mobile-controls">
<div className="joystick-zone"
onTouchStart={(e) => { e.preventDefault(); handleJoyStart(e.touches[0].clientX, e.touches[0].clientY); }}
onTouchMove={(e) => { e.preventDefault(); handleJoyMove(e.touches[0].clientX, e.touches[0].clientY); }}
onTouchEnd={handleJoyEnd}
onMouseDown={(e) => { setIsMouseDragging(true); handleJoyStart(e.clientX, e.clientY); }}
>
<div ref={joyStickRef} className="joystick-stick"></div>
</div>
</div>
</div>
);
}
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<Game />);
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment