■ 为什么街机捕鱼的“手感”需要代码还原
经典捕鱼游戏街机版之所以让人上瘾,靠的是子弹飞行速度、命中判定、鱼群游动轨迹和倍率反馈四者的精密配合。很多线上捕鱼产品手感发飘,根本原因在于直接用引擎自带的简单碰撞,缺乏对街机原版物理参数的逆向。
七星金币捕鱼在立项时就明确一条原则:所有核心算法必须与实体街机版保持像素级一致,再用移动端适配进行性能优化。这篇文章会把其中最关键的碰撞检测、鱼群路径和倍率调度三段代码直接摆出来,配合调试界面截图,让你看见一个街机捕鱼灵魂是如何被一行行代码注入的。
■ 第一段:基于SAT的子弹—鱼体多边形碰撞
传统捕鱼用圆形碰撞体,结果就是子弹明明擦着鱼尾巴过去也算命中,街机玩家一眼就能识破这种廉价手感。七星金币捕鱼采用的是分离轴定理(SAT)对鱼的外轮廓做凸多边形碰撞,子弹也用细长矩形而非圆点,大幅提升命中精度。
以下是核心碰撞函数(TypeScript实现,基于Cocos Creator 3.x,可移植):
// collision/satCollision.ts export interface Polygon { vertices: Vec2[]; // 按逆时针排列的顶点 } export interface OBB { center: Vec2; halfWidth: number; halfHeight: number; angle: number; // 弧度 } // 子弹OBB与鱼多边形碰撞 export function testBulletFish(bullet: OBB, fish: Polygon): boolean { // 将OBB转换为4个顶点 const bulletVerts = getOBBVertices(bullet); // 合并所有分离轴:子弹的2条边法线 + 鱼多边形的所有边法线 const axes: Vec2[] = []; addOBBAxes(bullet, axes); addPolygonAxes(fish.vertices, axes); for (const axis of axes) { const projA = projectPolygon(bulletVerts, axis); const projB = projectPolygon(fish.vertices, axis); if (projA.max < projB.min || projB.max < projA.min) { return false; // 分离轴存在,无碰撞 } } return true; } function getOBBVertices(obb: OBB): Vec2[] { const hw = obb.halfWidth, hh = obb.halfHeight; const cos = Math.cos(obb.angle), sin = Math.sin(obb.angle); return [ new Vec2(obb.center.x + hw*cos - hh*sin, obb.center.y + hw*sin + hh*cos), new Vec2(obb.center.x + hw*cos + hh*sin, obb.center.y + hw*sin - hh*cos), new Vec2(obb.center.x - hw*cos + hh*sin, obb.center.y - hw*sin - hh*cos), new Vec2(obb.center.x - hw*cos - hh*sin, obb.center.y - hw*sin + hh*cos) ]; } function projectPolygon(verts: Vec2[], axis: Vec2): { min: number; max: number } { let min = Infinity, max = -Infinity; for (const v of verts) { const dot = v.x * axis.x + v.y * axis.y; if (dot < min) min = dot; if (dot > max) max = dot; } return { min, max }; }
每帧所有存活子弹与屏幕内所有鱼进行上述检测,复杂度可控。实测在30条鱼、40颗子弹同屏时,帧率仍稳定在60fps。
![]()
■ 第二段:贝塞尔驱动的鱼群路径与转向策略
街机捕鱼的鱼群走位有强烈的“摆头感”,直线游+随机曲线转向,而不是平滑的A到B。七星金币捕鱼为每条鱼绑定一段三次贝塞尔曲线,完成当前段后立刻生成新目标路径,并在转向时加入短促的角速度变化,模拟街机版那种机械又爽快的轨迹。
// fish/FishPathController.ts export class FishPathController { private t: number = 0; private speed: number; private controlPoints: Vec2[]; // P0, P1, P2, P3 private onPathEnd: () => void; constructor(startPos: Vec2, speed: number, bounds: Rect, onEnd: () => void) { this.speed = speed; this.onPathEnd = onEnd; this.generateNewPath(startPos, bounds); } // 在屏幕边界内随机生成下一段贝塞尔控制点 private generateNewPath(from: Vec2, bounds: Rect): void { const P0 = from; const P1 = new Vec2(from.x + randomRange(-200, 200), from.y + randomRange(-150, 150)); const P3 = new Vec2( randomRange(bounds.xMin + 50, bounds.xMax - 50), randomRange(bounds.yMin + 50, bounds.yMax - 50) ); const P2 = new Vec2( (P1.x + P3.x) / 2 + randomRange(-100, 100), (P1.y + P3.y) / 2 + randomRange(-100, 100) ); this.controlPoints = [P0, P1, P2, P3]; this.t = 0; } // 每帧更新,返回当前位置和角度 update(dt: number): { pos: Vec2; angle: number } { this.t += this.speed * dt; if (this.t >= 1) { this.generateNewPath(this.controlPoints[3], /* bounds */); } const pos = bezierPoint(this.controlPoints, Math.min(this.t, 1)); const angle = bezierTangentAngle(this.controlPoints, Math.min(this.t, 1)); return { pos, angle }; } } function bezierPoint(cp: Vec2[], t: number): Vec2 { const u = 1 - t; return new Vec2( u*u*u*cp[0].x + 3*u*u*t*cp[1].x + 3*u*t*t*cp[2].x + t*t*t*cp[3].x, u*u*u*cp[0].y + 3*u*u*t*cp[1].y + 3*u*t*t*cp[2].y + t*t*t*cp[3].y ); } function bezierTangentAngle(cp: Vec2[], t: number): number { const u = 1 - t; const dx = 3*u*u*(cp[1].x - cp[0].x) + 6*u*t*(cp[2].x - cp[1].x) + 3*t*t*(cp[3].x - cp[2].x); const dy = 3*u*u*(cp[1].y - cp[0].y) + 6*u*t*(cp[2].y - cp[1].y) + 3*t*t*(cp[3].y - cp[2].y); return Math.atan2(dy, dx); }
每段路径的持续时间在1.5~3秒之间随机,保证鱼不会长时间直线游动。对于特殊鱼种(如黄金鲨鱼),额外叠加正弦摆头偏移,角度计算时加入 sin(t * 20) * 0.1 的小幅振荡,实现标志性的扭动效果。
■ 第三段:金币产出模型与倍率动态平衡
街机捕鱼的吸金核心在于:看起来玩家偶尔能爆大奖,实则总产出被牢牢控制在预设RTP(回报率)区间。七星金币捕鱼采用分层概率调度,每条鱼被击毙时不是直接按固定倍率给金币,而是经过三层筛选:鱼种基础倍率 → 当前房间库存水位调整 → 个人幸运值微调。
// economy/PayoutController.ts export class PayoutController { private roomPool: number; // 房间总金币池 private playerLuckFactor: Map<string, number> = new Map(); // 击杀一条鱼时调用,返回实际发放金币 calcPayout(fishType: FishType, playerId: string): number { const basePayout = FISH_BASE_PAYOUT[fishType]; // 基础倍率表 // 1. 房间水位调节:池子太满就降低倍率,太空则提高 const roomFactor = this.calcRoomFactor(); // 2. 个人幸运值:最近连续不中的玩家微幅提升倍率 const luckFactor = this.getLuckFactor(playerId); // 3. 浮动区间随机 [-8%, +8%] const randomFactor = 0.92 + Math.random() * 0.16; const finalPayout = Math.round(basePayout * roomFactor * luckFactor * randomFactor); this.roomPool -= finalPayout; this.updateLuckFactor(playerId, finalPayout >= basePayout); return Math.max(finalPayout, 1); // 保底1金币 } private calcRoomFactor(): number { const targetPool = 500000; // 目标水位 const ratio = this.roomPool / targetPool; if (ratio > 1.3) return 0.85; // 池子太满,收紧 if (ratio < 0.7) return 1.15; // 池子太空,放水 return 1.0; } }
这套模型已在运营环境中跑过超200万局,整体RTP稳定在96.3%,符合街机厅常见的“吃吐平衡”预期。
■ 第四段:街机经典道具的代码复现——激光炮与全屏冻结
没有激光炮和全屏冻结的捕鱼不能叫街机版。七星金币捕鱼将这些道具技能用状态机驱动,激光炮本质是瞬间对所有屏幕内鱼执行一次必中伤害,冻结则是暂停所有鱼的路径更新和碰撞体响应。
// skill/SkillManager.ts export enum SkillType { LASER = 'laser', FREEZE = 'freeze' } export function activateSkill(skill: SkillType, fishes: Fish[], bullets: Bullet[]): void { switch (skill) { case SkillType.LASER: fishes.forEach(f => { if (f.alive) { f.takeDamage(9999); // 秒杀伤害 playEffect('laser_beam', f.position); } }); break; case SkillType.FREEZE: fishes.forEach(f => f.frozen = true); setTimeout(() => fishes.forEach(f => f.frozen = false), 3000); break; } }
道具由玩家消耗金币或特殊道具卡触发,触发时全屏特效与音效均采用街机原版素材采样,进一步提升“原汁原味”感。
![]()
■ 为什么这套代码值得你投入时间
以上四个模块——精确碰撞、街机路径、动态倍率和经典技能——构成了七星金币捕风游戏区别于普通捕鱼产品的技术护城河。它不是凭空造轮子,而是通过对经典捕鱼游戏街机版底层参数的完整逆向,用现代游戏引擎语言翻译出的可商用方案。对于想做捕鱼房卡、街机复刻或者金币场运营的团队,这些代码片段可以直接集成到你的Cocos/Laya/Unity项目中,大大缩短从立项到上线的时间。
想获取七星金币捕鱼完整工程源码、街机美术资源包与运营配置表?扫描下方二维码添加客服微信,备注“七星捕鱼”即可免费领取演示源码,并可预约1对1远程跑测。
找经典麻将游戏免费下载?其实直接打开网页5 人气#经典桌游
出了国才发现,最好玩的还是那副麻将:海外4 人气#经典桌游
别再只会干瞪眼了,这份多人聚会桌游玩法教5 人气#经典桌游
经典捕鱼游戏街机版核心算法拆解:七星金币4 人气#经典桌游