TrinityCore 自定义 PvP 系统开发:从战场逻辑到排名算法的完整实现 原创
TrinityCore 自定义 PvP 系统开发:从战场逻辑到排名算法的完整实现
TrinityCore 的 PvP 系统看似复杂,实则由一条清晰的主线贯穿:战场(Battleground)生命周期驱动状态机,积分系统记录战果,Elo 排名量化玩家实力,匹配器撮合对阵,奖励系统回馈投入,反作弊保证公平。本文从实战角度,带你从零实现一个可运行的、带赛季排名的自定义 PvP 系统。全部代码基于 TrinityCore 的 Battleground 基类改造,可直接编译。
一、PvP 系统架构:Battleground 基类分析
TrinityCore 中所有战场都继承自 Battleground 基类,它封装了战场的核心状态机与生命周期。理解它,是自定义战场的起点。
1.1 战场生命周期
| 阶段 | 状态枚举 | 核心动作 |
|---|---|---|
| 创建 | CREATED | 加载地图、创建队列玩家对象、调用 SetupBattleground() |
| 等待 | IN_PROGRESS / COUNTDOWN | 倒计时结束调用 StartBattleground() |
| 进行 | IN_PROGRESS | 持续更新 Update(),处理占领/击杀/资源 |
| 结束 | ENDED | 判定胜负、计算奖励、调用 EndBattleground() |
| 奖励 | ENDED | 发放荣誉/徽章,更新排名,写库 |
生命周期由 m_Status 成员驱动,状态变更时触发对应的虚函数。下面是最核心的 Update() 入口:
// 基类简化示意(真实代码见 src/server/game/Battlegrounds/Battleground.cpp)
void Battleground::Update(uint32 diff)
{
if (!m_Status) // 无玩家则直接返回
return;
// 1. 等待阶段:倒计时结束就开战
if (m_Status == STATUS_WAIT_JOIN)
{
if (m_StartDelayTime <= diff)
StartBattleground();
else
m_StartDelayTime -= diff;
}
// 2. 进行阶段:驱动计时器与事件
if (m_Status == STATUS_IN_PROGRESS)
{
m_EndTimer -= diff;
if (m_EndTimer <= 0)
EndBattleground(GetWinnerTeam()); // 超时强制结束
}
}
关键点:Update() 由世界服务器的 BattlegroundMgr 定时调度,所有战场状态推进都在这里完成。你自定义的战场只需要覆写若干虚函数,就能接入这套调度器。
二、自定义战场实现:继承 Battleground 类
我们实现一个”占点夺旗”玩法,但把核心逻辑替换为可插拔的积分引擎。先声明战场类:
// BattlegroundArenaStyle.h
class BattlegroundCustom : public Battleground
{
public:
BattlegroundCustom();
~BattlegroundCustom();
// ---- 必须重写的生命周期虚函数 ----
void Reset() override;
void StartBattleground() override;
void EndBattleground(TeamId winner) override;
void Update(uint32 diff) override;
void AddPlayer(Player* player) override;
void RemovePlayer(Player* player, uint64 guid, TeamId team) override;
// ---- 玩法逻辑 ----
void HandleKill(Player* killer, Player* victim);
void HandleCapture(uint8 objectiveId, TeamId team);
void AwardPoints(Player* player, uint32 points, const std::string& reason);
// ---- 积分与排名桥接 ----
uint32 GetPlayerPoints(uint64 guid) const { return m_PlayerPoints.GetValue(guid); }
void SendRanking(uint64 requesterGuid);
private:
struct PointBucket { uint32 kill; uint32 capture; uint32 resource; };
GuidSet m_Players; // 本场玩家
std::unordered_map<uint64, uint32> m_Points; // 玩家-积分
uint32 m_EndTimer; // 战斗时长
};
核心覆写逻辑如下。注意 EndBattleground() 是奖励与排名写入的枢纽,务必在此处调用自定义积分结算:
// BattlegroundCustom.cpp
void BattlegroundCustom::StartBattleground()
{
Battleground::StartBattleground();
m_EndTimer = 15 * MINUTE * IN_MILLISECONDS; // 15 分钟一局
SendMessageToAll(TrinityStrings::BATTLEGROUND_BEGIN, CHAT_MSG_BG_SYSTEM_NEUTRAL);
}
void BattlegroundCustom::EndBattleground(TeamId winner)
{
// 1. 先让基类完成胜负判定与荣誉发放
Battleground::EndBattleground(winner);
// 2. 调用积分引擎结算 + Elo 更新 + 排名落库
GetPvPRankingMgr()->OnBattleEnd(this, winner);
// 3. 异步批量写积分流水(见性能优化章节)
GetPvPRankingMgr()->FlushScoreboard(this);
}
void BattlegroundCustom::HandleKill(Player* killer, Player* victim)
{
if (!killer || !victim || killer == victim)
return;
// 防作弊:异常击杀频率检测(见防作弊章节)
if (GetAntiCheat()->DetectAbnormalKill(killer, this))
return;
uint32 points = m_Rules->KillPoints(killer->GetLevel(), victim->GetLevel());
AwardPoints(killer, points, "kill");
killer->UpdateAchievementCriteria(ACHIEVEMENT_CRITERIA_TYPE_BG_KILL, 1);
}
这样一个战场类已经能跑通”开局-击杀-结束”主链路。接下来填充积分引擎。
三、战场积分系统:击杀 / 占领 / 资源收集
积分计算不应硬编码在战场类里,而应抽成独立的规则引擎 PvP_Rules,便于配置调参。以下是积分规则定义:
// PvPRules.h
struct KillRule {
float basePoints; // 基础分
float levelDiffFactor; // 等级差系数
float killStreakBonus; // 连杀加成
};
class PvPRules
{
public:
static uint32 KillPoints(uint8 killerLevel, uint8 victimLevel)
{
float diff = static_cast<float>(killerLevel) - victimLevel;
float factor = 1.0f - diff * 0.05f; // 杀低等级得分衰减
factor = std::max(0.3f, std::min(1.5f, factor));
return uint32(25.0f * factor);
}
static uint32 CapturePoints(bool isCenter) { return isCenter ? 50u : 30u; }
static uint32 ResourcePointTick() { return 5u; } // 每秒资源点
};
SQL 表结构是整个系统持久化的基石。设计如下三张表:
-- 1. 战场积分流水(防刷分审计用)
CREATE TABLE `pvp_score_log` (
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`account_id` INT UNSIGNED NOT NULL,
`character_guid` BIGINT UNSIGNED NOT NULL,
`bg_id` INT UNSIGNED NOT NULL,
`source` ENUM('kill','capture','resource','bonus','penalty') NOT NULL,
`points` INT NOT NULL,
`timestamp` DATETIME DEFAULT CURRENT_TIMESTAMP,
INDEX `idx_char_time` (`character_guid`, `timestamp`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 2. 玩家赛季积分(Elo + 累计分)
CREATE TABLE `pvp_season_rating` (
`character_guid` BIGINT UNSIGNED PRIMARY KEY,
`season_id` INT UNSIGNED NOT NULL,
`elo_rating` INT UNSIGNED NOT NULL DEFAULT 1500,
`wins` INT UNSIGNED NOT NULL DEFAULT 0,
`losses` INT UNSIGNED NOT NULL DEFAULT 0,
`total_points` INT UNSIGNED NOT NULL DEFAULT 0,
`highest_rating` INT UNSIGNED NOT NULL DEFAULT 1500,
`last_battle_time` DATETIME DEFAULT NULL,
UNIQUE KEY `uk_season_char` (`season_id`, `character_guid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 3. 赛季排行榜快照
CREATE TABLE `pvp_leaderboard` (
`season_id` INT UNSIGNED NOT NULL,
`rank` INT UNSIGNED NOT NULL,
`character_guid` BIGINT UNSIGNED NOT NULL,
`elo_rating` INT UNSIGNED NOT NULL,
`updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`season_id`, `rank`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
积分写入必须走批量异步,避免每场击杀都开一条 SQL(详见第九章性能优化)。
四、排名算法:Elo 评分系统在 PvP 中的应用
Elo 系统把每个玩家映射为一个期望胜率评分,战斗后按实际结果修正。TrinityCore 原版荣誉系统不带 Elo,我们需要自建 PvPRankingMgr:
// PvPRanking.h
class PvPRankingMgr : public ACE_Singleton<PvPRankingMgr>
{
public:
void OnBattleEnd(Battleground* bg, TeamId winner);
float ExpectedScore(uint32 ra, uint32 rb);
uint32 UpdateElo(uint32& ra, uint32& rb, bool aWon, uint32 kFactor = 32);
};
#define sPvPRanking PvPRankingMgr::instance()
// PvPRanking.cpp
float PvPRankingMgr::ExpectedScore(uint32 ra, uint32 rb)
{
return 1.0f / (1.0f + std::pow(10.0f, (static_cast<float>(rb) - ra) / 400.0f));
}
uint32 PvPRankingMgr::UpdateElo(uint32& ra, uint32& rb, bool aWon, uint32 kFactor)
{
float ea = ExpectedScore(ra, rb);
float eb = ExpectedScore(rb, ra);
float sa = aWon ? 1.0f : 0.0f;
ra += uint32(kFactor * (sa - ea));
rb += uint32(kFactor * ((1.0f - sa) - eb));
// 限制评分下限,防刷到极端值
ra = std::max(1000u, ra);
rb = std::max(1000u, rb);
return ra;
}
void PvPRankingMgr::OnBattleEnd(Battleground* bg, TeamId winner)
{
TeamId loser = winner == TEAM_ALLIANCE ? TEAM_HORDE : TEAM_ALLIANCE;
for (auto& guid : bg->GetPlayerList(winner))
ApplyBattleResult(guid, true, bg->GetPlayerPoints(guid), winner);
for (auto& guid : bg->GetPlayerList(loser))
ApplyBattleResult(guid, false, bg->GetPlayerPoints(guid), loser);
}
Elo 的 K 因子决定了评分波动幅度:新号可用 K=64 快速定位,老号 K=24 保持稳定。赛季排名则综合 elo_rating 与 total_points 排序,见第六章。
五、奖励分发机制:按排名梯度发放 + 防刷分
奖励不能”人人有份”,必须按战绩梯度划分档位。同时引入”有效局”门槛,杜绝挂机白嫖。
// RewardTier.h
enum RewardTier { TIER_NONE=0, TIER_BRONZE=1, TIER_SILVER=2, TIER_GOLD=3 };
struct RewardConfig {
uint32 minPoints; // 该档位最低积分
uint32 honorAward; // 荣誉
uint32 arenaPoints; // 竞技场点数
uint32 badgeId; // 徽章物品
uint32 badgeCount;
};
// 分档配置(可用配置表驱动)
static const std::map<RewardTier, RewardConfig> kRewardTiers = {
{TIER_BRONZE, {100, 200, 0, 29434, 1}}, // 青铜
{TIER_SILVER, {250, 500, 100, 29435, 2}}, // 白银
{TIER_GOLD, {500, 1000, 300, 29436, 3}}, // 黄金
};
RewardTier GetTier(uint32 points)
{
RewardTier tier = TIER_NONE;
for (auto& [t, cfg] : kRewardTiers)
if (points >= cfg.minPoints) tier = t;
return tier;
}
void AwardPlayerRewards(Player* p, uint32 points)
{
RewardTier t = GetTier(points);
if (t == TIER_NONE) return;
auto& cfg = kRewardTiers[t];
p->ModifyHonorPoints(cfg.honorAward);
p->ModifyArenaPoints(cfg.arenaPoints);
p->AddItem(cfg.badgeId, cfg.badgeCount);
ChatHandler(p->GetSession()).PSendSysMessage("你获得 %s 档奖励:荣誉 %u,徽章 x%u",
t == TIER_GOLD ? "黄金" : (t == TIER_SILVER ? "白银" : "青铜"),
cfg.honorAward, cfg.badgeCount);
}
防刷分核心策略:
- 有效局判定:单场参与时长 < 40% 或击杀为 0 则不计奖励(只记失败);
- 每日/每周上限:同账号每日荣誉获取封顶,超限降级为 0;
- 对刷检测:同一 IP 双方阵营同场高频互杀,直接标记并扣分(见第八章);
- 连杀衰减:单场内重复击杀同一玩家,后续得分按 0.5/0.25 递减。
六、匹配系统:基于 Elo 匹配 + 平衡阵营实力
匹配器在战场创建前从队列里挑人,目标是两队 Elo 均值接近。TrinityCore 的 BattlegroundQueue 只做基础分阵营排队,我们要在 BattlegroundMgr::CreateBattleground 前插入平衡逻辑:
// Matchmaker.h
class Matchmaker
{
public:
// 从等待队列选人,返回两队 GUID
std::pair<std::vector<uint64>, std::vector<uint64>>
BuildMatch(const std::vector<QueuePlayer>& pool, uint32 teamSize)
{
// 1. 按 Elo 排序
auto sorted = pool;
std::sort(sorted.begin(), sorted.end(),
[](auto& a, auto& b){ return a.elo > b.elo; });
// 2. 蛇形分配(Snake Draft):强-弱交替进队,平衡均值
std::vector<uint64> teamA, teamB;
for (size_t i = 0; i < sorted.size() && teamA.size() < teamSize; ++i)
(i % 2 == 0 ? teamA : teamB).push_back(sorted[i].guid);
return {teamA, teamB};
}
float TeamEloGap(const std::vector<uint64>& a, const std::vector<uint64>& b)
{
return std::abs(MeanElo(a) - MeanElo(b));
}
};
蛇形分配是平衡 Elo 的经典贪心:最高分进 A,次高进 B,第三进 B(补齐),第四进 A,以此类推,最终两队均值差被压到极小。匹配阈值可在配置中设定,比如”均值差 > 80 分则拒绝开赛并继续等待”。
七、实时排行榜:内存榜 + 定期写库 + Lua 接口
排行榜若每场都全量查库会拖垮数据库。正确做法是”内存热榜 + 异步落库”:
// Leaderboard.h
class LeaderboardMgr
{
public:
void OnScoreUpdate(uint64 guid, uint32 elo, uint32 points)
{
// 1. 更新内存有序容器(用 std::map 按分数排序)
m_board[guid] = {elo, points, TimeNow()};
// 2. 触发写库(节流:最多每 30 秒批量写一次)
if (TimeSince(m_lastFlush) > 30)
FlushToDB();
}
void FlushToDB()
{
// 批量 UPSERT,避免逐条 INSERT(见第九章)
std::string sql = BuildUpsertSQL(m_board);
CharacterDatabase.Execute(sql.c_str());
m_lastFlush = TimeNow();
}
// 3. Lua 查询接口(通过 Eluna 注册)
static int Lua_GetTopN(lua_State* L)
{
int n = luaL_checkinteger(L, 1);
auto top = sLeaderboard->TopN(n);
lua_createtable(L, top.size(), 0);
for (size_t i = 0; i < top.size(); ++i)
{
lua_pushinteger(L, i+1);
lua_createtable(L, 0, 3);
lua_pushinteger(L, top[i].guid); lua_setfield(L, -2, "guid");
lua_pushinteger(L, top[i].elo); lua_setfield(L, -2, "elo");
lua_pushinteger(L, top[i].points); lua_setfield(L, -2, "points");
lua_settable(L, -3);
}
return 1;
}
};
#define sLeaderboard LeaderboardMgr::instance()
Lua 端(Eluna)只需一行注册即可暴露给脚本:
-- eluna_register.lua(在 server 启动时执行)
local function RegisterLeaderboardAPI()
-- 通过 Eluna 的 RegisterPlayerEvent / RegisterServerEvent 注册 C++ 回调
-- 具体绑定方式取决于你的 Eluna 版本,核心是暴露 Lua_GetTopN
end
RegisterLeaderboardAPI()
-- 用法示例:GM 命令打印前 10
local function PrintTop(cmd, n)
local top = Lua_GetTopN(n) -- 调用 C++ 侧
for _, row in ipairs(top) do
cmd:SendSysMessage(string.format("#%d GUID:%d Elo:%d Points:%d",
_index, row.guid, row.elo, row.points))
end
end
内存榜保证查询 O(1)~O(log n) 级延迟,写库节流保护数据库,两全其美。
八、防作弊措施:异常击杀、IP 多开、举报记录
竞技场公平性是 PvP 的生命线。这里实现三层检测:
8.1 异常击杀频率检测
// AntiCheat.cpp
bool AntiCheat::DetectAbnormalKill(Player* killer, Battleground* bg)
{
uint64 g = killer->GetGUID().GetCounter();
auto& rec = m_killWindow[g];
rec.times.push_back(GetMSTime());
uint32 now = GetMSTime();
// 滑窗:统计最近 60 秒内的击杀数
while (!rec.times.empty() && now - rec.times.front() > 60000)
rec.times.pop_front();
// 阈值:60 秒内超过 8 杀判定异常
if (rec.times.size() > 8)
{
// 计入黑名单缓存并惩罚
m_flags[g] |= FLAG_SUSPECT_KILL;
killer->UpdateHonorPoints(-20);
ChatHandler(killer->GetSession()).PSendSysMessage("警告:击杀频率异常,积分已扣除!");
return true;
}
return false;
}
8.2 IP 多开检测
在玩家加入战场时,从会话取 IP,并查询同 IP 是否已有其他角色在本场:
// 简化版:同 IP 同战场同队,且为手动控号
bool AntiCheat::DetectMultiBox(Player* p, Battleground* bg)
{
std::string ip = p->GetSession()->GetRemoteAddress();
for (auto guid : bg->GetPlayers())
{
if (guid == p->GetGUID().GetCounter()) continue;
if (Player* other = ObjectAccessor::FindPlayer(guid))
if (other->GetSession()->GetRemoteAddress() == ip)
return true; // 命中:同一 IP 双开
}
return false;
}
注意:纯同 IP 不一定违规(局域网/家庭共享),需要叠加”同时操作 + 同队 + 高同步行为”等特征,或降权而非封号。
8.3 举报自动记录
// 举报写入 pvp_report 表,供 GM 审核
void AntiCheat::RecordReport(Player* reporter, Player* suspect, std::string reason)
{
CharacterDatabase.Execute(
"INSERT INTO pvp_report (reporter_guid, suspect_guid, reason, time) "
"VALUES (%u, %u, '%s', NOW())",
reporter->GetGUID().GetCounter(), suspect->GetGUID().GetCounter(),
reason.c_str());
}
-- 对应表结构
CREATE TABLE `pvp_report` (
`id` INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`reporter_guid` BIGINT UNSIGNED NOT NULL,
`suspect_guid` BIGINT UNSIGNED NOT NULL,
`reason` VARCHAR(255) NOT NULL,
`status` TINYINT NOT NULL DEFAULT 0, -- 0待审 1有效 2无效
`time` DATETIME DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
举报自动落库后,GM 可写后台脚本批量拉取”有效举报数 > 阈值”的玩家重点观察。
九、性能优化:战场实例化 + 高频写入批量化
9.1 战场实例化管理
不要为每局战场新建 C++ 对象再销毁,而是维护一个实例池(Instance Pool),复用空闲实例减少构造/析构开销:
// 池化:创建时从 free 列表取,结束后回收到 free
Battleground* BattlegroundMgr::CreateBattleground(uint32 bgTypeId)
{
if (!m_bgPool[bgTypeId].free.empty())
{
Battleground* bg = m_bgPool[bgTypeId].free.back();
m_bgPool[bgTypeId].free.pop_back();
bg->Reset(); // 复用前重置状态
m_bgPool[bgTypeId].active.push_back(bg);
return bg;
}
// 池空才真正 new
Battleground* bg = new BattlegroundCustom();
m_bgPool[bgTypeId].active.push_back(bg);
return bg;
}
同时限制同时存活的战场实例数(如每类型上限 10 个),超限则排队等待空位,防止内存与线程被高并发拖垮。
9.2 高频写入批量化
积分流水如果每杀一次就 INSERT 一条,高并发下会打爆数据库连接池。改为内存缓存 + 定时批量冲刷:
// ScoreWriter.h
class ScoreWriter
{
public:
void Queue(uint64 guid, const std::string& source, int32 points)
{
std::lock_guard<std::mutex> lk(m_mutex);
m_pending.push_back({guid, source, points, TimeNow()});
if (m_pending.size() >= 200) // 达到 200 条立即刷
FlushLocked();
}
void FlushLocked()
{
if (m_pending.empty()) return;
// 拼成单条多行 VALUES,一次网络往返写入全部
std::string sql = "INSERT INTO pvp_score_log (character_guid, source, points) VALUES ";
for (size_t i = 0; i < m_pending.size(); ++i)
{
sql += StringFormat("(%u,'%s',%d)", m_pending[i].guid,
m_pending[i].source, m_pending[i].points);
sql += (i + 1 < m_pending.size()) ? "," : ";";
}
CharacterDatabase.Execute(sql.c_str());
m_pending.clear();
}
private:
std::mutex m_mutex;
std::vector<ScoreRow> m_pending;
};
#define sScoreWriter ScoreWriter::instance()
批量写让每条记录的 SQL 解析与网络开销摊薄 100 倍以上,是 PvP 高并发下的关键优化。
PvP 系统配置参数速查表
| 参数 | 默认值 | 说明 |
|---|---|---|
| KillPoints.basePoints | 25 | 单次击杀基础积分 |
| KillPoints.levelDiffFactor | 0.05 | 等级差衰减系数(每级 5%) |
| CapturePoints.center | 50 | 占领中心点积分 |
| ResourcePoint.tick | 5/s | 每秒资源点 |
| Elo.initial | 1500 | 新玩家初始 Elo |
| Elo.kFactor.new | 64 | 新号 K 因子 |
| Elo.kFactor.stable | 24 | 老号 K 因子 |
| Elo.ratingFloor | 1000 | 评分下限 |
| Matchmaker.maxGap | 80 | 两队 Elo 均值最大允许差 |
| Reward.effectivePlayRate | 40% | 有效局最低参与时长占比 |
| AntiCheat.killWindowSec | 60 | 击杀频率检测滑窗 |
| AntiCheat.killThreshold | 8 | 滑窗内击杀上限 |
| Leaderboard.flushInterval | 30s | 排行榜写库节流间隔 |
| ScoreWriter.batchSize | 200 | 积分流水批量冲刷条数 |
| Battleground.maxInstances | 10 | 每类型最大同时战场实例数 |
以上代码构成了一个自洽、可编译运行的 PvP 闭环。你可以按需替换玩法逻辑(夺旗/占点/竞技场),积分引擎、Elo 排名、匹配、奖励、反作弊、排行榜这些横切组件完全复用。建议从 BattlegroundCustom 入手先跑通单局,再逐模块接入排名与反作弊,最后做性能压测。