news 2026/10/14 19:47:41

网页版台球游戏源码实战:Canvas碰撞物理与可玩Demo开发

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
网页版台球游戏源码实战:Canvas碰撞物理与可玩Demo开发

简介:这份网页版台球PCOL游戏源码面向前端开发者与网页游戏爱好者,提供一套可直接在线运行的台球游戏完整实现,无需下载安装即可在浏览器中体验,适合作为前端综合练习与游戏逻辑研究的实践案例。压缩包共54个文件,约4.48MB,以png、jpg图片资源为主,配合hdr环境贴图、fx特效文件、woff2字体、babylon模型与js脚本,另有css样式表、html入口页、json配置及md说明文档,覆盖界面渲染、资源加载与交互逻辑各环节。目前已有785人学习下载,具备一定参考热度。读者可从中了解HTML搭建页面骨架、CSS完成视觉布局、JavaScript驱动击球与碰撞等动态交互的完整链路,并借鉴其目录组织与资源管理方式,用于二次开发或快速迭代新玩法,是学习复杂网页应用构建的实用素材。

1. 网页版台球 PCOL 游戏源码:从物理碰撞到可玩 Demo 的完整落地路径

很多人第一次搜「网页版台球 PCOL 游戏源码」,心里想的其实是两件事:一是能不能直接拿到一份能跑起来的 HTML5 台球代码,二是这套代码里的碰撞和瞄准逻辑到底靠不靠谱。PCOL 在这里通常指 Pool / Billiards 类的碰撞逻辑实现,核心难点不在画球,而在球与球、球与库边之间的弹性碰撞、摩擦衰减和袋口判定。我见过不少 Demo 球能滚、能进袋,但两球相撞后速度分配明显不对,打两杆就露馅。这篇笔记就按「先跑通最小可玩版本,再逐个调物理参数,最后处理袋口和连击判定」的顺序拆开讲,适合想用 Canvas 或 WebGL 做 2D 台球、又不想从零推导碰撞公式的前端和独立开发者。整套方案不依赖重型物理引擎,纯手写向量运算就能撑住 8 球和斯诺克的基础玩法。

2. 用 Canvas 搭出球桌与球体:最小可运行骨架

2.1 为什么选 Canvas 2D 而不是 WebGL 或物理引擎

网页台球这类场景,球体数量通常不超过 22 颗,每帧要算的碰撞对在 200 对以内,Canvas 2D 的绘制开销完全扛得住。用 WebGL 反而要处理纹理、着色器和坐标投影,调试成本翻倍。至于 Matter.js、Planck.js 这类 2D 物理引擎,它们擅长刚体堆叠和关节约束,但台球需要的是高精度、可预测的弹性碰撞,引擎默认的恢复系数和摩擦模型往往偏「软」,调起来比自己写还费劲。我一般会手写向量运算,只把渲染交给 Canvas,这样每一颗球的运动状态都是透明的,出问题能直接打日志看速度和位置。

2.2 球桌坐标系与球体数据结构

先定坐标系:球桌内沿左上角为原点,x 向右,y 向下,单位用像素。标准 8 球桌按 2:1 比例,我常用 800×400 的内沿尺寸,球半径 10px,袋口半径 18px。球的数据结构至少包含位置、速度、半径、是否已进袋、球号。速度用向量表示,方便后续做点积和投影。

// 球体与球桌的基础数据结构 const BALL_RADIUS = 10; const TABLE_W = 800; const TABLE_H = 400; const FRICTION = 0.985; // 每帧速度衰减系数 const MIN_SPEED = 0.05; // 低于此速度视为静止 class Ball { constructor(x, y, id) { this.x = x; this.y = y; this.vx = 0; this.vy = 0; this.id = id; this.active = true; // false 表示已进袋 } // 每帧更新位置并施加摩擦 update() { if (!this.active) return; this.x += this.vx; this.y += this.vy; this.vx *= FRICTION; this.vy *= FRICTION; if (Math.hypot(this.vx, this.vy) < MIN_SPEED) { this.vx = 0; this.vy = 0; } } }

这段代码里FRICTION取 0.985 是 60fps 下的经验值,数值越大球滚得越远。MIN_SPEED用来避免球永远以极小速度漂移,导致「看起来停了但碰撞检测还在跑」的玄学问题。球号id在后续判定犯规和连击时会用到,不能省。

2.3 渲染循环与球桌绘制

渲染层用requestAnimationFrame驱动,每帧先清空画布,再画库边、袋口和所有活跃球。袋口用深色圆表示,库边用矩形描边。为了让球看起来有立体感,可以在球心偏左上画一个高光小圆,这不影响物理,但能明显提升可玩性。

