【游戏功能】
隐蔽报错引入:一个很隐蔽的报错——毒雾明明朝右喷,怪站在左边也蚀了血:扇面的张角算的是绝对角度,喷出的方向却是人物面向,两边一错位,扇面画反了方向。扇面雾的全部判定都长在"朝向加张角"这一对搭档上,搭档一散伙判定就指哪打不到哪。这篇把吐雾蚀纹整套写法重写成篇:张口聚雾、扇面铺开、近深远浅、雾散收口,一段一段照抄能跑。
一、效果演示
演示场是雾瘴林。点「吐雾!」:侠客朝怪的方向张口一喷,扇面的绿雾铺开——贴脸站在扇心里蚀二十六,离远了站在扇边上只蚀十二。雾粒一颗颗从口中滚出去,扇面铺开又慢慢散掉。七秒一口,喷的时机挑在怪贴上来的那一步,一口雾就能把它的进攻节奏整个蚀掉。
flowchart TD
A[点吐雾] --> B[张口聚雾]
B --> C[扇面朝怪铺开]
C --> D{怪站多近}
D -- 贴脸 --> E[蚀二十六]
D -- 离远 --> F[蚀十二]
E --> G[雾粒散掉]
F --> G
G --> H[七秒后再喷]
二、底层原理
吐雾系统的机关是"朝向加张角"——扇面的判定是两件事的乘积:喷的起点是口,喷的方向是面向,张角是扇面从面向两边各铺开的宽度。那场反方向的雾就是搭档散伙:张角在算,面向没进算式,扇面画在了背后。正解是判定先读面向再读张角,两样一起算出扇面的真实覆盖。
近深远浅是吐雾的伤害账:贴脸蚀二十六、离远蚀十二,毒雾的浓度在扇尖最浓、扇尾最淡——浓度的梯度跟着距离走,玩家站多近就吃多重,这口雾惩罚的是距离不是命运。雾粒的演出给了扇面生命:十八粒绿雾从口中滚出去、铺开、渐散,扇面的形状不用画线,雾粒自己会长出扇形。幂尔框架这边雾粒收在吐雾的口子里生成,蚀的判定读距离分档,雾散写在每粒的寿命尽头。
这套玩法的代码量不到八十行,难的不是喷雾而是朝向进算式——扇面画在背后,判定再准也是对着空气蚀。
再看近深远浅为什么不做成均一伤害——均一的雾只奖励"喷没喷中",梯度的雾奖励"贴得多近":同样的扇面,敢贴脸的人多蚀一半。梯度伤害把风险和赏金拴在同一个刻度上,吐雾就从消耗技变成了博弈技——多走近一步多蚀一段,被爪子拍到的风险也多一分。这条给一切扇面锥形类机制:梯度是扇面的灵魂。
三、核心代码:完整模块(上·骨架)
-- @file CrestConebreath.lua
-- 吐雾蚀纹 —— 一口毒雾扇面喷 站得近蚀得深
local CrestConebreath = {}
local CONST = {
MIST_N = 18,
NEAR_DMG = 26,
FAR_DMG = 12,
NEAR_LINE = 260,
BREATH_CD = 7,
AUTOINC_BASE = 1204900,
}
local _mists = {}
local _cd = 0
local _autoInc = 0
local function ShowTip(msg)
if msg and msg ~= "" then SL:ShowSystemTips(msg) end
end
local function GenID()
_autoInc = _autoInc + 1
return CONST.AUTOINC_BASE + _autoInc
end
function CrestConebreath.Dmg(dist)
if dist < CONST.NEAR_LINE then
return CONST.NEAR_DMG
end
return CONST.FAR_DMG
end
四、核心代码:完整模块(下·推进与卸载)
-- 吐雾: 雾粒沿面向铺开
function CrestConebreath.Breath(mobDist)
if _cd > 0 then
ShowTip("雾还在嗓子眼——再等" .. math.ceil(_cd) .. "秒")
return
end
_cd = CONST.BREATH_CD
for i = 1, CONST.MIST_N do
local sp = (i / CONST.MIST_N) * 0.5 - 0.25 + 0.08
_mists[#_mists + 1] = {
x = 230, y = 180,
vx = math.cos(sp) * (160 + math.random() * 120),
vy = math.sin(sp) * (60 + math.random() * 50) - 10,
t = 0,
life = 0.6 + math.random() * 0.4,
}
end
ShowTip(mobDist < CONST.NEAR_LINE
and "贴脸蚀" .. CONST.NEAR_DMG
or "离远了蚀" .. CONST.FAR_DMG)
return CrestConebreath.Dmg(mobDist)
end
if not _ticker then
_ticker = SL:Schedule(function(dt)
if _cd > 0 then _cd = _cd - dt end
for i = #_mists, 1, -1 do
local m = _mists[i]
m.t = m.t + dt
m.x = m.x + m.vx * dt
m.y = m.y + m.vy * dt
m.vx = m.vx * 0.985
if m.t >= m.life then table.remove(_mists, i) end
end
end, 0.02)
end
SL:ScheduleOnce(function()
SL:BindDebugButton("吐雾!", function()
CrestConebreath.Breath(SL:GetValue("mobDist") or 0)
end)
ShowTip("技能已加载: 吐雾蚀纹")
end, 1.0)
function CrestConebreath.Unload()
if _ticker then SL:Unschedule(_ticker) end
_mists = {}
end
return CrestConebreath
五、机制问答
问:雾喷反方向的病根是什么?
答:张角在算面向没进算式——扇面的判定是朝向乘张角,搭档散伙就指哪打不到哪。
问:近深远浅为什么不做成均一?
答:均一只奖励喷没喷中——梯度奖励贴得多近,吐雾从消耗技变博弈技。
问:贴脸的二十六冒着什么险?
答:多走近一步多蚀一段,被爪子拍到的风险也多一分——风险和赏金拴在同一刻度。
问:雾粒为什么不用画线画扇面?
答:雾粒自己会长出扇形——十八粒铺开渐散,形状比描边生动。
问:七秒一口管什么?
答:雾的稀缺——喷完即吐的雾是常态毒场,掐着喷的雾才是节奏武器。
六、调参与实战怎么用
参数层面三个量——张角宽度、近远分界、两档伤害。扇角半宽零点二五弧度配十八粒雾:铺得开又不散架。实战里这套朝向乘张角的骨架能搬去龙息、毒瘴喷吐、狮吼音波,全是张口、铺扇、梯度蚀一条线。常见坑两个:面向不进算式扇面画反;伤害均一,扇面没了灵魂。
补充一个设计层面的思考——招式的动作语言决定玩家怎么用招:回旋的招教人卡身位、抡圆的招教人贴距离、横扫的招教人排队列,每种动作自带一门功课。设计招式先想清楚要教玩家哪门功课,动作语言照着功课设计,招式才教得进人。
补充一个维护层面的思考——带轨道和弹道的运动体(回旋的刃、抡摆的锤、弹跳的弹)每个都要有自己的寿命终点:飞回手里、抡完收势、弹完化雾,终点一到就清账。没有终点的运动体是最难缠的幽灵——转了三十圈还在飞、弹了一百次还在弹,查起账来满屏都是它们。
补充一个工程层面的思考——判定窗口的开合要跟着动画的帧走而不是跟着表的读数走:动画播到接触帧判定开启,播过接触帧判定关闭,动画与判定共用同一口时钟。判定窗口和动画两张皮的招式,玩家看着打中了没账、账面有了没打中,全是时钟没对齐。
补充一个演出层面的思考——同一副骨架里认招,靠的是出手前三拍的预告各不相同:索有甩臂、壶有举顶、遁地有蹲身、扇骨有反手抽鞘、穿身有点地、引光有仰头,玩家读的就是这三拍。新招式设计完先自查一遍:把几只演武的预告帧摆在一处,长得一样就回炉。预告的差异比命中的差异更要紧——命中的爽只有半秒,预告的辨识度才是玩家认招的根据,也是每一出演武各有其相的根本。
写完留一句给做扇面系的同学:玩家读的是雾粒也是那对朝向加张角的搭档——梯度跟着距离走,一口雾才喷得出博弈。
全站技术干货持续更新:996 引擎 / Lua 实战帖,语法、参数与示例一篇讲透。进入文章地图 · 查看全部 →
【游戏功能】 隐蔽报错引入:一个很隐蔽的报错——毒雾明明朝右喷,怪站在左边也蚀了血:扇面的张角算的是绝对角度,喷出的方向却是…
【游戏功能】 线上事故引入:早上雷泽炸出个大雷——引雷的圈圈套在怪的脚下,雷柱劈下来却劈在了三步之外的空地上。雷柱的落点读的…
【游戏功能】 隐蔽报错引入:一个很隐蔽的报错——冰弹撞到第三面冰壁,人没弹回来,弹进了墙里。弹跳的账少写了边界反弹的第四面,…
【游戏功能】 单行代码拆解:先看一行代码: 光压着谁,谁挨烧 ——扫堂光的全部判定就在这一行:一道光柱从左往右匀速横扫,光柱…
【游戏功能】 线上事故引入:早上演武场的链锤出了事故——锤抡圆了把自己甩了个趔趄,查下去链长和圈速各写各的,圈还没抡满链已经…
【游戏功能】 先抛一个坑:飞出去的刀,为什么回头还有一刀?回旋刃的妙处就在"去回两账":出手那下是明账,回头那下是暗账——会…