资讯动态

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

发布时间:2026/10/14 19:47:46 来源:尧图企业网站定制
简介这份网页版台球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 就说明有优化空间。最后说一个我踩过的坑早期版本我把所有球的碰撞检测放在渲染之后结果画面先画了旧位置再算碰撞视觉上球会「先穿模再弹开」。后来把物理步进统一放到渲染之前问题消失。这个顺序问题在快速移动的球上特别明显慢速球几乎看不出来所以容易被忽略。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