【游戏功能】
return base * FIRE_MULT——不对,这一篇没有乘法:附身诀的全部代码里找不到一行伤害放大,它的伤害来自"换人出刀"。魂进了它的身体,它的爪挥出去的就是你的数字——这不是增益,是代打。这篇的核心代码是对着引擎技能脚本的全规格写的:配置表、视觉构件、状态机、定时器、调试按钮,一个模块该有的骨头一根不少。
一、效果演示:一颗魂,两种爪
演示场有左右两只怪。点「附身!」,玩家的魂脱体而出,拖着绿色的残影弧线飞进左边那只怪的身体——你的本体原地蒙灰半透明,左怪周身腾起绿雾、头顶挂出"附身 七秒"。从这一帧起,左怪转头去咬右边那只兄弟,一口十六,金色数字——那是你的伤害从它的爪子里出来。旁边还有一颗「借它的重击」按钮:按下去左怪抡出三十点的大抡锤,震屏闪光一应俱全——但重击落地的瞬间它挣脱附身,魂被弹回来,它头顶亮出"反噬",一秒里它缓过神来就要找你算账。不借重击就安安静静打满七秒,魂自己归位。两条路都走一遍,重击的那三十点值不值提前断,就是这道题的全部。
fx-ensoul
二、底层原理:一次魂体分离加一个代打出口
模块的状态机只有三段:魂在飞、魂在身、魂归位。飞段做插值——魂的位置从本体坐标向宿主坐标匀速推进,每一帧掉一枚残影;附身段做两件事——宿主的目标表被改写成最近的同族、宿主的攻击入口换用玩家的伤害系数;归位段做清算——宿主挂一秒反噬硬直、仇恨重置回施法者。全部机关里最要紧的设计是"重击即挣脱":借它的大招要付提前解约的代价,这个代价让附身从免费代打变成一道算术题——它的重击三十点、剩余附身时间还够三口十六点,四十八对三十,哪边划算,玩家自己按计算器。
三、核心代码:完整模块(上·骨架)
--------------------------------------------------------------------------------
-- @file Ensoul.lua
-- @brief 附身诀 —— 魂进它身,借它的爪打它的人
-- 客户端表现模块:配置表 / 视觉构件 / 魂体状态机 / 定时器
--------------------------------------------------------------------------------
local Ensoul = {}
--------------------------------------------------------------------------------
-- 配置与常量
--------------------------------------------------------------------------------
local CONST = {
POSSESS_LIFE = 7.0, -- 附身时长 (秒)
POSSESS_CD = 10.0, -- 施放冷却 (秒)
SOUL_FLY = 0.45, -- 魂飞行时长 (秒)
CLAW_INTERVAL = 1.2, -- 代打间隔 (秒)
CLAW_DMG = 16, -- 代打单口
HEAVY_DMG = 30, -- 借来的重击
RECOIL_STUN = 1.0, -- 反噬硬直 (秒)
-- 颜色
COLOR_SOUL = "#3fb950", -- 魂绿
COLOR_DEEP = "#1f6f3f", -- 深绿
COLOR_GHOST = "#7ee2a8", -- 残魂青
-- 层级
ZORDER_GROUND = 118,
ZORDER_SOUL = 134,
ZORDER_EFFECT = 135,
AUTOINC_BASE = 920000,
}
--------------------------------------------------------------------------------
-- 模块内部状态
--------------------------------------------------------------------------------
local _soul = nil -- 魂体对象 (同一时刻至多一颗)
local _scheduler = nil -- 定时器句柄
local _cdLeft = 0 -- 冷却余量
local _autoInc = 0
local m_sin = math.sin
local m_cos = math.cos
local m_rand = math.random
--------------------------------------------------------------------------------
-- 辅助函数
--------------------------------------------------------------------------------
local function GenID(prefix)
_autoInc = _autoInc + 1
return string.format("%s_%d", prefix or "ESL", CONST.AUTOINC_BASE + _autoInc)
end
local function GetActorPos(actorID)
if not actorID then return nil end
local x = SL:GetValue("ACTOR_POSITION_X", actorID)
local y = SL:GetValue("ACTOR_POSITION_Y", actorID)
if not x or not y then return nil end
return { x = x, y = y }
end
local function IsActorAlive(actorID)
local hp = SL:GetValue("ACTOR_HP", actorID)
return hp and hp > 0
end
local function SafeRemove(node)
if node and not GUI:Widget_IsNull(node) then
GUI:removeFromParent(node)
end
end
local function ShowTip(msg)
if msg and msg ~= "" then SL:ShowSystemTips(msg) end
end
---一次性特效播放
local function PlayEffectOnce(x, y, effID, scale, color)
local eff = GUI:Effect_Create(GUI:Attach_SceneF(), GenID("Eff"), x, y,
3, effID, 0, 0, 0, 1.0)
if eff then
if scale then GUI:setScale(eff, scale) end
if color then GUI:setColor(eff, color) end
GUI:setLocalZOrder(eff, CONST.ZORDER_EFFECT)
GUI:Effect_addOnCompleteEvent(eff, function() SafeRemove(eff) end)
end
return eff
end
---魂体拖尾粒子
local function CreateSoulTrail(x, y)
local node = GUI:Layout_Create(GUI:Attach_SceneF(), GenID("Trail"), x, y, 0, 0, false)
GUI:setLocalZOrder(node, CONST.ZORDER_SOUL)
for i = 1, 6 do
local a = i / 6 * 6.28
GUI:DrawLine_Create(node, string.format("t_%d", i), 0, 0, 2,
m_cos(a) * 6, m_sin(a) * 10, m_cos(a) * 14, m_sin(a) * 22,
CONST.COLOR_GHOST, 170)
end
GUI:runAction(node, GUI:ActionSequence(
GUI:ActionFadeOut(0.30),
GUI:CallFunc(function() SafeRemove(node) end)
))
end
骨架这一半先立三根柱子。CONST 表把所有会调的数全部钉在文件头——时长、冷却、伤害、配色、层级,改参数只进这一张表,出了表头的数字一律不给过。_soul 是单例字段而不是表——附身这个机制同一时刻全局只允许一颗魂,用表接多颗的版本迟早写出魂叠魂的事故。CreateSoulTrail 是视觉构件的标准写法:创建节点、挂层级、画线、动作序列、自毁回调,五步走完一个构件,后面所有的特效都长这个样子。
四、核心代码:完整模块(下·定时器与状态机)
--------------------------------------------------------------------------------
-- 魂体对象与状态机
--------------------------------------------------------------------------------
local function Soul_New(ownerID, hostID)
return {
ownerID = ownerID, hostID = hostID,
phase = "fly", -- fly / possess
flyT = CONST.SOUL_FLY,
life = CONST.POSSESS_LIFE,
clawT = CONST.CLAW_INTERVAL,
}
end
local function Soul_DoClaw(s)
local tp = GetActorPos(s.targetID)
if not tp then return end
PlayEffectOnce(tp.x, tp.y, 211, 0.9, CONST.COLOR_SOUL)
end
local function Soul_Expire(s, fierce)
SL:TipAbove(s.hostID, fierce and "反噬" or "魂归位")
SL:SetActorStun(s.hostID, fierce and CONST.RECOIL_STUN or 0)
SL:ResetHateTo(s.hostID, s.ownerID)
Ensoul.Cancel()
end
local function Soul_Update(s, dt)
local mp = GetActorPos(s.ownerID)
local hp = GetActorPos(s.hostID)
if not mp or not hp or not IsActorAlive(s.hostID) then
Soul_Expire(s, false)
return
end
if s.phase == "fly" then
s.flyT = s.flyT - dt
local k = 1 - math.max(0, s.flyT) / CONST.SOUL_FLY
CreateSoulTrail(mp.x + (hp.x - mp.x) * k, mp.y - 60 - m_sin(k * 3.14) * 50)
if s.flyT <= 0 then
s.phase = "possess"
SL:TipAbove(s.hostID, "附身!")
end
elseif s.phase == "possess" then
s.life = s.life - dt
s.clawT = s.clawT - dt
if s.clawT <= 0 then
s.clawT = CONST.CLAW_INTERVAL
Soul_DoClaw(s)
end
if s.life <= 0 then Soul_Expire(s, false) end
end
end
--------------------------------------------------------------------------------
-- 公共接口
--------------------------------------------------------------------------------
function Ensoul.Cast(ownerID, hostID)
if _soul or _cdLeft > 0 then return end
if not IsActorAlive(hostID) then return end
_soul = Soul_New(ownerID, hostID)
_cdLeft = CONST.POSSESS_CD
ShowTip("附身诀已施放")
end
function Ensoul.CastHeavy()
if not _soul or _soul.phase ~= "possess" then return end
SL:HeavyBlow(_soul.hostID, CONST.HEAVY_DMG)
Soul_Expire(_soul, true)
end
function Ensoul.Cancel()
_soul = nil
end
function Ensoul.Update(dt)
_cdLeft = math.max(0, _cdLeft - dt)
if _soul then Soul_Update(_soul, dt) end
end
--------------------------------------------------------------------------------
-- 初始化 & 调试
--------------------------------------------------------------------------------
if not _scheduler then
_scheduler = SL:Schedule(function(dt)
Ensoul.Update(dt)
end, 0.02)
end
SL:ScheduleOnce(function()
ShowTip("技能已加载: 附身诀 (调试按钮触发)")
end, 1.0)
SL:RegisterLUAEvent(LUA_EVENT_ACTOR_OUT_OF_VIEW, "ESL_OutView", function(data)
if _soul and data and (data.id == _soul.hostID or data.id == _soul.ownerID) then
Ensoul.Cancel()
end
end)
return Ensoul
下半场全靠那条定时器活着:SL:Schedule 挂两百分之一秒的节拍,每拍推进冷却和魂体状态机——模块的一切时间感都从这条节拍里来,别处不许再开第二个计时器,双节拍的模块早晚打架。Soul_Update 的两个分段各自管自己的寿命:飞段只做插值和掉尾,附身段只做拍数和到期——归零的那一刻全部汇到 Soul_Expire 一个出口清算,反噬、归位、解约共用一条收尾,账就永远不会两处结。最底下那条 RegisterLUAEvent 是保险丝:宿主或本体走出视野,魂就地注销——不注销的魂会追着一个已经不存在的坐标飞到天荒地老。
五、调参指南与商业服怎么用
调参先看三对数。附身七秒对代打间隔一点二秒是六口——六口十六是九十六点的代打产值,把它和玩家自己十秒的产值对表,代打不能超过本体,超过了附身就从借刀变成了换人。重击三十对挣脱代价是这套机制的灵魂数字:重击必须大于剩余时间里的代打期望,玩家才有"借不借"的纠结;借了必赚的重击是白送。反噬一秒是礼貌——让怪醒来先愣一下,玩家的撤退窗口全在这愣里。附身期间本体是无敌的灰影还是可被打的真身,要早点定:前者鼓励魂进魂出的换位打法,后者让附身变成豪赌,两个方向都能成立,就是不能含糊。常见坑三个:宿主死亡时魂没有就地注销,鬼魂追着尸体继续飞;重击按钮没做禁用态,附身外也能按出三十点;两只玩家同时附身同一只怪,单例字段救了场,用表接的版本魂叠魂。
商业服里附身诀最顺的落点是"高配体验":平民玩家附身一只能打的大怪,战力瞬间上探一个档次,体验期结束魂归位,落差就是充值理由。行会攻城里它是夺旗奇兵——附身守方自己的兽混进去,阵型的笑话全是它讲的。变体做成宿主品级:附身野怪七秒、附身精英十二秒、附身头目要竞价,一条机制三种标价。这一篇的演示和整套模块照抄能跑,改的就是 CONST 那张表。
写完留一句给做附身系的同学:魂体的全部尊严在"归位"那一刻——反噬、清算、仇恨回归一条出口走完,玩家的下一次附身才会安心。
全站技术干货持续更新:996 引擎 / Lua 实战帖,语法、参数与示例一篇讲透。进入文章地图 · 查看全部 →
【游戏功能】 安全巡检扫出一笔可疑的挂账:玩家的"周围单位结算"列表里,同一只怪每零点八秒出现一次,可战斗记录里玩家根本没出…
【游戏功能】 困灵阵的全机制就一句:阵落在它脚下,八秒里它就是圈中之物——敢跨出圈沿,一步吃十五还被弹回去,你在圈外安安稳稳…
【游戏功能】 先泼油还是先点火?这道题答错的代价是一地白烧的火——灯点在空地上,怪还没踩上来,四秒的火一点没沾着它的毛。火油…
【游戏功能】 寻路组收到一笔诡异的工单:怪物巡逻到某个坐标段必卡三秒,像踩了看不见的门槛——排查两小时,发现那段路上有一个上…
【游戏功能】 local a, b = math.min(x1, x2), math.max(x1, x2) ——一行把两根…
【游戏功能】 测试组报过一口邪门的单:头目的读条明明被打断过,技能照样出了膛——查了半天不是打断失效,是打断只"清了"不"冻…