「码动四季·开源同行:玩转 AtomCode」完全零基础也能搞开发——从零开发出高颜值坦克大战网页游戏
网址:AtomGit - 全球开发者的开源社区,开源代码托管平台
一、前言:AI赋能开源开发,新手也能玩转实战项目
在AI赋能开发的当下,开源免费的AI编码工具正在彻底降低编程实战的门槛。AtomCode 作为一款开源、多模型适配、免费Token的全自动AI编码助手,无需复杂配置、无需高额付费,兼顾代码生成、问题调试、功能优化、样式美化等全流程开发能力,是开发者入门实战、落地小型项目、打磨编码能力的优质工具。
本次参与AtomGit「码动四季·开源同行」夏季征稿活动,我将完整分享借助AtomCode从零开发网页版坦克大战游戏的全流程实战经验。从项目需求梳理、核心逻辑搭建、代码编写调试,到UI视觉美化、交互优化、bug修复,全程依托AtomCode辅助开发,完整记录开源AI工具在前端实战项目中的落地价值,同时拆解新手如何高效借助AtomCode快速落地完整可运行项目。

本次开发的坦克大战基于原生 HTML+CSS+JavaScript 实现,无需依赖任何框架,轻量化、可直接运行、功能完整,包含坦克移动射击、敌人AI刷新、墙体碰撞、道具拾取、粒子特效、计分系统、暂停重启、游戏胜负判定等全套核心功能,同时搭配赛博风动态UI,视觉效果远超基础原生小游戏。
二、项目开发前置:为什么选择AtomCode赋能本次开发?
在传统原生JS小游戏开发中,新手往往会面临诸多痛点:碰撞逻辑繁琐、动画特效编写复杂、边界判定容易出错、UI样式单调、bug排查耗时、功能迭代繁琐。而AtomCode完美解决了这些开发难题,相比传统手动编码、普通AI编码工具,具备多重核心优势,也是我本次选择它全程辅助开发的核心原因:
1. 开源免费无门槛:依托AtomGit开源生态,提供免费Token额度,无需付费、无需复杂部署,个人开发者可直接使用,适配学生、新手开发者实战练手需求。
2. 全场景编码适配:支持原生前端、后端、小游戏、工具类代码生成,针对本次原生JS坦克大战,能够精准生成规范、可运行、高兼容性的代码,无冗余垃圾代码。
3. 调试优化一体化:不仅能生成基础代码,还能针对性优化代码逻辑、修复运行bug、优化性能、美化UI样式,实现「代码生成-问题修复-功能迭代-视觉升级」全流程赋能。
4. 贴合实战场景:区别于通用AI的模板化代码,AtomCode生成的代码贴合真实开发规范,注释清晰、结构分层合理,便于二次修改和功能拓展,非常适合新手学习落地实战项目。
本次项目我全程采用「需求拆解+AI辅助生成+人工微调优化」的开发模式,大幅缩短开发周期,原本需要3-4天的完整项目开发,依托AtomCode仅用半天即可完成,同时保证代码质量和项目完整性。
三、项目整体需求与功能架构拆解
在正式开发前,我通过AtomCode辅助完成项目需求拆解和架构设计,明确本次坦克大战的核心功能模块,让开发流程条理清晰、避免逻辑混乱。
核心功能需求
1. 基础操作:方向键控制坦克上下左右移动、空格键开火、P键暂停/继续游戏;
2. 游戏场景:内置砖墙、钢铁墙双重障碍物,砖墙可被炮弹摧毁,钢铁墙永久不可破坏;
3. 敌人AI:自动刷新敌方坦克,敌方自主移动、自动瞄准玩家射击,智能规避墙体障碍;
4. 道具系统:随机刷新生命血包、加速道具,拾取后可恢复血量、临时提升移动速度;
5. 数据统计:实时展示关卡、得分、击杀数、剩余生命值,数据实时更新;
6. 特效交互:子弹光影、爆炸粒子特效、无敌闪烁、加速状态提示、动态背景动画;
7. 胜负机制:玩家血量耗尽游戏失败,累计击杀达标即可胜利,支持一键重启游戏;
8. 容错机制:完善的异常捕获、DOM校验、边界判定,避免游戏闪退、卡死、报错。
项目技术架构
项目采用分层开发思路,通过AtomCode辅助拆分四大核心模块,结构清晰易维护:
1. UI视觉层:HTML搭建页面结构,CSS实现动态渐变背景、霓虹边框、发光文字、悬浮动画;
2. 场景渲染层:Canvas画布实现游戏主体渲染,绘制坦克、墙体、子弹、道具、粒子特效;
3. 逻辑交互层:JS实现键盘监听、移动碰撞、射击逻辑、敌人AI、道具刷新、数据统计;
4. 状态管理层:统一管控游戏暂停、结束、胜利、无敌、加速等全局状态。
网页链接:https://ai.gitcode.com/ascend-model-ecosystem#mofix-agent

需要登录AtomCode

登录昇腾终端

昇腾显卡加持

登录使用

或者直接在web使用

四、AtomCode实战开发全流程:从0到1落地项目
本次开发全程依托AtomCode完成核心代码生成、逻辑优化、bug修复、样式升级,我将拆解关键开发步骤,分享具体的AI使用技巧和实操心得。
4.1 初始化项目结构,生成基础模板代码
新手开发小游戏最容易出现的问题是项目结构混乱、代码堆砌、无规范。我通过向AtomCode输入精准需求:「帮我生成一个原生HTML+CSS+JS坦克大战基础模板,包含Canvas画布、自适应布局、基础页面结构、全局样式重置、容错处理,代码结构分层清晰」。

终端更加详细:

