2D平面射击游戏
by SparkShark50339 lines12.3 KB
<!DOCTYPE html>
<html lang="zh-HK">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>2D平面射击游戏</title>
<style>
*{margin:0;padding:0;box-sizing:border-box;font-family:system-ui, sans-serif}
html,body{width:100%;height:100%;overflow:hidden}
#gameCanvas{width:100vw;height:100vh;display:block;touch-action:none}
#ui{position:fixed;inset:0;color:#fff;font-size:20px}
/* 主菜单 */
.mainMenu{position:absolute;inset:0;background:rgba(20,20,20,0.5);display:flex;flex-direction:column;justify-content:flex-end;align-items:flex-end;padding:40px;gap:16px}
.menuBtn{width:180px;height:70px;font-size:22px;border:none;border-radius:10px;background:#348ede;color:white}
/* 模式选择面板 */
.modePanel{position:absolute;inset:0;background:rgba(0,0,0,0.85);display:none;flex-direction:row;gap:30px;align-items:center;justify-content:center}
.modeBox{width:220px;height:280px;background:#333;border-radius:12px;display:flex;align-items:center;justify-content:center;font-size:26px;color:#fff;border:2px #666 solid}
/* 关卡选择 */
.stagePanel{position:absolute;inset:0;background:rgba(0,0,0,0.85);display:none;flex-direction:row;gap:20px;align-items:center;justify-content:center}
.stageBtn{width:140px;height:200px;background:#224466;border-radius:10px;font-size:22px;color:white;border:none}
/* 皮肤面板 */
.skinPanel{position:absolute;inset:0;background:rgba(0,0,0,0.85);display:none;flex-direction:column;align-items:center;padding:20px}
.skinRow{display:flex;gap:12px;flex-wrap:wrap;justify-content:center;margin:15px 0}
.skinItem{width:90px;height:90px;border-radius:8px;border:2px solid #777}
.skinAdvBtn{padding:12px 24px;font-size:18px}
/* 摇杆 默认隐藏 */
.joyArea{position:absolute;bottom:20px;left:20px;width:160px;height:160px;background:rgba(255,255,255,0.15);border-radius:50%;display:none}
.joyStick{width:70px;height:70px;background:rgba(255,255,255,0.4);border-radius:50%;position:absolute;left:45px;top:45px}
.shootBtn{position:absolute;bottom:40px;right:30px;width:110px;height:110px;border-radius:50%;background:rgba(255,60,60,0.6);border:none;color:#fff;font-size:22px;display:none}
/* 主画面中间角色按钮 */
.mainCharPreview{
position:absolute;
left:50%;
top:50%;
transform:translate(-50%,-50%);
width:120px;
height:120px;
border-radius:12px;
border:2px solid #ffffff77;
}
</style>
</head>
<body>
<canvas id="gameCanvas"></canvas>
<div id="ui">
<!-- 主菜单 -->
<div class="mainMenu" id="mainMenu">
<button class="menuBtn" id="modeSelectBtn">选择模式</button>
<button class="menuBtn" id="startBtn">开始游戏</button>
</div>
<!-- 主画面中间角色,点击拣皮肤 -->
<div class="mainCharPreview" id="charPreview"></div>
<!-- 模式选择:关卡 / 练习 -->
<div class="modePanel" id="modePanel">
<div class="modeBox" data-mode="stage">关卡模式</div>
<div class="modeBox" data-mode="practice">练习模式</div>
</div>
<!-- 关卡选择 -->
<div class="stagePanel" id="stagePanel">
<button class="stageBtn" data-stage="1">第1关</button>
<button class="stageBtn" data-stage="2">第2关</button>
<button class="stageBtn" data-stage="3">第3关</button>
</div>
<!-- 皮肤选择介面 -->
<div class="skinPanel" id="skinPanel">
<h2>角色皮肤</h2>
<div class="skinRow">
<div class="skinItem" style="background:white" data-color="#ffffff"></div>
<div class="skinItem" style="background:black" data-color="#000000"></div>
<div class="skinItem" style="background:#348ede" data-color="#348ede"></div>
<div class="skinItem" style="background:#ff7700" data-color="#ff7700"></div>
</div>
<button class="skinAdvBtn">进阶皮肤 (50币)</button>
<button onclick="closeSkinPanel()">返回</button>
</div>
</div>
<script>
const canvas = document.getElementById("gameCanvas");
const ctx = canvas.getContext("2d");
const charPreview = document.getElementById("charPreview");
function resizeCanvas(){
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
}
resizeCanvas();
window.addEventListener("resize",resizeCanvas);
// ===== 游戏设定参数 =====
const PLAYER_MAX_HP = 100;
let player = {
x:120,y:300,w:44,h:44,hp:PLAYER_MAX_HP,color:"#ffffff",
bulletReserve:3,bulletReloadTimer:0,
weapon:"pistol",shootCooldown:0
};
const WEAPON_DATA = {
pistol:{dmg:5,delay:0.25,bulletCount:1},
shotgun:{dmg:2,delay:0.5,bulletCount:10},
sniper:{dmg:30,delay:1,bulletCount:1}
};
let gameState = "menu"; // menu / play
let gameMode = "practice";
let selectStage = 1;
let enemies = [];
let bullets = [];
let damageTexts = [];
let poisonCircle = {radius:800,shrinkSpeed:0.1};
// 摇杆DOM
const joyArea = document.createElement("div");
joyArea.className="joyArea";
const joyStick = document.createElement("div");
joyStick.className="joyStick";
joyArea.appendChild(joyStick);
document.getElementById("ui").appendChild(joyArea);
const shootBtn = document.querySelector(".shootBtn");
document.getElementById("ui").appendChild(shootBtn);
// 摇杆触摸
joyArea.addEventListener("touchstart",(e)=>{
joy.active=true;
const t = e.touches[0];
joy.originX = t.clientX;
joy.originY = t.clientY;
})
let joy = {active:false,originX:0,originY:0,dx:0,dy:0};
document.addEventListener("touchmove",(e)=>{
if(!joy.active) return;
const t = e.touches[0];
const dx = t.clientX - joy.originX;
const dy = t.clientY - joy.originY;
const dist = Math.sqrt(dx*dx+dy*dy);
const maxR = 80;
if(dist>maxR){
joy.dx = dx/dist*maxR;
joy.dy = dy/dist*maxR;
}else{
joy.dx = dx;joy.dy = dy;
}
joyStick.style.left = (45 + joy.dx)+"px";
joyStick.style.top = (45 + joy.dy)+"px";
})
document.addEventListener("touchend",()=>{
joy.active=false;joy.dx=0;joy.dy=0;
joyStick.style.left="45px";joyStick.style.top="45px";
})
shootBtn.addEventListener("touchstart",shoot);
// 点击中间角色打开皮肤面板
charPreview.onclick = ()=>{
document.getElementById("skinPanel").style.display="flex";
}
function closeSkinPanel(){
document.getElementById("skinPanel").style.display="none";
}
// 拣基础皮肤颜色
document.querySelectorAll(".skinItem").forEach(item=>{
item.onclick = ()=>{
player.color = item.dataset.color;
}
})
// 菜单按钮
document.getElementById("modeSelectBtn").onclick = ()=>{
document.getElementById("modePanel").style.display="flex";
}
document.querySelectorAll(".modeBox").forEach(box=>{
box.onclick = ()=>{
gameMode = box.dataset.mode;
document.getElementById("modePanel").style.display="none";
if(gameMode==="stage"){
document.getElementById("stagePanel").style.display="flex";
}else{
document.getElementById("modeSelectBtn").innerText="练习模式";
}
}
})
document.querySelectorAll(".stageBtn").forEach(btn=>{
btn.onclick=()=>{
selectStage = btn.dataset.stage;
document.getElementById("stagePanel").style.display="none";
document.getElementById("modeSelectBtn").innerText=`关卡模式 - 第${selectStage}关`;
}
})
document.getElementById("startBtn").onclick = startGame;
function startGame(){
gameState="play";
player.hp = PLAYER_MAX_HP;
// 玩家出生喺画面中间
player.x = canvas.width/2;
player.y = canvas.height/2;
player.bulletReserve=3;player.bulletReloadTimer=0;
player.shootCooldown=0;
enemies=[];bullets=[];damageTexts=[];
poisonCircle.radius = 800;
// 游戏入面先显示摇杆同射击掣
joyArea.style.display="block";
shootBtn.style.display="block";
if(gameMode==="practice"){
enemies = [
{x:canvas.width/2+150,y:canvas.height/2,w:44,h:44,hp:80,movable:false,color:"#f44"}
]
}else{
enemies = [
{x:canvas.width/2+150,y:canvas.height/2,w:44,h:44,hp:80,movable:true,color:"#f44"}
]
}
}
function shoot(){
if(gameState!="play") return;
if(player.shootCooldown>0) return;
const wp = WEAPON_DATA[player.weapon];
player.shootCooldown = wp.delay;
if(player.bulletReserve<=0) return;
player.bulletReserve--;
const count = wp.bulletCount;
for(let i=0;i<count;i++){
let spread = (Math.random()-0.5)*0.25;
bullets.push({
x:player.x+player.w/2,
y:player.y+player.h/2,
vx:Math.cos(spread)*10,
vy:Math.sin(spread)*10,
dmg:wp.dmg,isPlayer:true
})
}
}
function loop(timestamp){
//草地背景
ctx.fillStyle="#4a7c59";
ctx.fillRect(0,0,canvas.width,canvas.height);
if(gameState==="play"){
//玩家移动
const spd = 4;
player.x += joy.dx*0.06*spd;
player.y += joy.dy*0.06*spd;
//子弹冷却计时
if(player.shootCooldown>0) player.shootCooldown -= 1/60;
//子弹补给
player.bulletReloadTimer +=1/60;
if(player.bulletReloadTimer>1.2 && player.bulletReserve<3){
player.bulletReserve++;
player.bulletReloadTimer=0;
}
//毒圈收缩
poisonCircle.radius -= poisonCircle.shrinkSpeed;
const centerX = canvas.width/2;
const centerY = canvas.height/2;
const distToCenter = Math.sqrt((player.x-centerX)**2 + (player.y-centerY)**2);
if(distToCenter>poisonCircle.radius){
player.hp -= 10/60;
}
//更新子弹
for(let i=bullets.length-1;i>=0;i--){
const b = bullets[i];
b.x += b.vx; b.y += b.vy;
if(b.isPlayer){
for(let ei=enemies.length-1;ei>=0;ei--){
const e = enemies[ei];
if(checkHit(b.x,b.y,8,8,e.x,e.y,e.w,e.h)){
e.hp -= b.dmg;
damageTexts.push({x:e.x,y:e.y,val:b.dmg,life:1.2});
bullets.splice(i,1);
if(e.hp<=0) enemies.splice(ei,1);
break;
}
}
}
}
//画敌人
enemies.forEach(e=>{
ctx.fillStyle = e.color;
ctx.fillRect(e.x,e.y,e.w,e.h);
//敌人双眼
ctx.fillStyle="#000";
ctx.beginPath();
ctx.arc(e.x+12,e.y+14,5,0,Math.PI*2);
ctx.arc(e.x+32,e.y+14,5,0,Math.PI*2);
ctx.fill();
})
//画玩家
ctx.fillStyle=player.color;
ctx.fillRect(player.x,player.y,player.w,player.h);
//玩家双眼
ctx.fillStyle="#000";
ctx.beginPath();
ctx.arc(player.x+12,player.y+14,5,0,Math.PI*2);
ctx.arc(player.x+32,player.y+14,5,0,Math.PI*2);
ctx.fill();
//玩家绿色血条
const barW = player.w + 6;
const barH = 6;
ctx.fillStyle="#38c948";
ctx.fillRect(player.x-3,player.y-12,barW*(player.hp/PLAYER_MAX_HP),barH);
ctx.strokeStyle="#fff";ctx.strokeRect(player.x-3,player.y-12,barW,barH);
//子弹3格UI(橙色/黑色)
for(let bi=0;bi<3;bi++){
ctx.fillStyle = bi < player.bulletReserve ? "#ff9922":"#222";
ctx.fillRect(player.x+bi*14,player.y-22,12,6);
}
//飘伤害数字
for(let di=damageTexts.length-1;di>=0;di--){
const d = damageTexts[di];
d.y -= 1.2; d.life -= 1/60;
ctx.fillStyle="#fff";
ctx.font="20px sans-serif";
ctx.fillText(d.val, d.x,d.y);
if(d.life <=0) damageTexts.splice(di,1);
}
//画毒圈
ctx.strokeStyle="#992222";
ctx.lineWidth=3;
ctx.beginPath();
ctx.arc(centerX,centerY,poisonCircle.radius,0,Math.PI*2);
ctx.stroke();
}else{
//主菜单:喺画面中间画预览角色
ctx.fillStyle=player.color;
ctx.fillRect(canvas.width/2 - 22, canvas.height/2 -22,44,44);
ctx.fillStyle="#000";
ctx.beginPath();
ctx.arc(canvas.width/2-10,canvas.height/2-8,5,0,Math.PI*2);
ctx.arc(canvas.width/2+10,canvas.height/2-8,5,0,Math.PI*2);
ctx.fill();
}
requestAnimationFrame(loop);
}
function checkHit(x1,y1,w1,h1,x2,y2,w2,h2){
return x1<x2+w2 && x1+w1>x2 && y1<y2+h2 && y1+h1>y2;
}
requestAnimationFrame(loop);
</script>
</body>
</html>Game Source: 2D平面射击游戏
Creator: SparkShark50
Libraries: none
Complexity: complex (339 lines, 12.3 KB)
The full source code is displayed above on this page.
Remix Instructions
To remix this game, copy the source code above and modify it. Add a ARCADELAB header at the top with "remix_of: 2d-sparkshark50-mvxlfmnt" to link back to the original. Then publish at arcadelab.ai/publish.