AttrMgr 属性管理器(传奇MMO Lua服务端)

定位:统一属性汇总、分层计算、属性变更分发、属性快照 分层原则(由底层到顶层,叠加计算)

基础属性 → 装备属性 → Buff属性 → 称号/勋章/坐骑 → 百分比加成 → 最终属性 区分【固定值add】和【百分比mul】,百分比是基于前面所有属性之和,不是原始基础

1. 常量定义(属性ID枚举)

ini 复制代码
-- attr_def.lua
ATTR = {
    HP = 1,         -- 生命
    MP = 2,         -- 魔法
    ATTACK = 3,     -- 攻击
    DEFENSE = 4,    -- 防御
    MAGIC = 5,      -- 魔法
    MAGIC_DEF = 6,  -- 魔防
    HIT = 7,        -- 命中
    DODGE = 8,      -- 闪避
    CRIT = 9,       -- 暴击
    CRIT_DAM = 10,  -- 暴击伤害
    MOVE_SPEED = 11,-- 移速
}

2. 错误码

ini 复制代码
ERR.ATTR_INVALID = 13001

3. Proto 协议(属性同步)

ini 复制代码
message AttrEntry {
    int32 attr_id = 1;
    int64 val = 2;
}
message SCAttrSync {
    uint64 obj_uid = 1;
    repeated AttrEntry attrs = 2;
}

4. AttrMgr 完整源码

lua 复制代码
local AttrMgr = {}
AttrMgr.__index = AttrMgr

-- 分层key
AttrMgr.LAYER = {
    BASE = 1,       -- 角色基础(等级、转生)
    EQUIP = 2,      -- 装备、强化、洗练
    BUFF = 3,       -- Buff
    TITLE = 4,      -- 称号
    MOUNT = 5,      -- 坐骑
    OTHER = 6,      -- 其他活动、勋章
    PERCENT = 99,   -- 全局百分比层
}

function AttrMgr:New()
    local obj = setmetatable({}, self)
    obj.cache = {}      -- obj_uid -> 最终属性缓存
    obj.layer_data = {} -- obj_uid -> layer -> {attr_id=val}
    obj.dirty = {}      -- 脏标记,是否需要重算
    return obj
end

-- 设置某一层属性
-- @param obj_uid 实体uid(玩家/怪物)
-- @param layer 分层
-- @param attr_id 属性id
-- @param val 增加值(add值,百分比单独存)
function AttrMgr:SetLayerAttr(obj_uid, layer, attr_id, val)
    if not self.layer_data[obj_uid] then
        self.layer_data[obj_uid] = {}
    end
    if not self.layer_data[obj_uid][layer] then
        self.layer_data[obj_uid][layer] = {}
    end
    local old = self.layer_data[obj_uid][layer][attr_id] or 0
    if old == val then
        return
    end
    self.layer_data[obj_uid][layer][attr_id] = val
    self.dirty[obj_uid] = true
end

-- 清除一层全部属性(buff过期、脱装备时调用)
function AttrMgr:ClearLayer(obj_uid, layer)
    if not self.layer_data[obj_uid] or not self.layer_data[obj_uid][layer] then
        return
    end
    self.layer_data[obj_uid][layer] = nil
    self.dirty[obj_uid] = true
end

-- 核心:重新计算全部属性
function AttrMgr:Recalc(obj_uid)
    if not self.dirty[obj_uid] then
        return self.cache[obj_uid]
    end
    local layers = self.layer_data[obj_uid]
    if not layers then
        self.cache[obj_uid] = {}
        self.dirty[obj_uid] = nil
        return {}
    end

    local total = {}
    local percent = {}
    -- 先累加所有固定层
    for layer, attr_map in pairs(layers) do
        if layer ~= AttrMgr.LAYER.PERCENT then
            for aid, v in pairs(attr_map) do
                total[aid] = (total[aid] or 0) + v
            end
        end
    end
    -- 百分比叠加
    local pct_map = layers[AttrMgr.LAYER.PERCENT] or {}
    for aid, pct in pairs(pct_map) do
        local base = total[aid] or 0
        total[aid] = math.floor(base * (1 + pct / 10000)) -- 万分比 10000=100%
    end

    -- 缓存结果
    self.cache[obj_uid] = total
    self.dirty[obj_uid] = nil
    return total
end