AtomCode快速生成了标准化的项目基础框架,包含完整的HTML结构、全局样式重置、动画预设、DOM获取容错函数,从源头保证代码规范。同时自动封装了safeGetElement、safeSetText等工具函数,避免DOM获取失败导致的页面报错,大幅提升项目稳定性。
4.2 核心游戏逻辑开发,AI高效攻克难点
坦克大战的核心难点集中在碰撞检测、敌人AI逻辑、子弹轨迹判定、实体移动限制,这也是原生JS开发最繁琐、最容易出错的部分。传统手动编写需要反复调试坐标、修正碰撞误差,耗时费力。
我借助AtomCode分模块生成核心逻辑代码:
1. 碰撞检测逻辑:指令需求「编写精准的矩形碰撞检测函数,支持坦克与墙体、坦克与敌人、子弹与障碍物、子弹与坦克的碰撞判定,区分可摧毁砖墙和不可摧毁钢铁墙」。AtomCode生成的
rectCollide函数判定精准,完美解决实体穿透、误碰撞问题。2. 玩家操控逻辑:实现方向键八向移动、空格开火、开火冷却、无敌保护机制,同时优化移动逻辑,支持单轴移动判定,避免坦克卡墙卡死。
3. 敌人AI智能逻辑:通过AI生成随机移动机制、玩家瞄准逻辑、自动射击冷却,敌方坦克可自主规避障碍、动态调整移动方向,同时根据距离判定是否瞄准玩家射击,AI行为更加智能真实。
4. 实体刷新机制:自动限制最大敌人数、定时刷新敌方坦克、随机刷新道具,保证游戏难度均衡,提升游玩体验。
4.3 视觉特效全面升级,告别简陋原生样式
基础原生小游戏普遍存在UI单调、无动画、无特效的问题,我通过AtomCode针对性优化视觉效果,打造赛博朋克动态游戏界面:
1. 动态渐变背景:实现四色渐变无限滚动动画,搭配悬浮粒子效果,页面层次感拉满;
2. 霓虹发光特效:标题流光、画布脉冲边框、数值发光高亮,适配游戏科技风格;
3. 粒子爆炸特效:坦克被击毁、子弹撞墙、道具拾取时触发动态粒子扩散效果;
4. 状态可视化:无敌闪烁、加速状态提示、游戏暂停遮罩、结束弹窗动画,交互反馈清晰直观;
5. 数据面板美化:将分数、击杀数、生命值、关卡数据模块化展示,配色区分状态,视觉整洁高级。
整个美化过程无需手动查阅CSS文档,通过AtomCode「需求描述+迭代优化」即可快速实现,大幅降低前端样式开发成本。

4.4 bug调试与性能优化,AI精准排错
项目开发中我遇到了多个典型问题:坦克卡墙卡死、子弹穿透障碍物、敌人无限刷新、道具不消失、游戏结束不弹窗等。以往需要逐行排查代码,耗时很久,而AtomCode的调试能力极大提升了效率。
我直接粘贴报错代码和问题现象,AtomCode快速定位bug根源并给出修复方案:
1. 修复坦克双向碰撞冲突,区分玩家与敌人碰撞逻辑,避免实体重叠卡死;
2. 优化子弹生命周期管理,过滤失效子弹,避免内存占用过高;
3. 增加道具过期销毁机制,防止场景道具堆积;
4. 完善游戏状态锁,暂停、结束状态下禁止多余操作;
5. 全局增加异常捕获,避免单个模块报错导致整个游戏崩溃。





经过AtomCode的迭代优化,项目实现了零报错、零卡死、运行流畅稳定,完全达到可上线体验的标准。
五、AtomCode使用核心心得与技巧总结
本次完整开发坦克大战项目,让我深度体验到AtomCode作为开源AI编码助手的核心价值,相比传统开发和其他AI工具,优势十分突出,同时也总结出新手高效使用的技巧:
5.1 核心使用优势
1. 开源免费,轻量化无负担:依托AtomGit开源生态,免费Token足够日常开发、项目实战、代码学习,无需付费订阅,对学生和入门开发者极度友好。
2. 代码实用性极强:生成的代码无套路、无冗余,可直接运行,逻辑严谨、注释规范,完全贴合真实开发标准,不是“玩具代码”。
3. 全流程赋能开发:从项目架构、代码生成、功能迭代、UI美化、bug调试到性能优化,一站式解决开发全流程问题,无需切换多个工具。
4. 适配新手成长:不仅能快速落地项目,还能通过生成的规范代码,学习分层开发、容错处理、逻辑封装的编程思维,实现“做项目+学技术”双向提升。
5.2 新手高效使用技巧
1. 需求精准拆分:不要一次性输入复杂需求,将项目拆分为结构、逻辑、样式、特效、调试等小模块,逐一生成,代码更精准、BUG更少;
2. 先复用再优化:先用AtomCode生成基础可运行代码,再通过AI迭代优化细节、美化样式、拓展功能,大幅提升开发效率;
3. 借助AI学逻辑:对于碰撞检测、AI逻辑等难点代码,可让AtomCode逐行讲解原理,在实战中吃透原生JS核心知识点;
4. 善用调试能力:遇到运行报错直接粘贴问题,无需自行盲目排查,AI可快速定位并修复底层逻辑问题。
六、开源视角:AtomCode带给开发者的价值思考
本次借助AtomCode完成实战项目,让我对开源AI工具有了全新的认知。在开源生态快速发展的今天,编程开发早已不是“纯手动敲代码”的时代,AtomCode以开源、免费、专业、轻量化的定位,打破了传统AI编码工具付费贵、代码质量差、适配性弱的痛点。
对于入门开发者而言,AtomCode降低了实战门槛,让每个人都能低成本落地完整项目,积累开源作品、夯实编程基础;对于进阶开发者而言,可借助AtomCode快速实现功能原型、优化代码性能、提升开发效率,专注于核心逻辑创新。
同时,AtomCode扎根AtomGit开源社区,依托社区生态持续迭代优化,汇聚海量开发者实战经验,形成了「工具赋能开发+社区共建成长」的良性开源生态,这也是本次积极参与「码动四季·开源同行」征稿活动的核心原因——希望通过真实实战案例,让更多开发者了解并使用这款优质开源AI编码工具。

七、项目最终效果与源码说明
本次开发的坦克大战项目已实现完整可运行、功能全覆盖、视觉高颜值的效果,支持PC端浏览器直接打开游玩,操作流畅、特效炫酷、难度均衡,兼具趣味性与技术性。
项目源码全程由AtomCode辅助生成、人工优化微调,代码结构清晰、注释完整、可二次拓展,新手可直接下载运行学习,也可基于此迭代双人模式、关卡升级、更多道具皮肤等拓展功能。
八、总结
本次基于AtomCode开发网页坦克大战的实战经历,让我真切感受到开源AI工具的强大赋能能力。从零基础搭建项目架构、编写核心游戏逻辑,到美化UI特效、调试修复bug、优化项目性能,AtomCode全程高效助力,极大缩短了开发周期,同时保证了项目质量与代码规范性。
「码动四季·开源同行」活动让更多开发者有机会接触、体验、深耕AtomCode开源生态。对于想要快速落地实战项目、提升编程能力、积累开源作品的开发者来说,AtomCode无疑是性价比最高、最适配新手的免费开源AI编码助手。未来我也会继续依托AtomCode,深耕前端实战、开源项目开发,持续输出更多优质实战内容,与开源社区共同成长。


