【游戏功能】
日志里躺着一条诡异记录:一次出杆,白球以每秒 4000 像素的速度穿过了台面、三颗彩球和库边,最后消失在坐标系外。物理引擎没报错,碰撞检测全跑了,唯一的解释是那一帧的位移步长太大——球"瞬移"过去了,穿透就这样发生。这是今天讲中式八球表现的由头:台球是所有小游戏里最考验步长纪律的。
一、一条瞄准线的仪式感
台球玩的就是"预判",所以表现层的第一主角不是球,是瞄准线。自动打法的流程是:选目标球、选袋口、算幽灵球点(目标球要进袋,白球必须撞到它身后的那个虚拟接触点)、从白球到幽灵球点拉一条虚线瞄准线、幽灵球位置画一个淡淡的虚线圆。出杆瞬间瞄准线闪一下再消隐。这一套可视化做完,玩家不用懂三角函数也能看懂"这一杆为什么这么打"——瞄准线就是物理引擎的说书人。球滚动时用径向渐变画出高光偏移,球速越快高光拖得越长,进袋瞬间袋口金圈亮一下加一颗小飘分。
完整演示在这,默认自动循环清台,可调力度:
fx-billiard
二、幂尔框架实现:步长切割防穿透
上面那个 4000 像素的事故,解法是把一步拆成多步。幂尔框架里用 SL:Schedule 做固定步长积分,每帧按速度切分子步:
local Billiard = {}
local KEY = { STEP = 0.008, FRICTION = 1.1, R = 10 }
local balls = {}
local function SplitMove(b, dt)
-- 按最大步长切割位移,杜绝高速穿透
local sp = math.sqrt(b.vx * b.vx + b.vy * b.vy)
local n = math.ceil(sp * dt / KEY.STEP)
n = math.max(1, math.min(8, n))
for i = 1, n do
b.x = b.x + b.vx * dt / n
b.y = b.y + b.vy * dt / n
Billiard.CheckWall(b)
Billiard.CheckPocket(b)
end
end
function Billiard.Sim(dt)
local moving = false
for _, b in ipairs(balls) do
if b.vx ~= 0 or b.vy ~= 0 then
SplitMove(b, dt)
b.vx = b.vx * (1 - dt * KEY.FRICTION)
b.vy = b.vy * (1 - dt * KEY.FRICTION)
if math.abs(b.vx) < 4 and math.abs(b.vy) < 4 then
b.vx, b.vy = 0, 0
else
moving = true
end
end
end
Billiard.Collide()
if not moving then
SL:SendEvent("balls_settled") -- 全停后通知瞄准模块
end
end
function Billiard.Collide()
for i = 1, #balls do
for j = i + 1, #balls do
local a, b = balls[i], balls[j]
local dx, dy = b.x - a.x, b.y - a.y
local d2 = dx * dx + dy * dy
if d2 < (KEY.R * 2) * (KEY.R * 2) and d2 > 0.01 then
local d = math.sqrt(d2)
local nx, ny = dx / d, dy / d
-- 沿法线交换分离速度
local rel = (a.vx - b.vx) * nx + (a.vy - b.vy) * ny
if rel > 0 then
a.vx = a.vx - rel * nx
a.vy = a.vy - rel * ny
b.vx = b.vx + rel * nx
b.vy = b.vy + rel * ny
SL:PlayFxAt("hit_spark", (a.x + b.x) / 2, (a.y + b.y) / 2)
end
end
end
end
end
return Billiard
球与球的碰撞只做一次法线分离就够了,台球不需要恢复系数的花活,动量守恒的观感已经足够真。
三、瞄准数值与清台节奏
自动打法的目标选择有个隐藏排序问题:最近的可进袋组合优先,但要排除"白球先落袋"的线路——瞄准方向的反向延长线离袋口太近的方案要跳过,不然一杆把自己打进去,观感极其尴尬。力度数值建议做成档位而非连续值,五档以内玩家才有"手感"概念,连续力度条反而没人看。
清台节奏也要设计:三球全进后不要立刻重开,让最后一颗进袋的音效和袋口金圈先播完,停 0.8 秒,飘一行"清台"再淡出重置——仪式感的成本几乎为零,玩家对"打完一局"的确认感却完全不同。碰撞火花别过量,两球相撞一颗小火花足够,五颗就是火灾现场。
全站技术干货持续更新:996 引擎 / Lua 实战帖,语法、参数与示例一篇讲透。进入文章地图 · 查看全部 →
【游戏功能】 heat = heat + 1; if heat = 3 then rest = 1.6 ——两行管刀温的代码…
【游戏功能】 兄弟俩打一只狼,哥哥攒了三层劲自己放,一层加二十五;弟弟站在旁边干瞪眼——直到有人把劲递了过去:同样的三层,过…
【游戏功能】 有篇纠错单写得像小说:同一只狼,有人打它半小时血线稳如老狗,有人三分钟放倒——排查到最后发现,会打的那个从头到…
【游戏功能】 追着你满场跑的狼怪,为什么绕过一根石柱就懵在原地?脱影的答案藏在柱子背后:视线被石柱掐断一秒二,它脑子里"你"…
【游戏功能】 巡查群里有人喊邪门:同一套连招,昨天刀刀见血,今天刀刀被挡——查录像看到第三遍才看懂:狼怪学会了。他的出刀间隔…
【游戏功能】 if hand == last then acid = acid + 1 ——一行数手的判定,攒出了换手全部的…