-- 对外接口:重新计算玩家属性 + 发包 + 抛事件
function AttrMgr:RecalcPlayerAttr(player)
    local obj_uid = player.uid
    local old_attr = self.cache[obj_uid] or {}
    local new_attr = self:Recalc(obj_uid)

    -- 对比差异,只同步变化属性
    local diff = {}
    for aid, v in pairs(new_attr) do
        if (old_attr[aid] or 0) ~= v then
            table.insert(diff, {attr_id = aid, val = v})
        end
    end
    for aid, v in pairs(old_attr) do
        if new_attr[aid] == nil then
            table.insert(diff, {attr_id = aid, val = 0})
        end
    end

    if #diff > 0 then
        -- 发给自己
        player:SendMsg(Proto.SCAttrSync, {obj_uid = obj_uid, attrs = diff})
        -- AOI广播给周围玩家
        AoiMgr:BroadCastWatcher(obj_uid, Proto.SCAttrSync, {obj_uid = obj_uid, attrs = diff})
        -- 抛属性变更事件
        EventMgr:Fire(EVENT.ATTR_CHANGE, player, old_attr, new_attr)
    end
    return new_attr
end

-- 获取单个属性
function AttrMgr:GetAttr(obj_uid, attr_id)
    local attr = self:Recalc(obj_uid)
    return attr[attr_id] or 0
end

-- 获取全部属性快照
function AttrMgr:GetAttrAll(obj_uid)
    return self:Recalc(obj_uid)
end

-- 实体销毁,清理内存
function AttrMgr:CleanObj(obj_uid)
    self.layer_data[obj_uid] = nil
    self.cache[obj_uid] = nil
    self.dirty[obj_uid] = nil
end

-- 单例
g_AttrMgr = AttrMgr:New()

5. 使用示例(各个模块调用)

lua 复制代码
-- 1. 玩家升级,修改基础层
AttrMgr:SetLayerAttr(player.uid, AttrMgr.LAYER.BASE, ATTR.HP, 1000)

-- 2. 穿上装备,设置装备层
AttrMgr:SetLayerAttr(player.uid, AttrMgr.LAYER.EQUIP, ATTR.ATTACK, 50)
-- 脱装备:清空装备层
AttrMgr:ClearLayer(player.uid, AttrMgr.LAYER.EQUIP)

-- 3. 加buff,BuffMgr调用
AttrMgr:SetLayerAttr(player.uid, AttrMgr.LAYER.BUFF, ATTR.ATTACK, 20)
-- buff过期
AttrMgr:ClearLayer(player.uid, AttrMgr.LAYER.BUFF)

-- 4. 设置百分比加成(万分比,2000=20%)
AttrMgr:SetLayerAttr(player.uid, AttrMgr.LAYER.PERCENT, ATTR.ATTACK, 2000)

-- 5. 改动后触发重算、同步、事件
AttrMgr:RecalcPlayerAttr(player)

-- 6. 读取属性
local atk = AttrMgr:GetAttr(player.uid, ATTR.ATTACK)

6. 模块依赖 & 边界说明

  • 依赖:EventMgr(抛ATTR_CHANGE)、AoiMgr(AOI广播属性)、PlayerMgr、Proto
  • 不负责:属性校验、扣血、伤害计算(伤害交给 FightMgr)
  • 缓存+脏标记:只有标记dirty才重算,避免每帧反复计算,性能友好
  • 百分比是在固定值总和之后乘,传奇类标准算法
  • 怪物也复用这套AttrMgr,怪物uid传入即可,一套代码同时支持玩家+怪物

7. 配套GM指令(查看属性)

lua 复制代码
GMMgr:RegisterCmd("attr_query", 1, function(gm, args)
    local uid = tonumber(args[1])
    local player = PlayerMgr:GetPlayerByUid(uid)
    if not player then return "玩家不存在" end
    local all = AttrMgr:GetAttrAll(player.uid)
    local str = ""
    for k,v in pairs(all) do
        str = str .. string.format("aid:%d=%d ",k,v)
    end
    return str
end)
相关推荐
超爱吃士力架17 小时前
程序员的副业 IP 探索之路----小白用AI做了个游戏,发出去第一天居然有300多个人玩!!!
游戏开发
SmalBox1 天前
08-05-热更新-完整热更新流程实战
unity3d·游戏开发
只睡四小时2 天前
Three.js Sub-tick:漏判 60.7%→0
开发语言·javascript·性能优化·ecmascript·游戏开发·three.js·碰撞检测
SmalBox2 天前
08-04-热更新-首包资源策略
unity3d·游戏开发
SmalBox3 天前
08-03-热更新-边玩边下载
unity3d·游戏开发
西风逍遥游4 天前
从 EveScript 到 B站 Toy:一个 WebGPU 游戏发布插件怎样设计
游戏开发
西风逍遥游4 天前
500 单位 RTS 为什么还没到 60 FPS:一次 WebGPU 游戏性能诊断
游戏开发
甲维斯4 天前
Codex,Zcode,KimiCode电脑操作能力对比!
人工智能·游戏开发
SmalBox4 天前
08-02-热更新-差量更新与断点续传
unity3d·游戏开发