CHUAN2 DEV ENGINE
996 正版授权研发中心 · 360 授权合作教学中心 · 抖音传奇直播合作授权 · 快手推广运营商授权
OFFICIAL LICENSED ACADEMY 查验官方授权证书 →
// 威海旷世互娱教学基地 · 技术文章
进阶实战游戏功能996引擎

附身诀:魂进它身,借它的爪打它的人

2026-10-01 04:09 作者:996 技术组 996引擎Lua教程传奇脚本进阶实战游戏功能996引擎

【游戏功能】

return base * FIRE_MULT——不对,这一篇没有乘法:附身诀的全部代码里找不到一行伤害放大,它的伤害来自"换人出刀"。魂进了它的身体,它的爪挥出去的就是你的数字——这不是增益,是代打。这篇的核心代码是对着引擎技能脚本的全规格写的:配置表、视觉构件、状态机、定时器、调试按钮,一个模块该有的骨头一根不少。

一、效果演示:一颗魂,两种爪

演示场有左右两只怪。点「附身!」,玩家的魂脱体而出,拖着绿色的残影弧线飞进左边那只怪的身体——你的本体原地蒙灰半透明,左怪周身腾起绿雾、头顶挂出"附身 七秒"。从这一帧起,左怪转头去咬右边那只兄弟,一口十六,金色数字——那是你的伤害从它的爪子里出来。旁边还有一颗「借它的重击」按钮:按下去左怪抡出三十点的大抡锤,震屏闪光一应俱全——但重击落地的瞬间它挣脱附身,魂被弹回来,它头顶亮出"反噬",一秒里它缓过神来就要找你算账。不借重击就安安静静打满七秒,魂自己归位。两条路都走一遍,重击的那三十点值不值提前断,就是这道题的全部。

demo
 fx-ensoul

二、底层原理:一次魂体分离加一个代打出口

模块的状态机只有三段:魂在飞、魂在身、魂归位。飞段做插值——魂的位置从本体坐标向宿主坐标匀速推进,每一帧掉一枚残影;附身段做两件事——宿主的目标表被改写成最近的同族、宿主的攻击入口换用玩家的伤害系数;归位段做清算——宿主挂一秒反噬硬直、仇恨重置回施法者。全部机关里最要紧的设计是"重击即挣脱":借它的大招要付提前解约的代价,这个代价让附身从免费代打变成一道算术题——它的重击三十点、剩余附身时间还够三口十六点,四十八对三十,哪边划算,玩家自己按计算器。

三、核心代码:完整模块(上·骨架)

lua
--------------------------------------------------------------------------------
-- @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 是视觉构件的标准写法:创建节点、挂层级、画线、动作序列、自毁回调,五步走完一个构件,后面所有的特效都长这个样子。

四、核心代码:完整模块(下·定时器与状态机)

lua
--------------------------------------------------------------------------------
-- 魂体对象与状态机
--------------------------------------------------------------------------------
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 技术组基于 996 引擎官方知识库与浮生梦老师课程体系整理。团队长期从事传奇类引擎 Lua 后端逻辑、客户端界面与商业版本交付,内容以官方知识库与真实项目为出处,按版本持续修订。

← 返回文章地图返回研学路径

幂尔框架 · 实战干货 · 接口调用

LATEST ARTICLES

全站技术干货持续更新:996 引擎 / Lua 实战帖,语法、参数与示例一篇讲透。进入文章地图 · 查看全部 →

进阶实战游戏功能

剑环:三柄飞剑绕着转,贴身就挨削

【游戏功能】 安全巡检扫出一笔可疑的挂账:玩家的"周围单位结算"列表里,同一只怪每零点八秒出现一次,可战斗记录里玩家根本没出…

2026-10-01 05:08 996 技术组
进阶实战游戏功能

困灵阵:画个圈关住它,出圈的腿会疼

【游戏功能】 困灵阵的全机制就一句:阵落在它脚下,八秒里它就是圈中之物——敢跨出圈沿,一步吃十五还被弹回去,你在圈外安安稳稳…

2026-10-01 05:08 996 技术组
进阶实战游戏功能

火油阵:先泼油再点灯,两步烧一条线

【游戏功能】 先泼油还是先点火?这道题答错的代价是一地白烧的火——灯点在空地上,怪还没踩上来,四秒的火一点没沾着它的毛。火油…

2026-10-01 05:08 996 技术组
进阶实战游戏功能

冰壁垒:一堵墙横在地上,谁也过不去

【游戏功能】 寻路组收到一笔诡异的工单:怪物巡逻到某个坐标段必卡三秒,像踩了看不见的门槛——排查两小时,发现那段路上有一个上…

2026-10-01 05:08 996 技术组
进阶实战游戏功能

双桩雷阵:两桩立起来,桩间就是雷区

【游戏功能】 local a, b = math.min(x1, x2), math.max(x1, x2) ——一行把两根…

2026-10-01 05:08 996 技术组
进阶实战游戏功能

镇魂碑:立碑镇住它,大招读不完

【游戏功能】 测试组报过一口邪门的单:头目的读条明明被打断过,技能照样出了膛——查了半天不是打断失效,是打断只"清了"不"冻…

2026-10-01 05:08 996 技术组