미사일 피하기 게임 – 머리식히기 웹용 게임


제미나이를 이용해서 하나 만들어 달라고 했습니다.

처음에는 뚝딱 만들어주니까 놀랬는데 하다보니 멈추더군요…(그럼 그렇지…)

그래서 멈춘다고 수정을 해달라고 했더니 다시 뚝딱 만들어주는데…

그 이후로는 문제가 없었습니다.

그때 알려준 소스코드를 올려드립니다.

HTML에서 그대로 복사해서 쓰시면 될거 같네요~

<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<title>미사일 피하기</title>
<style>
*{box-sizing:border-box}
html,body{margin:0!important;padding:0!important;width:100%!important;height:100%!important;background:#070b16!important;color:#fff!important;font-family:Arial,"Noto Sans KR",sans-serif!important;overflow:hidden!important;touch-action:none!important}
body{display:flex!important;align-items:center!important;justify-content:center!important;min-height:100%!important}
#wrap{position:relative;width:100%;height:100%;max-width:900px;max-height:700px;min-height:520px;background:#08101e;overflow:hidden;border-radius:16px;box-shadow:0 20px 70px rgba(0,0,0,.45)}
canvas{display:block;width:100%;height:100%;background:radial-gradient(circle at 50% 40%,#102443 0%,#07111f 55%,#030710 100%)}
#hud{position:absolute;top:14px;left:16px;right:16px;display:flex;justify-content:space-between;gap:10px;pointer-events:none;font-weight:700;text-shadow:0 2px 6px #000}
.pill{padding:9px 13px;border-radius:999px;background:rgba(0,0,0,.35);backdrop-filter:blur(5px);font-size:14px}
#overlay{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;background:rgba(0,0,0,.35);padding:20px}
.panel{width:min(92%,430px);text-align:center;padding:28px 22px;border-radius:22px;background:rgba(7,13,25,.9);border:1px solid rgba(255,255,255,.12);box-shadow:0 20px 60px rgba(0,0,0,.45)}
.panel h1{font-size:34px;margin:0 0 8px}
.panel p{line-height:1.6;opacity:.88;margin:8px 0}
.btn{margin-top:16px;border:0;border-radius:14px;padding:14px 24px;font-size:18px;font-weight:800;cursor:pointer;background:#fff;color:#0b1220}
.small{font-size:13px;opacity:.65}
#mobileHint{position:absolute;left:50%;bottom:18px;transform:translateX(-50%);font-size:12px;opacity:.65;pointer-events:none;white-space:nowrap}
@media (max-width:700px){
body{background:#000!important}
#wrap{width:100%;height:100%;min-height:100%;border-radius:0}
.panel h1{font-size:29px}
#hud{top:10px;left:10px;right:10px}
.pill{font-size:12px;padding:8px 10px}
}
</style>
</head>
<body>
<div id="wrap">
<canvas id="game"></canvas>

<div id="hud">
<div class="pill">생존 <span id="score">0.0</span>초</div>
<div class="pill">최고 <span id="best">0.0</span>초</div>
<div class="pill">Lv.<span id="level">1</span></div>
</div>

<div id="mobileHint">PC: 방향키 / WASD · 모바일: 화면을 드래그</div>

<div id="overlay">
<div class="panel">
<h1>🚀 미사일 피하기</h1>
<p>추적 미사일을 피해 최대한 오래 살아남으세요.</p>
<p><b>PC</b> 방향키 또는 WASD<br><b>모바일</b> 화면을 손가락으로 드래그</p>
<button id="startBtn" class="btn">게임 시작</button>
<p class="small">시간이 지날수록 미사일 수와 속도가 증가합니다.</p>
</div>
</div>
</div>

<script>
(() => {
const canvas = document.getElementById('game');
const ctx = canvas.getContext('2d');
const wrap = document.getElementById('wrap');
const overlay = document.getElementById('overlay');
const startBtn = document.getElementById('startBtn');
const scoreEl = document.getElementById('score');
const bestEl = document.getElementById('best');
const levelEl = document.getElementById('level');

let W=0,H=0,dpr=1;
let running=false, last=0, elapsed=0, spawnTimer=0;
let keys={};
let missiles=[], particles=[], stars=[];
let dragging=false, targetX=0, targetY=0;
let best = Number(localStorage.getItem('missileDodgeBest') || 0);
bestEl.textContent = best.toFixed(1);

const player = {x:0,y:0,r:14,speed:330,angle:-Math.PI/2};

function resize(){
const r = wrap.getBoundingClientRect();
dpr = Math.min(window.devicePixelRatio || 1, 2);
canvas.width = Math.floor(r.width*dpr);
canvas.height = Math.floor(r.height*dpr);
canvas.style.width = r.width+'px';
canvas.style.height = r.height+'px';
ctx.setTransform(dpr,0,0,dpr,0,0);
W=r.width; H=r.height;
if(!running){ player.x=W/2; player.y=H*0.7; }
if(stars.length===0) makeStars();
}
window.addEventListener('resize', resize);

function makeStars(){
stars = Array.from({length:90},()=>({
x:Math.random()*W,y:Math.random()*H,
s:Math.random()*1.7+.4,a:Math.random()*.65+.2
}));
}

function reset(){
missiles=[]; particles=[];
elapsed=0; spawnTimer=0;
player.x=W/2; player.y=H*0.72; player.angle=-Math.PI/2;
targetX=player.x; targetY=player.y;
scoreEl.textContent='0.0';
levelEl.textContent='1';
}

function start(){
reset();
overlay.style.display='none';
running=true;
last=performance.now();
requestAnimationFrame(loop);
}

function end(){
running=false;
if(elapsed>best){
best=elapsed;
localStorage.setItem('missileDodgeBest',best.toFixed(2));
bestEl.textContent=best.toFixed(1);
}
overlay.style.display='flex';
overlay.querySelector('.panel').innerHTML = `
<h1>💥 게임 오버</h1>
<p>생존 시간 <b>${elapsed.toFixed(1)}초</b></p>
<p>최고 기록 <b>${best.toFixed(1)}초</b></p>
<button id="restartBtn" class="btn">다시 도전</button>
<p class="small">미사일끼리 충돌시키는 것도 좋은 전략입니다.</p>
`;
document.getElementById('restartBtn').onclick=start;
}

function spawnMissile(){
const side=Math.floor(Math.random()*4);
let x,y;
if(side===0){x=Math.random()*W;y=-25}
if(side===1){x=W+25;y=Math.random()*H}
if(side===2){x=Math.random()*W;y=H+25}
if(side===3){x=-25;y=Math.random()*H}

const level=1+Math.floor(elapsed/12);
missiles.push({
x,y,vx:0,vy:0,r:8,
speed:125 + level*10 + Math.random()*28,
turn:1.7 + Math.min(level*.08,1.5),
life:0
});
}

function burst(x,y,count=18){
for(let i=0;i<count;i++){
const a=Math.random()*Math.PI*2;
const s=50+Math.random()*150;
particles.push({x,y,vx:Math.cos(a)*s,vy:Math.sin(a)*s,life:.5+Math.random()*.5,max:1});
}
}

function update(dt){
elapsed += dt;
const level=1+Math.floor(elapsed/12);
levelEl.textContent=level;
scoreEl.textContent=elapsed.toFixed(1);

let dx=0,dy=0;
if(keys['ArrowLeft']||keys['a']||keys['A']) dx--;
if(keys['ArrowRight']||keys['d']||keys['D']) dx++;
if(keys['ArrowUp']||keys['w']||keys['W']) dy--;
if(keys['ArrowDown']||keys['s']||keys['S']) dy++;

if(dx||dy){
const m=Math.hypot(dx,dy)||1;
player.x += dx/m*player.speed*dt;
player.y += dy/m*player.speed*dt;
player.angle=Math.atan2(dy,dx)+Math.PI/2;
} else if(dragging){
const vx=targetX-player.x, vy=targetY-player.y;
const dist=Math.hypot(vx,vy);
if(dist>3){
const step=Math.min(dist,player.speed*1.35*dt);
player.x += vx/dist*step;
player.y += vy/dist*step;
player.angle=Math.atan2(vy,vx)+Math.PI/2;
}
}

player.x=Math.max(18,Math.min(W-18,player.x));
player.y=Math.max(18,Math.min(H-18,player.y));

spawnTimer -= dt;
const interval=Math.max(.32,1.15-level*.055);
if(spawnTimer<=0){
spawnMissile();
if(level>=5 && Math.random()<.28) spawnMissile();
spawnTimer=interval;
}

for(const m of missiles){
m.life+=dt;
const desired=Math.atan2(player.y-m.y,player.x-m.x);
const current=Math.atan2(m.vy,m.vx);
let diff=((desired-current+Math.PI*3)%(Math.PI*2))-Math.PI;
const newAngle=current+Math.max(-m.turn*dt,Math.min(m.turn*dt,diff));
m.vx=Math.cos(newAngle)*m.speed;
m.vy=Math.sin(newAngle)*m.speed;
m.x+=m.vx*dt;
m.y+=m.vy*dt;

if(Math.hypot(m.x-player.x,m.y-player.y)<m.r+player.r-3){
burst(player.x,player.y,34);
end();
return;
}
}

// Missile-vs-missile collision.
// Mark collided missiles first, then remove them after iteration.
const removeMissiles = new Set();
for(let i=missiles.length-1;i>=0;i--){
if(removeMissiles.has(i)) continue;
const a=missiles[i];
if(!a) continue;

for(let j=i-1;j>=0;j--){
if(removeMissiles.has(j)) continue;
const b=missiles[j];
if(!b) continue;

if(Math.hypot(a.x-b.x,a.y-b.y)<a.r+b.r){
burst((a.x+b.x)/2,(a.y+b.y)/2,12);
removeMissiles.add(i);
removeMissiles.add(j);
break;
}
}
}

if(removeMissiles.size){
missiles = missiles.filter((m, idx) => !removeMissiles.has(idx));
}

missiles=missiles.filter(m=>m.life<20 && m.x>-120 && m.x<W+120 && m.y>-120 && m.y<H+120);

particles.forEach(p=>{
p.x+=p.vx*dt; p.y+=p.vy*dt;
p.vx*=.985; p.vy*=.985; p.life-=dt;
});
particles=particles.filter(p=>p.life>0);
}

function drawShip(){
ctx.save();
ctx.translate(player.x,player.y);
ctx.rotate(player.angle);

// flame
ctx.beginPath();
ctx.moveTo(-6,13); ctx.lineTo(0,25+Math.random()*7); ctx.lineTo(6,13);
ctx.fillStyle='rgba(255,160,50,.85)'; ctx.fill();

// ship
ctx.beginPath();
ctx.moveTo(0,-18); ctx.lineTo(13,14); ctx.lineTo(0,9); ctx.lineTo(-13,14); ctx.closePath();
ctx.fillStyle='#8dd8ff'; ctx.fill();

ctx.beginPath();
ctx.arc(0,-4,4.5,0,Math.PI*2);
ctx.fillStyle='#0c4268'; ctx.fill();
ctx.restore();
}

function drawMissile(m){
const a=Math.atan2(m.vy,m.vx);
ctx.save();
ctx.translate(m.x,m.y);
ctx.rotate(a);
ctx.beginPath();
ctx.moveTo(13,0);ctx.lineTo(-8,-6);ctx.lineTo(-13,0);ctx.lineTo(-8,6);ctx.closePath();
ctx.fillStyle='#ff5e62';ctx.fill();
ctx.beginPath();
ctx.moveTo(-12,-3);ctx.lineTo(-22,0);ctx.lineTo(-12,3);ctx.closePath();
ctx.fillStyle='#ffb347';ctx.fill();
ctx.restore();
}

function draw(){
ctx.clearRect(0,0,W,H);

for(const s of stars){
ctx.globalAlpha=s.a;
ctx.fillStyle='#fff';
ctx.fillRect(s.x,s.y,s.s,s.s);
s.y+=.08;
if(s.y>H) s.y=0;
}
ctx.globalAlpha=1;

// faint grid
ctx.strokeStyle='rgba(100,170,255,.05)';
ctx.lineWidth=1;
for(let x=0;x<W;x+=60){ctx.beginPath();ctx.moveTo(x,0);ctx.lineTo(x,H);ctx.stroke()}
for(let y=0;y<H;y+=60){ctx.beginPath();ctx.moveTo(0,y);ctx.lineTo(W,y);ctx.stroke()}

missiles.forEach(drawMissile);
drawShip();

particles.forEach(p=>{
ctx.globalAlpha=Math.max(0,p.life);
ctx.fillStyle='#ffd166';
ctx.fillRect(p.x,p.y,3,3);
});
ctx.globalAlpha=1;
}

function loop(t){
if(!running) return;
const dt=Math.min((t-last)/1000,.033);
last=t;
update(dt);
draw();
if(running) requestAnimationFrame(loop);
}

window.addEventListener('keydown',e=>{
keys[e.key]=true;
if(['ArrowUp','ArrowDown','ArrowLeft','ArrowRight',' '].includes(e.key)) e.preventDefault();
},{passive:false});
window.addEventListener('keyup',e=>keys[e.key]=false);

function pointerPos(e){
const r=canvas.getBoundingClientRect();
return {x:e.clientX-r.left,y:e.clientY-r.top};
}
canvas.addEventListener('pointerdown',e=>{
dragging=true;
canvas.setPointerCapture?.(e.pointerId);
const p=pointerPos(e);targetX=p.x;targetY=p.y;
});
canvas.addEventListener('pointermove',e=>{
if(!dragging) return;
const p=pointerPos(e);targetX=p.x;targetY=p.y;
});
canvas.addEventListener('pointerup',()=>dragging=false);
canvas.addEventListener('pointercancel',()=>dragging=false);

startBtn.onclick=start;
resize();
draw();
})();
</script>
</body>
</html>


김프로의 국장 미국장에서 더 알아보기

구독을 신청하면 최신 게시물을 이메일로 받아볼 수 있습니다.

댓글 남기기