修改bot_stuff来控制爆头率
你好!本指南将带你一步步了解,怎么在你使用的插件Manico's CSGOBetterbots里,增加一个自定义爆头率的功能。当然还会聊聊为什么你设置的,和Bot实际打出来的有差异。
📚 SourcePawn & ConVar 初级入门
在开始修改前,先来简单了解一下插件的几个小概念:
-
SourcePawn 是什么?
SourcePawn 是一种编程语言,它是专门为 Valve 的 Source 引擎设计,用来编写 SourceMod 插件的。你现在看到的这些代码,就是用 SourcePawn 写的。通过 SourceMod 插件,我们可以在 CS:GO 等游戏里添加各种自定义功能,比如调整 Bot 的行为。
-
ConVar (控制台变量) 是什么?
ConVar 全称是 "Console Variable",翻译过来就是“控制台变量”。简单来说,它就像是插件提供给服务器管理员的一个“开关”或“调节器”。有了 ConVar,你不需要修改或重新编译插件代码,就能直接在游戏控制台或者服务器配置文件里改变插件的一些设定,比如 Bot 的爆头率!
显然,比起每次都要关掉服务器、修改代码、重新编译、覆盖文件,用控制台更改参数是更合理轻松的选择,也更灵活。
-
ConVar 在 SourcePawn 里长啥样?
在 SourcePawn 代码里,一个 ConVar 首先需要被声明(告诉程序有这么个东西),格式会像 ConVar g_cvYouNameIt; 这样。
然后,在插件启动的时候,也就是在 OnPluginStart() 函数里,它需要被**初始化**(给它一个名字、默认值、描述等等),这会用到 CreateConVar() 函数。你会在后面的代码里看到这些格式。
💻 代码修改步骤
下面,我们将从 Manico 的原始版本开始,一步步把它升级成带可配置爆头率的版本。
1. Manico 的原始函数:最初的爆头逻辑
最开始的时候,Bot 瞄准目标的代码可能长这样。这里面的爆头率是固定写死的,调整起来很不方便:
public void SelectBestTargetPos(int client, float fTargetPos[3])
{
if(IsValidClient(g_iTarget[client]) && IsPlayerAlive(g_iTarget[client]))
{
int iBone = LookupBone(g_iTarget[client], "head_0");
int iSpineBone = LookupBone(g_iTarget[client], "spine_3");
if (iBone < 0 || iSpineBone < 0)
return;
bool bShootSpine; // 默认值为 false,意味着倾向于射击头部
float fHead[3], fBody[3], fBad[3];
GetBonePosition(g_iTarget[client], iBone, fHead, fBad);
GetBonePosition(g_iTarget[client], iSpineBone, fBody, fBad);
fHead[2] += 4.0;
if (BotIsVisible(client, fHead, false, -1))
{
if (BotIsVisible(client, fBody, false, -1))
{
if (!IsValidEntity(g_iActiveWeapon[client])) return;
int iDefIndex = GetEntProp(g_iActiveWeapon[client], Prop_Send, "m_iItemDefinitionIndex");
switch(iDefIndex)
{
// 这些是大多数步枪、冲锋枪等等的武器ID,这里有个硬编码的概率
case 7, 8, 10, 13, 14, 16, 17, 19, 23, 24, 25, 26, 27, 28, 29, 33, 34, 35, 39, 60:
{
float fTargetDistance = GetVectorDistance(g_fBotOrigin[client], fHead);
// 核心判断:如果IsItMyChance(70.0) 返回true (有70%概率发生) 并且距离近
if (IsItMyChance(70.0) && fTargetDistance < 2000.0)
bShootSpine = true; // 目标瞄准身体(非爆头)
}
// 这些是AWP、散弹枪等等的武器ID,它们总是瞄准身体
case 9, 11, 38: // 9 是 AWP
{
bShootSpine = true; // 总是瞄准身体(非爆头)
}
}
}
}
else
{
// 头部不可见时的备用逻辑:尝试寻找其他可见骨骼
for (int b = 0; b <= sizeof(g_szBoneNames) - 1; b++)
{
iBone = LookupBone(g_iTarget[client], g_szBoneNames[b]);
if (iBone < 0)
return;
GetBonePosition(g_iTarget[client], iBone, fHead, fBad);
if (BotIsVisible(client, fHead, false, -1))
break;
else
fHead[2] = 0.0;
}
}
if(bShootSpine)
fTargetPos = fBody; // 最终瞄准身体
else
fTargetPos = fHead; // 最终瞄准头部
}
}
简单分析一下这个版本:
bShootSpine 变量在函数开始时没有明确初始化,但根据后面的逻辑,它默认倾向于 false(瞄准头部)。
- 对于大多数武器,如果距离很近(小于
2000.0),并且 IsItMyChance(70.0) 返回 true,Bot 才会选择瞄准身体。这意味着有 70% 的概率去瞄准身体,30% 的概率去瞄准头部。
- 对于像 AWP (武器ID 9) 这样的武器,Bot 总是瞄准身体。这很合理,因为打胸也能一枪死,强行追求爆头反而有风险。
2. un1 的修改版:让爆头率可控
现在我们将上面的函数修改。这个版本引入了 ConVar ,实现了更精细的控制,比如全局强制爆头开关,以及 CT 和 T 阵营 Bot 不同的爆头率调整:
public void SelectBestTargetPos(int client, float fTargetPos[3])
{
if(IsValidClient(g_iTarget[client]) && IsPlayerAlive(g_iTarget[client]))
{
int iBone = LookupBone(g_iTarget[client], "head_0");
int iSpineBone = LookupBone(g_iTarget[client], "spine_3");
if (iBone < 0 || iSpineBone < 0)
return;
bool bShootSpine = false; // **重要!**在这里明确初始化为 false,表示默认瞄准头部
float fHead[3], fBody[3], fBad[3];
GetBonePosition(g_iTarget[client], iBone, fHead, fBad);
GetBonePosition(g_iTarget[client], iSpineBone, fBody, fBad);
fHead[2] += 4.0; // 稍微抬高瞄准点,让子弹更容易命中真实头部区域
if (BotIsVisible(client, fHead, false, -1))
{
if (BotIsVisible(client, fBody, false, -1))
{
if (!IsValidEntity(g_iActiveWeapon[client])) return;
int iDefIndex = GetEntProp(g_iActiveWeapon[client], Prop_Send, "m_iItemDefinitionIndex");
// 优先判断:如果开启了全局强制爆头模式 (g_cvHeadShotOnly.BoolValue)
// 并且当前武器不是 AWP (武器ID 9),或者这个 Bot 被特殊标记为强制爆头 (g_bHeadShotOnly[client])
if((g_cvHeadShotOnly.BoolValue && iDefIndex != 9) || g_bHeadShotOnly[client])
{
bShootSpine = false; // 强制瞄准头部
}
else // 如果没有强制爆头,就根据概率和武器类型来判断
{
switch(iDefIndex)
{
// 特定武器ID (AWP, 散弹枪等),总是强制瞄准身体
case 9, 11, 38: // 9: Weapon_AWP, 11: Weapon_XM1014, 38: Weapon_SawedOff
{
bShootSpine = true; // 强制射击脊椎(身体),即不爆头
}
// 大多数其他武器类型 (步枪、冲锋枪等)
default:
{
float fCurrentHeadshotChance = 0.0;
// 根据 Bot 的阵营,获取对应的爆头率 ConVar 值
if (g_bIsCT[client])
{
fCurrentHeadshotChance = g_cvBotHSC.FloatValue; // 获取 CT 的爆头率参数
}
else if (g_bIsT[client])
{
fCurrentHeadshotChance = g_cvBotHST.FloatValue; // 获取 T 的爆头率参数
}
// 关键的爆头率判断:
// 记住,IsItMyChance(X) 返回 true 的概率是 X%。
// 如果返回 true,Bot 就会切换瞄准身体 (bShootSpine = true)。
// 所以,如果想实现 fCurrentHeadshotChance% 的爆头率,
// 那就需要 (100.0 - fCurrentHeadshotChance)% 的概率去瞄准身体。
// 因此 IsItMyChance 的参数是 100.0 - fCurrentHeadshotChance。
if (IsItMyChance(100.0 - fCurrentHeadshotChance))
{
bShootSpine = true; // Bot 决定射击脊椎(身体)
}
// 如果 If 条件不满足,bShootSpine 仍然保持默认的 false,表示瞄准头部
}
}
}
}
}
else
{
// 如果头部不可见,这里是尝试寻找其他可见骨骼的备用逻辑。
// 这个循环会找到第一个能看到的骨骼,并把它的位置赋给 fHead。
// 如果所有骨骼都看不到,fHead[2] 会被设为0.0,可能导致Bot瞄准地面,
// 在更复杂的Bot行为中,这块逻辑可能需要更精细的处理。
for (int b = 0; b <= sizeof(g_szBoneNames) - 1; b++)
{
iBone = LookupBone(g_iTarget[client], g_szBoneNames[b]);
if (iBone < 0)
return;
GetBonePosition(g_iTarget[client], iBone, fHead, fBad);
if (BotIsVisible(client, fHead, false, -1))
break; // 找到可见骨骼,跳出循环
else
fHead[2] = 0.0; // 当前骨骼不可见,重置,继续尝试下一个
}
}
// 最后,根据 bShootSpine 的值来确定最终 Bot 要瞄准的位置
if(bShootSpine)
fTargetPos = fBody; // 瞄准身体
else
fTargetPos = fHead; // 瞄准头部
}
}
⚠️ 关于 IsItMyChance() 的知识
在代码里,if (IsItMyChance(X)) bShootSpine = true; 这句的意思是说:当 IsItMyChance(X) 返回 true 的时候(它的成功概率是 X%),Bot 就会瞄准身体,也就是**不爆头**。
所以,如果你想要 Bot 有 V% 的概率爆头,那么你给 IsItMyChance 传的参数应该是 (100.0 - V)。这样,当它以 (100-V)% 的概率返回 true 时,就瞄准身体;当它以 V% 的概率返回 false 时,就瞄准头部,这正好符合“V%爆头”的逻辑!
上面的代码会按这个逻辑来设置 ConVar,所以 ConVar 的值直接就是你想要的爆头率百分比(0.0到100.0)。
3. 声明 ConVar
在你的 SourceMod (SP) 插件文件最上面,或者全局变量声明的地方,你需要告诉程序你将会有这些 ConVar。根据 SourceMod 的常用写法,会像这样声明:
#include <sourcemod> // SourceMod 插件必备的库
#include <sdktools> // 如果你的插件用到 LookupBone, GetBonePosition 等函数,也需要这个,通常不用自己改动
// ... 其他你插件里本来就有的全局变量 ...
// 强制爆头模式的 ConVar
ConVar g_cvHeadShotOnly;
// Bot CT 爆头率的 ConVar
ConVar g_cvBotHSC;
// Bot T 爆头率的 ConVar
ConVar g_cvBotHST;
// 假设这些是你插件里已经有了的变量,用来判断 Bot 阵营或特殊标记
bool g_bIsCT[MAXPLAYERS+1];
bool g_bIsT[MAXPLAYERS+1];
bool g_bHeadShotOnly[MAXPLAYERS+1]; // 每个 Bot 的强制爆头标记
4. 初始化 ConVar
ConVar 声明之后,你还需要告诉程序它们叫什么名字、默认值是多少、有什么用。这些初始化工作通常在你的插件启动函数 OnPluginStart 里完成:
public void OnPluginStart()
{
// ... 其他你插件里本来就有的初始化代码 ...
// 创建“强制爆头模式”ConVar。注意这里的命名通常会自动加上 'sm_' 前缀
g_cvHeadShotOnly = CreateConVar(
"bot_headshot_only", // ConVar 在控制台里的名字
"0", // 默认值是“0”,表示关闭强制爆头
"如果开启(1), Bot 将总是尝试爆头 (AWP除外).", // 在控制台输入 'sm_cvarlist' 时能看到的描述
_, // 这个下划线表示使用默认的 FCVAR_PLUGIN 标志
true, 0.0, // 最小值是 0.0
true, 1.0 // 最大值是 1.0 (因为是个开关,只有0和1)
);
// 创建 CT 阵营 Bot 爆头率 ConVar
g_cvBotHSC = CreateConVar(
"bot_hsc", // CT 爆头率 ConVar 的名字
"75.0", // 默认 CT 爆头率是 75.0%。注意:这是你提供的默认值。
"Bot Stuff - HSC parameter.\nDefault: 75.0", // 描述
_, // 默认标志
true, 0.0, // 最小值 0.0
true, 100.0 // 最大值 100.0
);
// 创建 T 阵营 Bot 爆头率 ConVar
g_cvBotHST = CreateConVar(
"bot_hst", // T 爆头率 ConVar 的名字
"75.0", // 默认 T 爆头率是 75.0%。注意:这是你提供的默认值。
"Bot Stuff - HST parameter.\nDefault: 75.0", // 描述
_, // 默认标志
true, 0.0, // 最小值 0.0
true, 100.0 // 最大值 100.0
);
}
关于 ConVar 名字的小提醒:
根据 SourceMod 的规则,如果你在 CreateConVar 里没有给 ConVar 名字加上 sm_ 前缀,但使用了 FCVAR_PLUGIN 标志(这个在你提供的代码里用 _ 占位符默认是开启的),那么 SourceMod 会自动帮你加上 sm_。所以,虽然你代码里写的是 "bot_hst",但在控制台里你可能需要用 sm_bot_hst 来设置它。
5. 编译和部署
好啦,代码部分搞定!现在你需要把这些代码保存成一个 .sp 文件(最好备份一下再保存),然后用 SourceMod 的编译器把它编译成 .smx 文件。如果你不知道怎么编译,可以去 SourceMod 官网 找教程。
编译好之后,把生成的 .smx 文件放到你的 CS:GO 服务器的 addons/sourcemod/plugins 文件夹里。
服务器启动或加载插件后,你就可以在游戏控制台里输入以下命令来调整 Bot 的爆头率:
sm_rcon bot_headshot_only 1:开启全局强制爆头模式。除了 AWP (武器ID 9),所有武器都会尝试爆头。
sm_rcon bot_headshot_only 0:关闭全局强制爆头模式,Bot 会根据 CT/T 阵营的概率来爆头。
sm_rcon bot_hsc [0.0-100.0]:设置 CT 阵营 Bot 的爆头率。比如,sm_bot_hsc 75.0 会把 CT Bot 爆头率设为 75%。
sm_rcon bot_hst [0.0-100.0]:设置 T 阵营 Bot 的爆头率。比如,sm_bot_hst 50.0 会把 T Bot 爆头率设为 50%。
如果你想让这些设置在服务器重启后也生效,你可以在服务器的 cfg/sourcemod/ 文件夹里,创建一个对应 ConVar 名字的 .cfg 文件(比如 sm_bot_hsc.cfg),在里面写入你的值,这样服务器每次启动都会自动加载它们。
🤔 为什么我设置的爆头率与实际不同?
这是一个很常见也很容易让人困惑的问题。你通过 ConVar 设置的比如 sm_bot_hsc,它只是 Bot 在特定情况下决定瞄准头部的一个概率,而不是他们实际打出的爆头总数的百分比。
核心概念:条件概率与执行限制
你设定的爆头率 ConVar 只有在满足了一系列 前提条件 之后,才会真正“生效”:
- 爆头的伤害高: 这导致死亡的击杀往往是因为爆头,结算就是爆头+1。
具体原因在这:爆头率计算的数学原理。
- 头部和身体“都”能看见:
- 武器类型限制:
像 AWP (9)、XM1014 (11) 和 Sawed-Off (38) 这些武器,Bot 是永远瞄准身体的(bShootSpine = true)。无论 ConVar 爆头率设得多高,这些武器的爆头率在代码层面就是 0%。对于ProBot,狙击手的爆头率会低一些。