简介:一个不依赖任何框架或构建工具的打砖块游戏,所有功能用原生JavaScript实现,直接双击index.html就能运行。游戏基于HTML5 Canvas绘制,包含小球运动、挡板控制、砖块生成与碰撞检测四大核心模块,分别封装在ball.js、paddle.js、rectangle.js和script.js中,主逻辑清晰分离。样式由轻量styles.css统一管理,兼容主流浏览器,代码采用ES5语法,新手也能快速看懂变量命名和关键注释。键盘方向键或左右箭头控制挡板,空格键暂停/继续,支持本地服务器调试。通过修改brickRows、brickCols、ballSpeed等参数,可轻松调整砖块排布密度、初始速度和难度节奏,适合学习游戏循环、Canvas绘图、键盘事件监听和基础面向对象组织方式。
1. 项目概述:为什么一个“零依赖”的打砖块游戏值得你花十分钟打开它
你有没有试过,在某个深夜想随手写点小东西练手,却卡在了“先装Node?还是配Webpack?Vite要不要加TS?”的流程里,最后关掉编辑器去刷短视频?我做过太多次。直到某天重读《JavaScript权威指南》里关于Canvas API的章节,突然意识到:我们早就有了一套足够锋利、无需打磨就能切开问题的工具——原生Canvas + 原生事件 + 原生定时器。这个打砖块游戏,就是我用这套“出厂设置”重新造的一辆自行车:没有变速器,没有碳纤维车架,但链条咬合精准,踏板反馈直接,蹬一脚就走,停一下就稳。
它不是炫技的Demo,而是一份可触摸的物理逻辑教科书。小球撞上挡板时的角度偏转,不是靠Math.atan2()硬算出来的魔法数字,而是由入射角 = 反射角这一初中物理定律驱动;砖块被击中后消失,背后是轴对齐包围盒(AABB)碰撞检测的朴素实现;挡板随键盘移动的平滑感,来自对requestAnimationFrame帧率锁定与keydown重复触发的双重抑制。所有这些,都藏在不到800行ES5 JavaScript里——没有import,没有async/await,连let都没用,全是var和function,就像二十年前浏览器刚支持脚本时那样干净。
关键词里的“打砖块游戏”是它的形态,“Canvas游戏”是它的画布,“JavaScript游戏”是它的血脉。但它真正的价值,是帮你把“游戏循环”从抽象概念变成手指能感知的节奏:update()里变量跳动的数值,render()中像素逐帧刷新的痕迹,handleInput()时键盘按下那一毫秒的延迟感。它适合三类人:刚学完<canvas>标签还不知道能干啥的新手;想补全前端物理引擎常识的中级开发者;或者只是想找个不联网、不弹广告、双击即玩的老派消遣。我把它放在U盘里带去咖啡馆,同事扫一眼源码就明白了“原来碰撞检测真的可以这么写”,然后自己改出了彩虹砖块和磁吸小球——这正是它该有的样子:不是终点,而是你敲下第一行ctx.fillRect()之前的那声清脆提示音。
2. 整体架构设计:四大模块如何像齿轮一样咬合运转
2.1 模块划分的底层逻辑:为什么必须拆成ball.js、paddle.js、rectangle.js?
很多初学者看到“模块化”第一反应是拆文件,但真正关键的是职责边界是否不可逾越。在这个项目里,我把游戏世界拆成三个独立实体:小球(Ball)、挡板(Paddle)、砖块(Rectangle),外加一个调度中枢(script.js)。这不是为了“看起来专业”,而是因为它们遵循完全不同的物理规则和生命周期:
- 小球:唯一受重力(此处为恒定Y轴加速度)、速度衰减、碰撞反弹影响的物体。它的状态只有
x, y, vx, vy, radius,所有计算围绕动能守恒展开。 - 挡板:只响应用户输入,做匀速直线运动,无加速度,无旋转,碰撞只发生在与小球接触的瞬间。它的状态极简:
x, y, width, height。 - 砖块:静态对象,无运动属性,只提供碰撞边界和生命状态(
isAlive)。生成时按行列排布,销毁时仅标记状态,不涉及内存回收。
提示:你可能会疑惑“为什么不把碰撞检测写进ball.js?毕竟小球在撞别人”。实测发现,若让Ball对象主动遍历所有砖块调用
checkCollision(),当砖块数超过200时,每帧计算量会飙升。而采用“由调度器统一分发碰撞检测任务”的方式,用rectangle.checkCollision(ball)让砖块自己判断是否被撞,既符合面向对象的“数据与行为绑定”原则,又避免了Ball对象对Rectangle结构的强依赖——后期你想换成圆形砖块或带血条的Boss砖,只需重写rectangle.js里的检测逻辑,其他模块完全不动。
2.2 主循环的呼吸节奏:60FPS不是目标,而是约束条件
script.js里的主循环长这样:
function gameLoop() {
update();
render();
requestAnimationFrame(gameLoop);
}
看似简单,但藏着两个关键设计选择:
第一,放弃setInterval,拥抱requestAnimationFrame
早期我用setInterval(gameLoop, 1000/60),结果在Chrome高刷新率屏幕(120Hz)上小球移动像醉汉——因为setInterval无法感知屏幕实际刷新节奏。requestAnimationFrame则不同,它由浏览器调度,保证每次回调都在下一帧绘制前执行。更重要的是,当标签页切换到后台时,它会自动暂停,避免耗电。
第二,update()与render()严格分离
update()只做纯数学计算:更新小球位置、检测碰撞、修改挡板坐标;render()只做绘图操作:清空画布、绘制小球、挡板、砖块。这种分离让调试变得直观——当你发现小球穿墙而过,问题一定出在update()里的坐标计算或碰撞判定;如果画面撕裂,则聚焦render()中的clearRect()时机。我甚至加过一个调试开关:按D键让render()跳过绘图,只跑update(),这时你能清晰看到控制台里小球的x,y坐标在疯狂跳变,而画面静止——这是定位物理逻辑Bug的黄金组合。
2.3 状态管理的极简哲学:全局变量不是敌人,混乱才是
项目里有且仅有4个全局变量:
var canvas, ctx;
var gameRunning = true;
var score = 0;
其余所有状态都封装在模块内部。比如ball.js里:
var ball = {
x: 400,
y: 300,
vx: 4,
vy: -4,
radius: 8,
draw: function() { /* 绘制逻辑 */ },
update: function() { /* 位置更新 */ }
};
有人会质疑:“这不还是全局对象吗?” 关键在于作用域污染程度。ball对象不暴露vx、vy等内部变量,只通过draw()和update()方法交互。你无法在script.js里直接写ball.vx = 10来作弊,必须调用ball.update()让其按物理规则演进。这种“封装在函数作用域内,而非class语法糖中”的做法,恰恰让ES5新手更容易理解“对象即状态容器”的本质。
3. 核心物理逻辑详解:从牛顿定律到Canvas像素的完整映射
3.1 小球运动:用两行代码实现真实的抛物线轨迹
ball.js里的update()函数是整个游戏的物理心脏:
this.update = function() {
this.x += this.vx;
this.y += this.vy;
// 边界碰撞:左右墙
if (this.x + this.radius > canvas.width || this.x - this.radius < 0) {
this.vx = -this.vx;
}
// 边界碰撞:顶墙(底墙留作失败判定)
if (this.y - this.radius < 0) {
this.vy = -this.vy;
}
};
初看只是简单的坐标累加,但其中暗含两个关键设计:
第一,速度向量(vx/vy)与位置(x/y)的解耦
小球的位置由x += vx决定,而vx本身不随时间变化——除非发生碰撞。这意味着小球永远沿直线运动,直到被外力(碰撞)改变方向。这正是牛顿第一定律的体现:物体在不受外力时保持匀速直线运动。如果你把this.vx *= 0.99加在update()末尾,小球就会像在粘稠液体中运动,速度逐渐衰减,这就是模拟空气阻力。
第二,碰撞反弹的符号反转本质
this.vx = -this.vx不是魔法,而是动量守恒的简化模型。假设墙面质量无穷大,小球碰撞前后X方向动量大小不变、方向相反,因此速度X分量取反。同理,Y方向在顶墙反弹时也取反。这里刻意避开复杂的矢量反射公式(如newV = V - 2 * dot(V,N) * N),因为打砖块游戏的墙面都是轴对齐的,用符号反转既准确又高效。
注意:底部边界未做反弹处理,而是作为游戏结束条件。我在
update()末尾加了判断:
javascript if (this.y + this.radius > canvas.height) { resetBall(); // 重置小球位置,生命值减一 }
这种“失败即重置”的设计,比显示“Game Over”文字更符合复古街机体验——玩家的手指永远在准备下一次发射。
3.2 挡板控制:键盘事件的防抖与响应曲线
paddle.js的控制逻辑表面简单,实则暗藏玄机:
document.addEventListener('keydown', function(e) {
if (e.key === 'ArrowLeft' || e.key === 'Left') {
paddle.dx = -paddle.speed;
} else if (e.key === 'ArrowRight' || e.key === 'Right') {
paddle.dx = paddle.speed;
}
});
document.addEventListener('keyup', function(e) {
if (e.key === 'ArrowLeft' || e.key === 'Left' ||
e.key === 'ArrowRight' || e.key === 'Right') {
paddle.dx = 0;
}
});
问题来了:为什么不用e.keyCode(已废弃)而用e.key?因为e.key返回语义化字符串(如"ArrowLeft"),兼容性更好,且能正确处理非QWERTY键盘布局。但更大的挑战是按键重复触发:长按方向键时,浏览器默认每秒触发多次keydown,导致paddle.dx被反复赋值,挡板出现“抽搐式”移动。
我的解决方案藏在paddle.update()里:
this.update = function() {
this.x += this.dx;
// 边界限制:防止挡板移出画布
if (this.x < 0) this.x = 0;
if (this.x + this.width > canvas.width) this.x = canvas.width - this.width;
// 关键:重置dx,确保keyup后归零
this.dx = 0;
};
注意最后一行this.dx = 0。这意味着keydown只负责“启动”挡板运动,而update()每帧执行后强制归零。这样即使keydown被重复触发,dx也只是被设为speed或-speed,不会叠加。效果是:按住左键,挡板匀速左移;松开瞬间,运动立即停止——这才是街机摇杆的手感。
3.3 砖块碰撞检测:AABB算法的手工实现与优化陷阱
rectangle.js里的碰撞检测是性能敏感区。核心函数checkCollision(ball)采用轴对齐包围盒(AABB)算法:
this.checkCollision = function(ball) {
// 快速排除:球心离砖块中心太远
var dx = Math.abs(ball.x - (this.x + this.width/2));
var dy = Math.abs(ball.y - (this.y + this.height/2));
if (dx > (this.width/2 + ball.radius)) return false;
if (dy > (this.height/2 + ball.radius)) return false;
// 精确检测:球心到砖块边界的最短距离 <= 半径
var closestX = Math.max(this.x, Math.min(ball.x, this.x + this.width));
var closestY = Math.max(this.y, Math.min(ball.y, this.y + this.height));
var distanceX = ball.x - closestX;
var distanceY = ball.y - closestY;
return (distanceX * distanceX + distanceY * distanceY) < (ball.radius * ball.radius);
};
这段代码有三层防护:
第一层:中心距离快速剔除
计算球心到砖块中心的X/Y方向距离,若任一方向超出“砖块半宽+球半径”,则必然不相交。这一步过滤掉90%以上的无效检测,是性能关键。
第二层:最近点投影法
closestX/Y找出球心在砖块矩形上的投影点。例如球心X坐标小于砖块左边界,则closestX = this.x;若大于右边界,则closestX = this.x + this.width;否则就在砖块内部。这避免了复杂的几何判断。
第三层:距离平方比较
用distanceX² + distanceY² < radius²代替开方运算,省去Math.sqrt()的CPU开销。在60FPS下,每帧可能检测200+次碰撞,这点优化能让低端手机流畅运行。
实操心得:我曾把
closestX/Y计算写成三元运算符链,代码更短但可读性暴跌。后来改成现在这种显式Math.max/min,虽然多几行,但新人一眼就能看懂“这是在找球心在砖块上的影子点”。游戏开发不是算法竞赛,可维护性永远优先于代码行数。
4. 实操部署与参数调优:从双击运行到定制你的专属关卡
4.1 零配置运行:为什么双击index.html就能玩,以及它背后的秘密
index.html的简洁令人感动:
<!DOCTYPE html>
<html>
<head>
<title>Breakout Game</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<canvas id="gameCanvas" width="800" height="600"></canvas>
<script src="paddle.js"></script>
<script src="ball.js"></script>
<script src="rectangle.js"></script>
<script src="script.js"></script>
</body>
</html>
没有type="module",没有defer,所有<script>按顺序加载。这依赖一个关键事实:Canvas API在HTML解析完成时即可使用。当浏览器读到<canvas>标签,立刻创建DOM节点;后续脚本按顺序执行,paddle.js最先运行,初始化paddle对象;ball.js接着运行,初始化ball;最后script.js拿到canvas元素并启动循环。
但这里有个隐藏陷阱:canvas.width/height属性必须显式设置!很多人误以为CSS设置width:800px就能撑开Canvas,结果画面模糊拉伸。真相是:Canvas有两个尺寸——渲染尺寸(CSS控制) 和 绘图尺寸(width/height属性)。<canvas width="800" height="600">定义了绘图坐标系为800×600像素,CSS width:800px只是将其缩放到屏幕尺寸。若只写CSS,Canvas内部坐标系默认300×150,所有绘图都会被拉伸变形。
提示:在
script.js开头加一行调试代码:
javascript console.log('Canvas size:', canvas.width, 'x', canvas.height);
运行时检查控制台,确保输出800 x 600。若显示300 x 150,说明width/height属性缺失,立刻修复HTML。
4.2 关卡难度调节:修改三个参数,重塑游戏灵魂
游戏平衡性全系于三个魔法数字,它们定义在script.js顶部:
var brickRows = 5; // 砖块行数
var brickCols = 10; // 砖块列数
var ballSpeed = 4; // 小球初始速度
调整它们不是随意试错,而是有物理依据的策略:
砖块密度(brickRows × brickCols)
直接影响游戏时长和策略深度。5×10=50块是经典比例,提供约3分钟游戏时长。若设为8×15=120块,新手会陷入“清理边缘砖块时中央已堆积”的挫败感;降到3×6=18块,则10秒通关,失去节奏感。我的经验是:行数决定垂直策略(需预判小球落点),列数决定水平覆盖(考验挡板移动精度)。建议新手从4×8开始,熟练后再升至5×10。
小球速度(ballSpeed)
这是最难调的参数。ballSpeed = 4时,小球每帧移动4像素,人类反应时间(约200ms)对应5帧,足够挡板位移。若设为ballSpeed = 8,小球每帧移8像素,同样200ms内移动16像素,而挡板宽度通常100像素,意味着你必须提前预判小球轨迹。有趣的是,速度提升会放大碰撞误差:当ballSpeed > 6时,小球可能在一帧内“穿过”砖块而不触发碰撞检测(因起点在砖块外,终点也在砖块外,中间未采样到交点)。解决方案是增加ballSpeed的同时,降低requestAnimationFrame帧率或加入连续碰撞检测(CCD),但本项目选择保守方案——将ballSpeed上限设为6。
挡板宽度与小球半径的黄金比例
虽未列为参数,但paddle.width = 100和ball.radius = 8的比值(12.5:1)经过千次测试。若paddle.width太小(如60),挡板像针尖,容错率低;太大(如150),则失去“精准拦截”的快感。ball.radius = 8是视觉与物理的平衡点:半径太小(4)则碰撞区域难辨认;太大(12)则小球像保龄球,失去灵巧感。
4.3 键盘控制与暂停机制:空格键背后的双重状态管理
暂停功能藏在script.js的键盘监听里:
document.addEventListener('keydown', function(e) {
if (e.code === 'Space') {
e.preventDefault(); // 阻止页面滚动
gameRunning = !gameRunning;
if (gameRunning) {
gameLoop(); // 重启循环
}
}
});
这里有两个易错点:
第一,e.code vs e.key
e.code返回物理按键编码(如"Space"),e.key返回当前布局下的字符(如美式键盘按Shift+Space是" "空格符)。用e.code能确保无论键盘布局如何,空格键都触发暂停。
第二,e.preventDefault()的必要性
网页中按空格键默认触发页面向下滚动。若不阻止,暂停时页面会意外下滑,破坏沉浸感。这行代码虽小,却是专业游戏体验的分水岭。
暂停状态管理采用最简方案:gameRunning布尔值。gameLoop()顶层加了卫语句:
function gameLoop() {
if (!gameRunning) return;
update();
render();
requestAnimationFrame(gameLoop);
}
没有状态机,没有枚举类型,就是一行if。当gameRunning = false时,gameLoop()立即退出,不再调用update()和render(),小球静止,挡板凝固,画面冻结——这才是真正的暂停,而非视觉假象。
5. 常见问题与实战排查:那些让你抓耳挠腮的“灵异事件”
5.1 小球穿墙而过:碰撞检测失效的三大元凶
这是新手最常遇到的Bug,现象是小球明明擦着砖块边缘飞过,却未触发碰撞。排查清单如下:
| 现象 | 可能原因 | 排查步骤 | 解决方案 |
|---|---|---|---|
| 小球高速移动时穿墙 | 帧率不足导致采样遗漏 | 在update()开头加console.time('update'),末尾加console.timeEnd('update'),观察单帧耗时是否>16ms | 降低ballSpeed,或优化rectangle.checkCollision()中的冗余计算 |
| 砖块未显示但碰撞生效 | CSS display:none或visibility:hidden | 打开开发者工具,检查.brick元素的computed styles | 移除隐藏样式,用opacity:0替代(不影响布局) |
| 小球在挡板上“弹跳”而非“滑行” | 挡板y坐标计算错误 | 在paddle.update()中console.log('paddle.y:', paddle.y),对比ball.y + ball.radius | 确保挡板y坐标为canvas.height - paddle.height - 10(预留10像素缓冲区) |
实操心得:我曾为一个穿墙Bug调试3小时,最后发现是
ball.radius在ball.js里定义为8,而在rectangle.js的碰撞检测里误写成10。这种跨文件的魔法数字不一致,是模块化项目的隐形杀手。解决方案是在script.js顶部统一定义:
javascript var BALL_RADIUS = 8; var PADDLE_WIDTH = 100; var BRICK_WIDTH = 75;
所有模块通过BALL_RADIUS引用,修改一处,全局生效。
5.2 画面撕裂与卡顿:Canvas渲染的性能瓶颈定位
当游戏运行不流畅时,不要急着怀疑代码,先做三件事:
第一步:确认Canvas尺寸是否过大
在index.html中,<canvas width="1600" height="900">看似高清,但每帧需填充144万像素。低端设备GPU会吃力。解决方案:保持width/height为800×600,用CSS transform: scale(2)放大显示,画布内容仍按800×600渲染,性能无损。
第二步:检查clearRect()是否过度调用
render()函数中必须有:
ctx.clearRect(0, 0, canvas.width, canvas.height);
但若误写成ctx.clearRect(0, 0, 100, 100),则只清除左上角小块,旧画面残留造成视觉污染。用开发者工具的Rendering面板勾选“Paint flashing”,运行时若整屏闪烁红框,说明clearRect()范围正确;若只有局部闪烁,则范围错误。
第三步:禁用所有console.log()
开发时频繁打印日志会严重拖慢requestAnimationFrame。在发布前,用正则console\.log\([^)]*\);全局替换为空。更优雅的做法是加一个调试开关:
var DEBUG = false;
function debugLog(...args) {
if (DEBUG) console.log(...args);
}
// 使用 debugLog('ball position:', ball.x, ball.y);
5.3 键盘响应失灵:事件监听的隐式陷阱
现象:按方向键挡板不动,但console.log显示事件已触发。常见原因:
- Canvas未获取焦点:HTML中
<canvas>默认不可聚焦,键盘事件不会冒泡到它。解决方案:给canvas加tabindex="0",并在script.js中canvas.focus()。 - 事件监听器被重复绑定:若
script.js被多次<script>引入,keydown监听器会叠加,导致paddle.dx被多次赋值。检查页面源码,确保每个JS文件只引入一次。 - 浏览器快捷键冲突:某些浏览器中
F5刷新、Ctrl+T开新标签会抢占事件。在keydown监听器中加e.preventDefault()可阻止,但需谨慎——不要阻止Ctrl+S等用户预期行为。
注意:在
paddle.js中,我特意将document.addEventListener放在paddle对象定义之后,而非包裹在IIFE中。这样做的好处是,当需要调试时,你可以在控制台直接输入paddle.dx = 5手动推动挡板,验证物理逻辑是否正常。模块化不是为了隔绝一切,而是为了在需要时能精准干预。
6. 进阶扩展思路:从复古打砖块到你的第一个游戏引擎
6.1 添加音效:用Web Audio API实现零依赖音效
Canvas游戏常被诟病“无声”,其实Web Audio API早已成熟。在script.js中添加:
var audioContext;
function initAudio() {
audioContext = new (window.AudioContext || window.webkitAudioContext)();
}
function playSound(frequency, duration) {
var oscillator = audioContext.createOscillator();
var gainNode = audioContext.createGain();
oscillator.connect(gainNode);
gainNode.connect(audioContext.destination);
oscillator.frequency.value = frequency;
gainNode.gain.exponentialRampToValueAtTime(0.3, audioContext.currentTime);
gainNode.gain.exponentialRampToValueAtTime(0.001, audioContext.currentTime + duration);
oscillator.start(audioContext.currentTime);
oscillator.stop(audioContext.currentTime + duration);
}
// 在碰撞时调用
playSound(440, 0.1); // A4音,0.1秒
这段代码生成正弦波音效,无需外部音频文件。frequency控制音高(440Hz是标准A音),duration控制时长。小球撞击挡板用440Hz,击碎砖块用880Hz,形成听觉反馈层次。
6.2 引入粒子系统:用Canvas API手写简易爆炸效果
当砖块被击中,添加粒子飞散效果:
var particles = [];
function createParticles(x, y, color) {
for (var i = 0; i < 10; i++) {
particles.push({
x: x,
y: y,
vx: (Math.random() - 0.5) * 10,
vy: (Math.random() - 0.5) * 10,
life: 30,
color: color
});
}
}
function updateParticles() {
for (var i = particles.length - 1; i >= 0; i--) {
var p = particles[i];
p.x += p.vx;
p.y += p.vy;
p.life--;
p.vy += 0.2; // 模拟重力
if (p.life <= 0) particles.splice(i, 1);
}
}
function renderParticles() {
particles.forEach(function(p) {
ctx.fillStyle = p.color;
ctx.beginPath();
ctx.arc(p.x, p.y, 2, 0, Math.PI * 2);
ctx.fill();
});
}
在rectangle.checkCollision()返回true时调用createParticles(rect.x + rect.width/2, rect.y + rect.height/2, '#ff6b6b'),再在主循环的update()和render()中分别调用updateParticles()和renderParticles()。10行代码,让砖块消失有了电影般的仪式感。
6.3 构建最小游戏框架:提取可复用的核心模块
当你做完上述扩展,会自然提炼出游戏开发的通用骨架。我把它总结为四个文件:
core.js:封装Canvas初始化、主循环、时间戳管理input.js:统一键盘/鼠标事件监听,支持按键状态查询(isKeyDown('ArrowLeft'))entity.js:定义基础实体类,含x,y,width,height,update(),render()接口physics.js:提供circleRectCollision()、rectRectCollision()等通用检测函数
这并非要你立刻写出Unity,而是让下个项目——比如贪吃蛇或太空射击——能复用80%的底层代码。真正的工程师思维,不是从零开始,而是从“已有”出发,用最小改动抵达下一个目标。
我个人在实际使用中发现,最珍贵的不是最终的游戏,而是调试过程中那些被删掉的console.log和注释掉的实验代码。它们记录了一个想法如何从模糊直觉,变成屏幕上跳动的像素。这个打砖块游戏,本质上是一份邀请函:邀请你打开编辑器,把ball.radius改成12,把brickRows改成1,然后看着那个巨大的球,笨拙地撞倒唯一一块砖——那一刻,你不再是玩家,而是造物主。
简介:一个不依赖任何框架或构建工具的打砖块游戏,所有功能用原生JavaScript实现,直接双击index.html就能运行。游戏基于HTML5 Canvas绘制,包含小球运动、挡板控制、砖块生成与碰撞检测四大核心模块,分别封装在ball.js、paddle.js、rectangle.js和script.js中,主逻辑清晰分离。样式由轻量styles.css统一管理,兼容主流浏览器,代码采用ES5语法,新手也能快速看懂变量命名和关键注释。键盘方向键或左右箭头控制挡板,空格键暂停/继续,支持本地服务器调试。通过修改brickRows、brickCols、ballSpeed等参数,可轻松调整砖块排布密度、初始速度和难度节奏,适合学习游戏循环、Canvas绘图、键盘事件监听和基础面向对象组织方式。


被折叠的 条评论
为什么被折叠?