坦克大战完整代码如下:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>坦克大战 - Tank Battle</title>
<style>
/* ========== 全局重置与主题 ========== */
* { margin: 0; padding: 0; box-sizing: border-box; }
@keyframes bgShift {
0% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
100% { background-position: 0% 50%; }
}
@keyframes titleGlow {
0%, 100% { text-shadow: 0 0 10px #4fc3f7, 0 0 20px #4fc3f7, 0 0 40px #0288d1; }
50% { text-shadow: 0 0 15px #81c784, 0 0 30px #81c784, 0 0 60px #388e3c; }
}
@keyframes borderPulse {
0%, 100% { border-color: #4fc3f7; box-shadow: 0 0 12px rgba(79,195,247,0.3); }
50% { border-color: #81c784; box-shadow: 0 0 20px rgba(129,199,132,0.5); }
}
@keyframes fadeInUp {
from { opacity: 0; transform: translateY(30px) scale(0.9); }
to { opacity: 1; transform: translateY(0) scale(1); }
}
@keyframes overlayIn {
0% { opacity: 0; backdrop-filter: blur(0px); }
100% { opacity: 1; backdrop-filter: blur(4px); }
}
@keyframes floatDot {
0%, 100% { transform: translateY(0); opacity: 0.3; }
50% { transform: translateY(-20px); opacity: 0.7; }
}
@keyframes neonPulse {
0%, 100% { box-shadow: 0 0 5px #4fc3f7, 0 0 10px #4fc3f7; }
50% { box-shadow: 0 0 15px #81c784, 0 0 30px #81c784; }
}
body {
background: linear-gradient(-45deg, #0a0a1a, #1a1a3e, #0d2137, #16213e);
background-size: 400% 400%;
animation: bgShift 12s ease infinite;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
font-family: 'Microsoft YaHei', 'Segoe UI', sans-serif;
overflow: hidden;
}
/* 浮动背景粒子 */
body::before, body::after {
content: '';
position: fixed;
width: 300px; height: 300px;
border-radius: 50%;
background: radial-gradient(circle, rgba(79,195,247,0.06), transparent 70%);
pointer-events: none;
z-index: 0;
}
body::before { top: -80px; left: -80px; animation: floatDot 6s ease-in-out infinite; }
body::after { bottom: -80px; right: -80px; animation: floatDot 8s ease-in-out infinite reverse; }
/* ========== 页面标题 ========== */
.page-title {
text-align: center;
margin-bottom: 18px;
font-size: 28px;
font-weight: 900;
letter-spacing: 4px;
color: #e0e0e0;
animation: titleGlow 3s ease-in-out infinite;
position: relative;
z-index: 1;
}
.page-title span { color: #4fc3f7; }
/* ========== 游戏容器 ========== */
.game-container {
background: rgba(13, 18, 30, 0.85);
backdrop-filter: blur(12px);
border-radius: 20px;
padding: 28px;
box-shadow: 0 8px 48px rgba(0,0,0,0.7), inset 0 1px 0 rgba(255,255,255,0.05);
border: 1px solid rgba(79,195,247,0.15);
position: relative;
z-index: 1;
}
/* ========== 游戏状态栏 ========== */
.game-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
color: #c8c8d8;
font-size: 16px;
font-weight: 600;
padding: 0 4px;
}
.game-header .stat-item {
display: flex;
align-items: center;
gap: 6px;
background: rgba(255,255,255,0.04);
padding: 6px 14px;
border-radius: 10px;
border: 1px solid rgba(255,255,255,0.06);
}
.game-header .stat-icon { font-size: 18px; }
.game-header .stat-label { opacity: 0.6; font-size: 13px; font-weight: 400; }
.game-header .stat-value {
font-size: 20px;
font-weight: 800;
min-width: 24px;
text-align: center;
}
.game-header .score .stat-value { color: #4fc3f7; text-shadow: 0 0 8px rgba(79,195,247,0.4); }
.game-header .lives .stat-value { color: #ef5350; text-shadow: 0 0 8px rgba(239,83,80,0.4); }
.game-header .level .stat-value { color: #81c784; text-shadow: 0 0 8px rgba(129,199,132,0.4); }
.game-header .kills .stat-value { color: #ffb74d; text-shadow: 0 0 8px rgba(255,183,77,0.4); }
/* ========== 画布 ========== */
canvas {
display: block;
border: 2px solid rgba(79,195,247,0.3);
border-radius: 12px;
background: #141428;
animation: borderPulse 4s ease-in-out infinite, neonPulse 3s ease-in-out infinite;
position: relative;
z-index: 1;
}
.canvas-wrapper {
position: relative;
display: inline-block;
}
.canvas-wrapper::before {
content: '';
position: absolute;
inset: -3px;
border-radius: 14px;
background: linear-gradient(45deg, #4fc3f7, #81c784, #4fc3f7);
opacity: 0.15;
z-index: 0;
animation: neonPulse 4s ease-in-out infinite;
}
/* ========== 游戏结束遮罩 ========== */
.game-over-overlay {
display: none;
position: absolute;
top: 0; left: 0; right: 0; bottom: 0;
background: rgba(0,0,0,0.7);
backdrop-filter: blur(4px);
border-radius: 12px;
justify-content: center;
align-items: center;
flex-direction: column;
color: white;
z-index: 10;
animation: overlayIn 0.6s ease-out;
}
.game-over-overlay.show {
display: flex;
}
.game-over-overlay h1 {
font-size: 52px;
margin-bottom: 10px;
animation: fadeInUp 0.5s ease-out 0.1s both;
text-shadow: 0 0 20px rgba(255,255,255,0.3);
}
.game-over-overlay .final-score {
font-size: 22px;
margin-bottom: 8px;
color: #4fc3f7;
animation: fadeInUp 0.5s ease-out 0.2s both;
}
.game-over-overlay .sub-text {
font-size: 14px;
color: #999;
margin-bottom: 24px;
animation: fadeInUp 0.5s ease-out 0.3s both;
}
.game-over-overlay .restart-btn {
padding: 14px 48px;
font-size: 20px;
background: linear-gradient(135deg, #4fc3f7, #0288d1);
color: #fff;
border: none;
border-radius: 12px;
cursor: pointer;
font-weight: 700;
letter-spacing: 1px;
transition: all 0.3s;
animation: fadeInUp 0.5s ease-out 0.35s both;
box-shadow: 0 4px 20px rgba(79,195,247,0.3);
}
.game-over-overlay .restart-btn:hover {
transform: translateY(-3px) scale(1.03);
box-shadow: 0 8px 30px rgba(79,195,247,0.5);
background: linear-gradient(135deg, #81c784, #4fc3f7);
}
.game-over-overlay .restart-btn:active {
transform: scale(0.97);
}
/* ========== 操作提示 ========== */
.controls {
margin-top: 16px;
color: #8888aa;
font-size: 13px;
text-align: center;
padding: 10px 16px;
background: rgba(255,255,255,0.03);
border-radius: 12px;
border: 1px solid rgba(255,255,255,0.05);
letter-spacing: 0.5px;
}
.controls kbd {
background: rgba(79,195,247,0.1);
color: #4fc3f7;
padding: 3px 10px;
border-radius: 6px;
font-size: 12px;
border: 1px solid rgba(79,195,247,0.2);
font-family: inherit;
font-weight: 600;
}
.controls .divider {
color: rgba(255,255,255,0.1);
margin: 0 6px;
}
/* ========== 错误横幅 ========== */
.error-banner {
display: none;
background: linear-gradient(135deg, #c62828, #b71c1c);
color: white;
padding: 10px 20px;
border-radius: 10px;
margin-bottom: 10px;
font-size: 14px;
text-align: center;
font-weight: 500;
box-shadow: 0 4px 16px rgba(198,40,40,0.3);
}
.error-banner.show { display: block; }
</style>
</head>
<body>
<div class="game-container">
<div class="page-title">⚡ 坦克<span>大战</span> ⚡</div>
<div class="game-header">
<div class="stat-item level">
<span class="stat-icon">🏰</span>
<span class="stat-label">关卡</span>
<span class="stat-value" id="levelDisplay">1</span>
</div>
<div class="stat-item score">
<span class="stat-icon">🏆</span>
<span class="stat-label">分数</span>
<span class="stat-value" id="scoreDisplay">0</span>
</div>
<div class="stat-item kills">
<span class="stat-icon">💀</span>
<span class="stat-label">击杀</span>
<span class="stat-value" id="killsDisplay">0</span>
</div>
<div class="stat-item lives">
<span class="stat-icon">❤️</span>
<span class="stat-label">生命</span>
<span class="stat-value" id="livesDisplay">3</span>
</div>
</div>
<div class="error-banner" id="errorBanner"></div>
<div class="canvas-wrapper">
<canvas id="gameCanvas" width="800" height="600"></canvas>
<div class="game-over-overlay" id="gameOverOverlay">
<h1 id="resultTitle">🎮 游戏结束</h1>
<div class="final-score">最终得分: <span id="finalScore">0</span></div>
<div class="sub-text" id="subText"></div>
<button class="restart-btn" id="restartBtn">🔄 重新开始</button>
</div>
</div>
<div class="controls">
<kbd>↑</kbd><kbd>↓</kbd><kbd>←</kbd><kbd>→</kbd> 移动 <span class="divider">|</span> <kbd>空格</kbd> 开火 <span class="divider">|</span> <kbd>P</kbd> 暂停
</div>
</div>
<script>
(function() {
'use strict';
// ---------- 安全工具函数 ----------
function safeGetElement(id) {
try {
const el = document.getElementById(id);
if (!el) throw new Error(`Element #${id} not found`);
return el;
} catch (e) {
console.error(`[safeGetElement] ${e.message}`);
return null;
}
}
function safeSetText(id, value) {
try {
const el = safeGetElement(id);
if (el) el.textContent = value;
} catch (e) {
console.error(`[safeSetText] ${e.message}`);
}
}
// ---------- DOM 引用 ----------
const canvas = safeGetElement('gameCanvas');
const ctx = canvas ? canvas.getContext('2d') : null;
const restartBtn = safeGetElement('restartBtn');
const gameOverOverlay = safeGetElement('gameOverOverlay');
const errorBanner = safeGetElement('errorBanner');
if (!canvas || !ctx) {
if (errorBanner) {
errorBanner.textContent = '❌ 画布初始化失败,请检查浏览器支持';
errorBanner.classList.add('show');
}
return;
}
// ---------- 游戏常量 ----------
const TANK_SIZE = 36;
const BULLET_SIZE = 6;
const TANK_SPEED = 2.5;
const BULLET_SPEED = 5;
const ENEMY_SPEED = 1.2;
const ENEMY_SHOOT_INTERVAL = 1200; // ms
const MAX_ENEMIES = 4;
const SPAWN_INTERVAL = 4000; // ms
const INVINCIBLE_DURATION = 2000; // ms
// ---------- 游戏状态 ----------
let game = null;
// ---------- 工具函数 ----------
function rand(min, max) {
try {
return Math.floor(Math.random() * (max - min + 1)) + min;
} catch (e) {
return min;
}
}
function rectCollide(a, b) {
try {
return a.x < b.x + b.w && a.x + a.w > b.x &&
a.y < b.y + b.h && a.y + a.h > b.y;
} catch (e) {
return false;
}
}
function distance(x1, y1, x2, y2) {
try {
return Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2);
} catch (e) {
return Infinity;
}
}
// ---------- 墙壁生成 ----------
function generateWalls(canvasW, canvasH) {
try {
const walls = [];
const w = 40, h = 20;
// 外围墙壁 (除了顶部留出入口)
for (let x = 0; x < canvasW; x += w) {
walls.push({ x, y: 0, w, h, hp: 3, type: 'brick' });
walls.push({ x, y: canvasH - h, w, h, hp: 3, type: 'brick' });
}
for (let y = 0; y < canvasH; y += h) {
walls.push({ x: 0, y, w, h, hp: 3, type: 'brick' });
walls.push({ x: canvasW - w, y, w, h, hp: 3, type: 'brick' });
}
// 内部障碍
const innerPositions = [
[120, 120], [320, 120], [520, 120],
[200, 240], [480, 240],
[120, 360], [320, 360], [520, 360],
[200, 480], [480, 480],
[360, 180], [360, 380],
];
for (const [px, py] of innerPositions) {
walls.push({ x: px, y: py, w: w * 1.5, h: h * 1.5, hp: 3, type: 'brick' });
}
// 钢铁墙壁 (不可摧毁)
const steelPositions = [[360, 80], [720, 280], [40, 280]];
for (const [px, py] of steelPositions) {
walls.push({ x: px, y: py, w: w, h: h, hp: Infinity, type: 'steel' });
}
return walls;
} catch (e) {
console.error('[generateWalls]', e);
return [];
}
}
// ---------- 创建玩家 ----------
function createPlayer(canvasW, canvasH) {
try {
return {
x: canvasW / 2 - TANK_SIZE / 2,
y: canvasH - TANK_SIZE - 20,
w: TANK_SIZE,
h: TANK_SIZE,
dir: 'up',
speed: TANK_SPEED,
hp: 3,
maxHp: 3,
invincibleUntil: 0,
cooldown: 0,
cooldownMax: 15, // frames
alive: true,
speedBoostUntil: 0,
};
} catch (e) {
console.error('[createPlayer]', e);
return null;
}
}
// ---------- 创建敌人 ----------
function createEnemy(canvasW, canvasH) {
try {
const spawns = [
{ x: 40, y: 40 },
{ x: canvasW / 2 - TANK_SIZE / 2, y: 40 },
{ x: canvasW - TANK_SIZE - 40, y: 40 },
];
const pos = spawns[rand(0, spawns.length - 1)];
return {
x: pos.x,
y: pos.y,
w: TANK_SIZE,
h: TANK_SIZE,
dir: 'down',
speed: ENEMY_SPEED + (Math.random() * 0.4),
hp: 1,
alive: true,
isEnemy: true,
cooldown: 0,
cooldownMax: rand(30, 80),
moveTimer: 0,
moveDir: 'down',
score: 100,
};
} catch (e) {
console.error('[createEnemy]', e);
return null;
}
}
// ---------- 初始化游戏 ----------
function initGame() {
try {
const cw = canvas.width;
const ch = canvas.height;
const player = createPlayer(cw, ch);
if (!player) throw new Error('Failed to create player');
game = {
player,
enemies: [],
bullets: [],
walls: generateWalls(cw, ch),
particles: [],
healthPacks: [],
powerups: [],
score: 0,
kills: 0,
level: 1,
paused: false,
gameOver: false,
win: false,
frame: 0,
spawnTimer: 0,
powerupSpawnTimer: 600,
keys: {},
canvasW: cw,
canvasH: ch,
};
// 初始生成 2 个敌人(适度挑战)
for (let i = 0; i < 2; i++) {
try {
const enemy = createEnemy(cw, ch);
if (enemy) game.enemies.push(enemy);
} catch (e) {
console.error('[initGame] spawn enemy', e);
}
}
updateUI();
if (gameOverOverlay) gameOverOverlay.classList.remove('show');
if (errorBanner) errorBanner.classList.remove('show');
return true;
} catch (e) {
console.error('[initGame]', e);
if (errorBanner) {
errorBanner.textContent = '❌ 游戏初始化失败: ' + e.message;
errorBanner.classList.add('show');
}
return false;
}
}
// ---------- 碰撞检测 ----------
function canMove(entity, nx, ny, walls, enemies, player) {
try {
const rect = { x: nx, y: ny, w: entity.w, h: entity.h };
// 边界
if (nx < 0 || nx + entity.w > canvas.width ||
ny < 0 || ny + entity.h > canvas.height) {
return false;
}
// 墙壁 (跳过已摧毁的墙)
for (const wall of walls) {
if (wall.hp <= 0) continue;
if (rectCollide(rect, wall)) return false;
}
// 玩家与敌人双向碰撞(敌人之间不互相碰撞)
if (entity.isEnemy) {
// 敌人与玩家
if (player && player.alive && rectCollide(rect, player)) return false;
} else {
// 玩家与敌人
for (const other of enemies) {
if (!other || !other.alive) continue;
if (rectCollide(rect, other)) return false;
}
}
return true;
} catch (e) {
console.error('[canMove]', e);
return false;
}
}
// ---------- 射击 ----------
function shoot(entity, isEnemy) {
try {
if (!game || !entity || !entity.alive) return;
const cx = entity.x + entity.w / 2 - BULLET_SIZE / 2;
const cy = entity.y + entity.h / 2 - BULLET_SIZE / 2;
let vx = 0, vy = 0;
switch (entity.dir) {
case 'up': vy = -BULLET_SPEED; break;
case 'down': vy = BULLET_SPEED; break;
case 'left': vx = -BULLET_SPEED; break;
case 'right': vx = BULLET_SPEED; break;
default: vy = -BULLET_SPEED;
}
game.bullets.push({
x: cx, y: cy,
w: BULLET_SIZE, h: BULLET_SIZE,
vx, vy,
isEnemy,
alive: true,
});
} catch (e) {
console.error('[shoot]', e);
}
}
// ---------- 添加粒子效果 ----------
function addExplosion(x, y, count, color) {
try {
if (!game) return;
for (let i = 0; i < count; i++) {
const angle = Math.random() * Math.PI * 2;
const speed = 1 + Math.random() * 3;
game.particles.push({
x, y,
vx: Math.cos(angle) * speed,
vy: Math.sin(angle) * speed,
life: 20 + rand(0, 20),
maxLife: 40,
size: 2 + rand(0, 4),
color: color || '#ff6f00',
});
}
} catch (e) {
console.error('[addExplosion]', e);
}
}
// ---------- 生成血包 ----------
function spawnHealthPack() {
try {
if (!game) return;
const size = 20;
for (let attempt = 0; attempt < 20; attempt++) {
const x = rand(40, game.canvasW - 40 - size);
const y = rand(40, game.canvasH - 40 - size);
const rect = { x, y, w: size, h: size };
let blocked = false;
for (const wall of game.walls) {
if (wall.hp <= 0) continue;
if (rectCollide(rect, wall)) { blocked = true; break; }
}
if (!blocked) {
game.healthPacks.push({ x, y, w: size, h: size, alive: true, createdAt: Date.now() });
return;
}
}
} catch (e) {
console.error('[spawnHealthPack]', e);
}
}
// ---------- 生成速度道具 ----------
function spawnSpeedPowerup() {
try {
if (!game) return;
const size = 20;
for (let attempt = 0; attempt < 20; attempt++) {
const x = rand(40, game.canvasW - 40 - size);
const y = rand(40, game.canvasH - 40 - size);
const rect = { x, y, w: size, h: size };
let blocked = false;
for (const wall of game.walls) {
if (wall.hp <= 0) continue;
if (rectCollide(rect, wall)) { blocked = true; break; }
}
if (!blocked) {
game.powerups.push({ x, y, w: size, h: size, alive: true, createdAt: Date.now() });
return;
}
}
} catch (e) {
console.error('[spawnSpeedPowerup]', e);
}
}
// ---------- 玩家输入 ----------
function setupInput() {
try {
window.addEventListener('keydown', (e) => {
try {
if (!game) return;
game.keys[e.key] = true;
if (e.key === 'p' || e.key === 'P') {
game.paused = !game.paused;
}
if (e.key === ' ' && !game.gameOver) {
e.preventDefault();
if (game.player && game.player.alive && game.player.cooldown <= 0) {
shoot(game.player, false);
game.player.cooldown = game.player.cooldownMax;
}
}
} catch (err) {
console.error('[keydown handler]', err);
}
});
window.addEventListener('keyup', (e) => {
try {
if (game) game.keys[e.key] = false;
} catch (err) {
console.error('[keyup handler]', err);
}
});
// 重启按钮
if (restartBtn) {
restartBtn.addEventListener('click', () => {
try {
if (initGame()) {
requestAnimationFrame(gameLoop);
}
} catch (err) {
console.error('[restart]', err);
}
});
}
} catch (e) {
console.error('[setupInput]', e);
}
}
// ---------- 更新 UI ----------
function updateUI() {
try {
if (!game) return;
safeSetText('scoreDisplay', game.score);
safeSetText('killsDisplay', game.kills);
safeSetText('livesDisplay', game.player ? game.player.hp : 0);
safeSetText('levelDisplay', game.level);
} catch (e) {
console.error('[updateUI]', e);
}
}
// ---------- 更新逻辑 ----------
function update() {
try {
if (!game || game.paused || game.gameOver) return;
const { player, enemies, bullets, walls, canvasW, canvasH } = game;
game.frame++;
// ----- 玩家移动 -----
if (player && player.alive) {
try {
let dx = 0, dy = 0;
if (game.keys['ArrowUp'] || game.keys['w'] || game.keys['W']) { dy = -1; player.dir = 'up'; }
else if (game.keys['ArrowDown'] || game.keys['s'] || game.keys['S']) { dy = 1; player.dir = 'down'; }
else if (game.keys['ArrowLeft'] || game.keys['a'] || game.keys['A']) { dx = -1; player.dir = 'left'; }
else if (game.keys['ArrowRight'] || game.keys['d'] || game.keys['D']) { dx = 1; player.dir = 'right'; }
if (dx !== 0 || dy !== 0) {
const nx = player.x + dx * player.speed;
const ny = player.y + dy * player.speed;
if (canMove(player, nx, ny, walls, enemies, player)) {
player.x = nx;
player.y = ny;
} else if (dx !== 0) {
// 尝试仅 x 移动
if (canMove(player, nx, player.y, walls, enemies, player)) {
player.x = nx;
}
} else if (dy !== 0) {
if (canMove(player, player.x, ny, walls, enemies, player)) {
player.y = ny;
}
}
}
if (player.cooldown > 0) player.cooldown--;
// 键盘射击 (空格已在 keydown 中处理)
} catch (e) {
console.error('[player update]', e);
}
}
// ----- 敌人 AI -----
for (const enemy of enemies) {
try {
if (!enemy.alive) continue;
enemy.moveTimer--;
if (enemy.moveTimer <= 0) {
const dirs = ['up', 'down', 'left', 'right'];
enemy.moveDir = dirs[rand(0, 3)];
enemy.moveTimer = rand(40, 120);
}
let dx = 0, dy = 0;
switch (enemy.moveDir) {
case 'up': dy = -1; enemy.dir = 'up'; break;
case 'down': dy = 1; enemy.dir = 'down'; break;
case 'left': dx = -1; enemy.dir = 'left'; break;
case 'right': dx = 1; enemy.dir = 'right'; break;
}
const nx = enemy.x + dx * enemy.speed;
const ny = enemy.y + dy * enemy.speed;
if (canMove(enemy, nx, ny, walls, enemies, player)) {
enemy.x = nx;
enemy.y = ny;
} else {
enemy.moveDir = dirs[rand(0, 3)];
enemy.moveTimer = rand(20, 60);
}
// 敌人射击
if (enemy.cooldown <= 0) {
if (player && player.alive && distance(enemy.x, enemy.y, player.x, player.y) < 500) {
// 瞄准玩家方向
const dx2 = player.x - enemy.x;
const dy2 = player.y - enemy.y;
if (Math.abs(dx2) > Math.abs(dy2)) {
enemy.dir = dx2 > 0 ? 'right' : 'left';
} else {
enemy.dir = dy2 > 0 ? 'down' : 'up';
}
shoot(enemy, true);
enemy.cooldown = rand(20, 60);
} else if (Math.random() < 0.02) {
shoot(enemy, true);
enemy.cooldown = enemy.cooldownMax;
}
} else {
enemy.cooldown--;
}
} catch (e) {
console.error('[enemy update]', e);
}
}
// ----- 子弹更新 -----
for (const bullet of bullets) {
try {
if (!bullet.alive) continue;
bullet.x += bullet.vx;
bullet.y += bullet.vy;
// 边界
if (bullet.x < 0 || bullet.x > canvasW ||
bullet.y < 0 || bullet.y > canvasH) {
bullet.alive = false;
continue;
}
// 墙壁碰撞 (跳过已摧毁的墙)
for (const wall of walls) {
if (wall.hp <= 0) continue;
if (rectCollide(bullet, wall)) {
bullet.alive = false;
if (wall.hp !== Infinity) {
wall.hp--;
}
addExplosion(bullet.x, bullet.y, 6, '#ffab00');
break;
}
}
if (!bullet.alive) continue;
// 玩家子弹击中敌人
if (!bullet.isEnemy) {
for (const enemy of enemies) {
if (!enemy.alive) continue;
if (rectCollide(bullet, enemy)) {
bullet.alive = false;
enemy.alive = false;
game.score += enemy.score;
game.kills++;
addExplosion(enemy.x + enemy.w / 2, enemy.y + enemy.h / 2, 20, '#ff6f00');
spawnHealthPack();
updateUI();
break;
}
}
}
// 敌人子弹击中玩家
if (bullet.isEnemy && player && player.alive) {
if (rectCollide(bullet, player)) {
bullet.alive = false;
if (game.frame > player.invincibleUntil) {
player.hp--;
player.invincibleUntil = game.frame + INVINCIBLE_DURATION / (1000 / 60);
addExplosion(player.x + player.w / 2, player.y + player.h / 2, 15, '#4fc3f7');
updateUI();
if (player.hp <= 0) {
player.alive = false;
addExplosion(player.x + player.w / 2, player.y + player.h / 2, 30, '#ef5350');
}
}
}
}
} catch (e) {
console.error('[bullet update]', e);
}
}
// ----- 生成敌人 -----
try {
game.spawnTimer--;
if (game.spawnTimer <= 0 && enemies.filter(e => e.alive).length < MAX_ENEMIES) {
const enemy = createEnemy(canvasW, canvasH);
if (enemy) enemies.push(enemy);
game.spawnTimer = SPAWN_INTERVAL / (1000 / 60);
}
} catch (e) {
console.error('[spawn enemy]', e);
}
// ----- 生成速度道具(每 10 秒)-----
try {
game.powerupSpawnTimer--;
if (game.powerupSpawnTimer <= 0) {
spawnSpeedPowerup();
game.powerupSpawnTimer = 600; // 10秒 × 60帧
}
} catch (e) {
console.error('[spawn powerup]', e);
}
// ----- 粒子更新 -----
for (const p of game.particles) {
try {
p.x += p.vx;
p.y += p.vy;
p.vx *= 0.95;
p.vy *= 0.95;
p.life--;
} catch (e) { /* skip bad particle */ }
}
game.particles = game.particles.filter(p => p.life > 0);
// ----- 血包拾取 -----
if (player && player.alive) {
for (const hp of game.healthPacks) {
if (!hp.alive) continue;
if (rectCollide(player, hp)) {
hp.alive = false;
if (player.hp < player.maxHp) {
player.hp++;
}
addExplosion(hp.x + hp.w / 2, hp.y + hp.h / 2, 10, '#81c784');
updateUI();
}
}
}
game.healthPacks = game.healthPacks.filter(hp => {
if (!hp.alive) return false;
if (Date.now() - hp.createdAt > 30000) return false;
return true;
});
// ----- 速度道具拾取 -----
if (player && player.alive) {
for (const pu of game.powerups) {
if (!pu.alive) continue;
if (rectCollide(player, pu)) {
pu.alive = false;
player.speed = TANK_SPEED * 2;
player.speedBoostUntil = Date.now() + 5000;
addExplosion(pu.x + pu.w / 2, pu.y + pu.h / 2, 12, '#4fc3f7');
}
}
}
// 速度道具过期清理(15秒)+ 加速到期恢复
game.powerups = game.powerups.filter(pu => {
if (!pu.alive) return false;
if (Date.now() - pu.createdAt > 15000) return false;
return true;
});
if (player && player.alive && player.speedBoostUntil > 0 && Date.now() > player.speedBoostUntil) {
player.speed = TANK_SPEED;
player.speedBoostUntil = 0;
}
// ----- 清理 -----
game.bullets = bullets.filter(b => b.alive);
game.enemies = enemies.filter(e => e.alive);
// ----- 游戏结束 -----
if (!player || !player.alive) {
game.gameOver = true;
game.win = false;
showGameOver(false);
}
// ----- 胜利条件 (消灭 20 个敌人) -----
if (game.score >= 2000 && player && player.alive) {
game.gameOver = true;
game.win = true;
showGameOver(true);
}
} catch (e) {
console.error('[update]', e);
}
}
// ---------- 显示游戏结束 ----------
function showGameOver(win) {
try {
if (!gameOverOverlay) return;
const titleEl = safeGetElement('resultTitle');
const scoreEl = safeGetElement('finalScore');
const subTextEl = safeGetElement('subText');
if (titleEl) titleEl.textContent = win ? '🏆 胜利!' : '💀 游戏结束';
if (scoreEl) scoreEl.textContent = game ? game.score : 0;
if (subTextEl && game) {
subTextEl.textContent = win
? `🎯 消灭 ${game.kills} 个敌人,你是坦克之王!`
: `💪 消灭 ${game.kills} 个敌人,再接再厉!`;
}
gameOverOverlay.classList.add('show');
} catch (e) {
console.error('[showGameOver]', e);
}
}
// ---------- 渲染 ----------
function draw() {
try {
if (!game || !ctx) return;
const { player, enemies, bullets, walls, particles, healthPacks, powerups } = game;
ctx.clearRect(0, 0, canvas.width, canvas.height);
// 绘制网格背景
try {
ctx.strokeStyle = '#3a3a5c';
ctx.lineWidth = 0.5;
for (let x = 0; x < canvas.width; x += 40) {
ctx.beginPath();
ctx.moveTo(x, 0);
ctx.lineTo(x, canvas.height);
ctx.stroke();
}
for (let y = 0; y < canvas.height; y += 40) {
ctx.beginPath();
ctx.moveTo(0, y);
ctx.lineTo(canvas.width, y);
ctx.stroke();
}
} catch (e) { /* skip grid */ }
// 绘制墙壁
for (const wall of walls) {
try {
if (wall.hp <= 0) continue;
if (wall.type === 'steel') {
ctx.fillStyle = '#78909c';
ctx.fillRect(wall.x, wall.y, wall.w, wall.h);
ctx.strokeStyle = '#546e7a';
ctx.lineWidth = 1;
ctx.strokeRect(wall.x, wall.y, wall.w, wall.h);
// 钢纹
ctx.fillStyle = '#90a4ae';
ctx.fillRect(wall.x + 4, wall.y + 4, wall.w - 8, 4);
ctx.fillRect(wall.x + 4, wall.y + wall.h - 8, wall.w - 8, 4);
} else {
// 砖墙 - 根据血量显示破损程度
const alpha = wall.hp / 3;
ctx.fillStyle = `rgba(188, 108, 37, ${alpha})`;
ctx.fillRect(wall.x, wall.y, wall.w, wall.h);
ctx.strokeStyle = '#6d4c21';
ctx.lineWidth = 1;
// 砖纹
for (let bx = wall.x; bx < wall.x + wall.w; bx += 20) {
for (let by = wall.y; by < wall.y + wall.h; by += 10) {
ctx.strokeRect(bx, by, 20, 10);
}
}
}
} catch (e) { /* skip wall */ }
}
// 绘制坦克
function drawTank(tank, color, isPlayer) {
try {
if (!tank || !tank.alive) return;
const cx = tank.x + tank.w / 2;
const cy = tank.y + tank.h / 2;
ctx.save();
ctx.translate(cx, cy);
// 闪烁 (无敌时)
if (isPlayer && game.frame < tank.invincibleUntil &&
Math.floor(game.frame / 6) % 2 === 0) {
ctx.globalAlpha = 0.5;
}
// 坦克主体
ctx.fillStyle = color;
ctx.fillRect(-tank.w / 2, -tank.h / 2, tank.w, tank.h);
// 履带
ctx.fillStyle = isPlayer ? '#2e7d32' : '#b71c1c';
ctx.fillRect(-tank.w / 2, -tank.h / 2, 5, tank.h);
ctx.fillRect(tank.w / 2 - 5, -tank.h / 2, 5, tank.h);
// 炮塔
ctx.fillStyle = isPlayer ? '#66bb6a' : '#ef5350';
const turretSize = tank.w * 0.55;
ctx.fillRect(-turretSize / 2, -turretSize / 2, turretSize, turretSize);
// 炮管
ctx.fillStyle = isPlayer ? '#388e3c' : '#c62828';
const barrelLen = tank.w * 0.6;
const barrelW = 5;
switch (tank.dir) {
case 'up': ctx.fillRect(-barrelW / 2, -tank.h / 2 - barrelLen / 2, barrelW, barrelLen); break;
case 'down': ctx.fillRect(-barrelW / 2, tank.h / 2 - barrelLen / 2, barrelW, barrelLen); break;
case 'left': ctx.fillRect(-tank.w / 2 - barrelLen / 2, -barrelW / 2, barrelLen, barrelW); break;
case 'right': ctx.fillRect(tank.w / 2 - barrelLen / 2, -barrelW / 2, barrelLen, barrelW); break;
}
ctx.restore();
} catch (e) {
console.error('[drawTank]', e);
}
}
drawTank(player, '#43a047', true);
for (const enemy of enemies) {
drawTank(enemy, '#e53935', false);
}
// 绘制子弹
for (const bullet of bullets) {
try {
if (!bullet.alive) continue;
ctx.fillStyle = bullet.isEnemy ? '#ff7043' : '#4fc3f7';
ctx.shadowColor = bullet.isEnemy ? '#ff7043' : '#4fc3f7';
ctx.shadowBlur = 8;
ctx.beginPath();
ctx.arc(bullet.x + bullet.w / 2, bullet.y + bullet.h / 2, bullet.w / 2 + 1, 0, Math.PI * 2);
ctx.fill();
ctx.shadowBlur = 0;
} catch (e) { /* skip bullet */ }
}
// 绘制粒子
for (const p of particles) {
try {
const alpha = p.life / p.maxLife;
ctx.globalAlpha = alpha;
ctx.fillStyle = p.color;
ctx.fillRect(p.x - p.size / 2, p.y - p.size / 2, p.size, p.size);
} catch (e) { /* skip particle */ }
}
ctx.globalAlpha = 1;
// 绘制血包
const now = Date.now();
for (const hp of healthPacks) {
try {
if (!hp.alive) continue;
const age = now - hp.createdAt;
// 最后 5 秒闪烁
if (age > 25000 && Math.floor(age / 200) % 2 === 0) continue;
// 绿色底板
ctx.fillStyle = '#2e7d32';
ctx.shadowColor = '#4caf50';
ctx.shadowBlur = 10;
ctx.fillRect(hp.x, hp.y, hp.w, hp.h);
// 白色十字
ctx.shadowBlur = 0;
ctx.fillStyle = '#ffffff';
ctx.fillRect(hp.x + 8, hp.y + 2, 4, hp.h - 4);
ctx.fillRect(hp.x + 2, hp.y + 8, hp.w - 4, 4);
} catch (e) { /* skip health pack */ }
}
ctx.shadowBlur = 0;
// 绘制速度道具(蓝色闪电)
for (const pu of powerups) {
try {
if (!pu.alive) continue;
// 蓝色底板
ctx.fillStyle = '#1565c0';
ctx.shadowColor = '#42a5f5';
ctx.shadowBlur = 12;
ctx.fillRect(pu.x, pu.y, pu.w, pu.h);
// 白色闪电 ⚡
ctx.shadowBlur = 0;
ctx.fillStyle = '#ffffff';
ctx.font = 'bold 16px sans-serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText('⚡', pu.x + pu.w / 2, pu.y + pu.h / 2 + 1);
} catch (e) { /* skip powerup */ }
}
ctx.shadowBlur = 0;
// 加速状态指示
if (player && player.alive && player.speedBoostUntil > 0) {
ctx.fillStyle = '#42a5f5';
ctx.font = 'bold 14px sans-serif';
ctx.textAlign = 'left';
ctx.textBaseline = 'top';
ctx.fillText('⚡ 加速中', 10, 10);
}
// 暂停提示
if (game.paused && !game.gameOver) {
try {
ctx.fillStyle = 'rgba(0,0,0,0.5)';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = 'white';
ctx.font = 'bold 48px sans-serif';
ctx.textAlign = 'center';
ctx.fillText('⏸ 暂停中', canvas.width / 2, canvas.height / 2);
ctx.font = '20px sans-serif';
ctx.fillText('按 P 继续', canvas.width / 2, canvas.height / 2 + 50);
} catch (e) { /* skip pause overlay */ }
}
} catch (e) {
console.error('[draw]', e);
}
}
// ---------- 游戏主循环 ----------
function gameLoop() {
try {
if (!game) return;
update();
draw();
if (!game.gameOver) {
requestAnimationFrame(gameLoop);
}
} catch (e) {
console.error('[gameLoop]', e);
// 尝试恢复:继续循环
try { requestAnimationFrame(gameLoop); } catch (err) {
console.error('[gameLoop recovery failed]', err);
}
}
}
// ---------- 启动 ----------
function start() {
try {
if (initGame()) {
setupInput();
requestAnimationFrame(gameLoop);
console.log('🎮 坦克大战启动成功!');
} else {
console.error('❌ 游戏初始化失败');
}
} catch (e) {
console.error('[start]', e);
if (errorBanner) {
errorBanner.textContent = '❌ 游戏启动失败: ' + e.message;
errorBanner.classList.add('show');
}
}
}
// 页面加载完成后启动
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', start);
} else {
start();
}
})();
</script>
</body>
</html>
更多推荐







所有评论(0)