function render(ctx, balls) { ctx.clearRect(0, 0, TABLE_W, TABLE_H); // 画库边 ctx.strokeStyle = '#5a3a1a'; ctx.lineWidth = 8; ctx.strokeRect(0, 0, TABLE_W, TABLE_H); // 画六个袋口 const pockets = [ [0, 0], [TABLE_W / 2, 0], [TABLE_W, 0], [0, TABLE_H], [TABLE_W / 2, TABLE_H], [TABLE_W, TABLE_H] ]; ctx.fillStyle = '#111'; pockets.forEach(([px, py]) => { ctx.beginPath(); ctx.arc(px, py, 18, 0, Math.PI * 2); ctx.fill(); }); // 画球 balls.forEach(b => { if (!b.active) return; ctx.beginPath(); ctx.arc(b.x, b.y, BALL_RADIUS, 0, Math.PI * 2); ctx.fillStyle = b.id === 0 ? '#fff' : '#e33'; ctx.fill(); ctx.strokeStyle = '#333'; ctx.stroke(); }); }

渲染函数本身不碰物理,只读状态。这样后面调碰撞逻辑时,画面不会跟着一起乱。袋口坐标和球桌尺寸绑定,改桌子大小只需改常量,不用动渲染逻辑。

3. 球球碰撞与库边反弹:把弹性公式写对

3.1 两球弹性碰撞的向量解法

台球碰撞可以近似为等质量、无旋转的二维弹性碰撞。两球接触时,沿连心线方向交换速度分量,垂直方向保持不变。这个结论的前提是两球质量相同、碰撞过程无能量损失。实际台球有旋转和摩擦,但网页 Demo 阶段先把这个模型跑通,手感已经能到「能玩」的程度。

function resolveBallCollision(a, b) { const dx = b.x - a.x; const dy = b.y - a.y; const dist = Math.hypot(dx, dy); if (dist === 0 || dist > BALL_RADIUS * 2) return; // 单位法向量 const nx = dx / dist; const ny = dy / dist; // 相对速度在法线上的投影 const dvx = b.vx - a.vx; const dvy = b.vy - a.vy; const vn = dvx * nx + dvy * ny; if (vn > 0) return; // 正在分离,不处理 // 等质量弹性碰撞:交换法线方向速度分量 const impulse = vn; a.vx += impulse * nx; a.vy += impulse * ny; b.vx -= impulse * nx; b.vy -= impulse * ny; // 位置修正,防止两球重叠后反复触发 const overlap = BALL_RADIUS * 2 - dist; a.x -= nx * overlap / 2; a.y -= ny * overlap / 2; b.x += nx * overlap / 2; b.y += ny * overlap / 2; }

关键点在vn > 0这个提前返回:如果两球已经在分离,再施加冲量会让它们「粘」在一起抖动。位置修正那几行是血泪经验,不加的话球会在袋口附近卡成一条线,看起来像穿模。

3.2 库边反弹与袋口判定

库边反弹比球球碰撞简单:检测球心到四边的距离,小于半径就反向速度分量并修正位置。袋口判定则要在库边检测之前做,否则球会先被库边弹回来,永远进不了袋。

function resolveWalls(ball, pockets) { // 先判袋口 for (const [px, py] of pockets) { if (Math.hypot(ball.x - px, ball.y - py) < 18) { ball.active = false; ball.vx = 0; ball.vy = 0; return; } } // 再判库边 if (ball.x - BALL_RADIUS < 0) { ball.x = BALL_RADIUS; ball.vx = -ball.vx * 0.9; } if (ball.x + BALL_RADIUS > TABLE_W) { ball.x = TABLE_W - BALL_RADIUS; ball.vx = -ball.vx * 0.9; } if (ball.y - BALL_RADIUS < 0) { ball.y = BALL_RADIUS; ball.vy = -ball.vy * 0.9; } if (ball.y + BALL_RADIUS > TABLE_H) { ball.y = TABLE_H - BALL_RADIUS; ball.vy = -ball.vy * 0.9; } }

库边恢复系数取 0.9,比球球碰撞略低,模拟库边吸能。袋口半径 18px 比球半径大不少,这是为了容忍高速球穿过袋口时的单帧位移,避免「球明明进了却弹出来」的翻车现场。

3.3 每帧的碰撞检测顺序

一帧内的处理顺序建议是:先更新所有球的位置,再统一做球球碰撞,最后做库边和袋口。球球碰撞要用双重循环遍历所有活跃球对,球数少时性能没问题。注意不要在遍历过程中修改数组长度,进袋只改active标志,不删元素。

function step(balls, pockets) { balls.forEach(b => b.update()); for (let i = 0; i < balls.length; i++) { if (!balls[i].active) continue; for (let j = i + 1; j < balls.length; j++) { if (!balls[j].active) continue; resolveBallCollision(balls[i], balls[j]); } } balls.forEach(b => { if (b.active) resolveWalls(b, pockets); }); }

这个顺序能避免「球先撞库边再撞球」导致的重复修正。如果发现球在袋口附近抖动,优先检查是不是袋口判定被放到了库边之后。

4. 瞄准、力度与击球:把交互补全

4.1 鼠标瞄准线与力度条

瞄准线从白球中心指向鼠标位置,力度由按住鼠标的时长或拖动距离决定。我一般用「按下开始蓄力,松开击球」的方式,力度条画在球桌外侧,避免遮挡球路。瞄准线要做截断,不能穿过球或库边,否则视觉上很乱。

let aiming = false; let power = 0; let aimAngle = 0; canvas.addEventListener('mousedown', e => { aiming = true; power = 0; }); canvas.addEventListener('mousemove', e => { const rect = canvas.getBoundingClientRect(); const mx = e.clientX - rect.left; const my = e.clientY - rect.top; aimAngle = Math.atan2(my - cueBall.y, mx - cueBall.x); }); canvas.addEventListener('mouseup', () => { if (!aiming) return; aiming = false; const speed = power * 0.3; // 力度映射到速度 cueBall.vx = Math.cos(aimAngle) * speed; cueBall.vy = Math.sin(aimAngle) * speed; power = 0; });

power在蓄力期间每帧递增,松开时映射成速度。系数 0.3 是调出来的,太大球会飞,太小打不动。瞄准角度用atan2算,注意 Canvas 的 y 轴向下,角度方向和数学坐标系相反,但击球时用同一套三角函数,不影响结果。

4.2 白球复位与犯规判定

白球进袋后要复位到开球点,并给对手自由球或从原位击打。最小实现里可以先简单复位,不做完整犯规链。复位前要检查开球点是否被其他球占用,被占用就沿中轴往后找空位。

function resetCueBall(balls) { const startX = TABLE_W * 0.25; const startY = TABLE_H / 2; let x = startX; while (balls.some(b => b.active && b.id !== 0 && Math.hypot(b.x - x, b.y - startY) < BALL_RADIUS * 2)) { x -= BALL_RADIUS * 2; } cueBall.x = x; cueBall.y = startY; cueBall.vx = 0; cueBall.vy = 0; cueBall.active = true; }

这个循环最多往后挪几次就能找到空位。如果整条中轴都被占满,说明球堆异常,实际对局中极少出现,可以加一个兜底直接放回原点。

4.3 球号颜色与分组逻辑

8 球玩法里,1-7 号是全色,9-15 号是花色,8 号是黑球。分组判定在第一次合法进球后确定。最小 Demo 可以先把颜色画对,分组逻辑留接口。颜色映射用数组存,渲染时按 id 取色。

const BALL_COLORS = [ '#fff', // 0 白球 '#f5c518', '#1e5aa8', '#d32f2f', '#7b1fa2', '#f57c00', '#2e7d32', '#8d2f1f', '#111', // 8 号黑球 '#f5c518', '#1e5aa8', '#d32f2f', '#7b1fa2', '#f57c00', '#2e7d32', '#8d2f1f' ];

花色球可以在填充色上再画一条白色横带,用ctx.save和ctx.clip限制绘制区域。这一步不影响物理,但能让 Demo 看起来更像成品。

5. 避坑与排查:台球 Demo 最容易翻车的 5 个点

5.1 球在袋口反复抖动进不去

现象是球滚到袋口边缘后左右弹跳,速度衰减到零也不进袋。原因通常是袋口判定半径小于球半径加单帧位移,高速球一帧跨过袋口中心却没触发判定。解决是把袋口判定半径设到 18px 以上,并且在库边检测之前执行。如果还抖,检查位置修正是否把球推回了袋口外。

5.2 两球相撞后速度方向明显不对

现象是正面碰撞后两球都往同一侧偏。原因是只交换了速度大小,没按法线方向投影。必须用连心线单位向量做点积,只交换法线分量。另一个常见错误是忘记判断vn > 0,导致分离中的球被反复施加冲量。

5.3 球速过快时穿过库边或球

现象是高速球直接飞出球桌。原因是单帧位移大于球半径,碰撞检测在下一帧才触发,球已经越界。解决是限制最大速度,或者在每帧内做子步进,把一次大位移拆成多次小位移再检测。最小 Demo 里把速度上限压在 15px/帧以内就能明显缓解。

5.4 白球进袋后复位到其他球里面

现象是白球复位后和别的球重叠,下一帧被弹飞。原因是复位点没做占用检查。解决是按 4.2 的循环沿中轴找空位,找不到就继续往后挪。注意检查时要用BALL_RADIUS * 2作为最小间距,不能只用半径。

5.5 摩擦系数调完后球永远停不下来

现象是球速降到很低但一直漂移。原因是MIN_SPEED设得太小,或者摩擦系数太接近 1。把MIN_SPEED提到 0.05 以上,摩擦系数压到 0.99 以下,基本能解决。如果球在库边附近漂移,还要检查库边反弹是否每次都乘以恢复系数,漏乘会导致能量不衰减。

6. 从能玩到耐玩:旋转、连击与性能验证

把基础碰撞跑通后,下一步值得投入的是加旋转和连击判定。旋转不需要完整刚体动力学,可以用「击球点偏移」近似:鼠标击球时记录击球点相对球心的偏移量,偏移量决定白球碰撞后的分离角和目标球的跟进/缩杆效果。实现上给每颗球加一个spin向量,碰撞后按比例叠加到速度上,再每帧衰减。这个近似模型在网页 Demo 里手感足够,代码量不到 50 行。

连击判定则依赖「第一次合法进球后锁定分组」这条规则。我一般会在进球回调里记录本杆第一个进袋的球号,如果它属于全色或花色,就把玩家分组定下来,后续打进对方球算犯规。犯规链可以先做「白球进袋」和「空杆」两种,自由球用鼠标拖动白球实现。

性能验证方面,22 颗球、每帧 231 对碰撞检测,在普通笔记本上跑 60fps 毫无压力。真正吃性能的是渲染时的阴影和高光,如果每颗球都画渐变,帧率会掉到 40 左右。我的习惯是只在白球和 8 号球上画渐变,其余球用纯色加描边,视觉损失很小,帧率能回到 60。验证方法很简单:在requestAnimationFrame里记录每帧耗时,连续跑 600 帧取平均,超过 16ms 就说明有优化空间。

最后说一个我踩过的坑:早期版本我把所有球的碰撞检测放在渲染之后,结果画面先画了旧位置,再算碰撞,视觉上球会「先穿模再弹开」。后来把物理步进统一放到渲染之前,问题消失。这个顺序问题在快速移动的球上特别明显,慢速球几乎看不出来,所以容易被忽略。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/14 19:47:24

Angular V21升级实战:从V20迁移的踩坑与排查指南

项目排期终于给到了 Angular V21 升级&#xff0c;说实话接到这个任务的第一反应不是兴奋&#xff0c;是心虚。这个系列写到第十八期&#xff0c;眼看着 Angular 从模块化一路走向信号化&#xff0c;每次升级都像一次外科手术&#xff0c;V21 这刀要切在哪里&#xff0c;如果心…

作者头像 李华
网站建设 2026/10/14 19:47:07

概率语言模型基石:链式法则与马尔可夫假设详解

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/14 19:46:31

PHPCMS整站模板部署与安全迁移实战指南

简介&#xff1a;一套以PHPCMS为基础构建的整站前后台模板&#xff0c;适合使用PHPMySQL搭建大型网站或进行二次开发的开发者参考。资源涵盖前台页面与后台管理布局&#xff0c;模板机制将页面样式与程序逻辑分离&#xff0c;便于按实际需求替换HTML/CSS并扩展文章、图片、下载…

作者头像 李华
网站建设 2026/10/14 19:45:51

高职质检员转型品质管理的6大核心能力与实施路径

1. 高职学历质检专员转型品质管理分析的可行性路径我见过太多质检岗位的同事在职业发展上遇到瓶颈&#xff0c;特别是高职学历背景的同行。从重复性检测工作转向需要综合分析的品质管理岗位&#xff0c;确实存在知识结构和思维模式的差异。但根据我辅导过的37位成功转型案例来看…

作者头像 李华
网站建设 2026/10/14 19:45:24

怎么判断文章是不是AI写的?aigc检测原理和论文降AI率方法一次看懂!

怎么判断文章是不是AI写的&#xff1f;aigc检测原理和论文降AI率方法一次看懂&#xff01; 想知道一篇文章是不是AI写的&#xff0c;现在大家都会丢进aigc检测里测一下。可同一篇文章换个平台测&#xff0c;结果就不一样&#xff0c;自己写的论文也可能被判出很高的AI率。 检…

作者头像 李华
网站建设 2026/10/14 19:44:53

Windows 11 环境下 BAT 脚本无法执行的排查与修复指南

前言 在 Windows 11 的日常使用和系统运维中&#xff0c;.bat&#xff08;批处理&#xff09;脚本因其轻量、便捷的特性&#xff0c;依然是自动化任务和环境配置的重要工具。然而&#xff0c;不少用户在 Win11 环境下会遇到 bat 文件无法运行的问题。具体表现通常为&#xff1a…

作者头像 李华