简单来说,开发者需要明确自己的操作对象,对接一个合适的事件,在挂钩目标函数内修改相关属性或行为.
想要用AMXX实现自己想要的功能,首先得了解四个重要的抽象概念:
对象(Object) 实体(Entity) 事件(Event) 挂钩(Hook)
在面向对象编程中,对象拥有属性(数据)和方法(函数),对象是面向对象编程中的一个基本概念.
对象属性用于存储对象的状态,方法用来定义对象的行为和操作.
我们可以借用这一概念,将万物视作对象,以此了解自己想做什么.
比如变量是一个对象,拥有名称,是否是数组,是否是函数,储存的值,Tag标签,被什么说明符声明等等.
比如数组是一个对象,一个数组拥有长度,表示内部储存了多少个值,
动态数组甚至会有拥有增删查改方法,以某种规律排列元素的方法,或更改容量的方法.
cs1.6游戏中的实体是实打实的对象.拥有属性与方法,属性用于存储各种状态,方法用于定义各种行为.
比如游戏中的玩家是一个对象,拥有名称,移动速度,等属性.还有攻击行为,跳跃行为,奔跑行为等等.
大部分时候,amxx插件都被用于更改实体相关的属性与行为.
对象拥有指针(类似入门教程中介绍的数组索引),表示其数据储存在计算机的某个位置.是数据的地址.
而游戏中所有实体(包括玩家)的指针都被储存在一个数组中,可以称呼为实体数组,或实体列表.
一个对象将另一个对象存入自己的某个属性是非常常见的.
比如玩家有一个属性就是储存当前使用的武器,还有一个属性储存数组,数组内储存自身拥有的所有武器.
将所有相同类型的对象存入数组中,可以让开发者非常方便的进行统一管理.
amxx便通过索引访问实体数组中的实体,而这种索引也被叫做实体索引.
游戏中,实体包含:玩家,人质,武器,道具,门,按钮,水,电梯,载具,气泡,玻璃,雷电,激光,烟雾,光源.
看不见的,可得见的,可碰触的,不可碰触的,几乎都是实体.
实体被储存在一个数组中,amxx可以通过实体索引访问数组中的实体,查改实体属性.也可以增删实体.
实体被划分为多个类型,不同的类型拥有通用属性,也有其类型专属的属性.
cstrike模块的相关头文件,提供了一些针对CS玩家实体的接口函数,可查改玩家小部分专属属性.
fakemeta模块的相关头文件,提供了大量接口函数,可查改实体通用,专属属性.增删实体.
hamsandwich模块的相关头文件,提供了些许接口函数,可查改实体某一类专属属性.
游戏中所发生的每件事,玩家的每个行为,都是由函数实现的,而执行这些函数可以视作触发事件.
amxx自身携带的种种模块,将一部分事件封装为对象,可通过forward native接口函数访问.
可以在它们触发前后,执行各种函数,给事件附加种种影响.或者在某一刻强行触发事件.
能被抽象为事件的函数,拥有前置挂钩(Pre Hook)和后置挂钩(Post Hook)两种挂钩.
前置挂钩基本上都可以利用挂钩目标函数的返回值阻止事件,或通过一些native函数更改事件参数和事件返回值.
后置挂钩不能阻止事件,但依然有可能更改事件参数和返回值.
一个事件通常可以添加多个挂钩,每个事件和挂钩都对应一个目标函数.
一般情况下,即便事件被阻止也依然会触发所有后置挂钩.
执行事件,实际上就是执行事件的目标函数.注册事件,说的就是给事件添加挂钩.
执行事件的目标函数之前,会先调用所有前置挂钩的目标函数.
执行事件的目标函数之后,会调用所有后置挂钩的目标函数.
这些钩子目标函数,事件目标函数都是在同一帧(时间)内按照不同顺序执行:
执行所有前置挂钩的目标函数→执行事件的目标函数→执行所有后置挂钩的目标函数.
事件或挂钩,都是对某种特殊函数对象的抽象化称呼,挂钩的目标函数本身也可以事件化.
开发者必须把钩子的目标函数设定为公共函数,才能被实现本机函数的插件或模块访问,调用.
稍微介绍一下amxmodx.inc的常用forward函数(详情查阅inc文件或上网查询):
// 在调用plugin_precache之前被调用,实现它,可制作native接口函数
forward plugin_natives();
// 在调用plugin_init之前被调用,实现它,可缓存额外的资源文件(模型,音效,图片),让插件可以使用更多资源
forward plugin_precache();
// 在启动服务器之后被调用,实现它,可以为插件的数据进行初始化,或添加各种钩子
forward plugin_init();
// 在玩家信息变更后被调用,比如模型,名称变更
forward client_infochanged(id);
// 在玩家开始连接服务端之后被调用(未进入服务端)
forward client_connect(id);
// 在玩家退出服务器之前,之后被调用(被踢或主动退出的区别)
forward client_disconnected(id, bool:drop, message[], maxlen);
// 在玩家彻底退出服务器,实体都被删掉后被调用.(如果只是断线,有回归的可能)
forward client_remove(id, bool:drop, const message[]);
// 在玩家发送命令到客户端前被调用
forward client_command(id);
// 在玩家进入服务器之后被调用(文件资源下载完毕)
forward client_putinserver(id);
很多头文件都声明了forward函数,各有各的用途,没事多看看.
稍微介绍一下常用于为事件添加挂钩的native函数(详情查阅inc文件或上网查询):
下面是来自amxmodx.inc的挂钩函数:
// 当插件调用指定的native函数(name)时触发目标函数(handler),用于实现native函数的具体功能,同一个native函数仅允许一个挂钩.后来者无效
native register_native(const name[], const handler[], style = 0);
// 当服务端发送指定消息(event)时触发目标函数(function),比如击杀消息
native register_event(const event[], const function[], const flags[], const cond[] = "", ...);
// 当服务端发送指定日志消息(用...参数描述你想监听的消息)时触发目标函数(function),比如回合结束消息
native register_logevent(const function[], argsnum, ...);
// 当客户端玩家发送指定客户端命令(client_cmd)时触发目标函数(function),不存在的命令也可,能用于实现新命令
native register_clcmd(const client_cmd[], const function[], flags = -1, const info[] = "", FlagManager = -1, bool:info_ml = false);
// 当客户端玩家发送指定服务端命令(server_cmd)时触发目标函数(function),不存在的命令也可,能用于实现新命令
native register_srvcmd(const server_cmd[], const function[], flags = -1, const info[] = "", bool:info_ml = false);
// 当一个老式菜单索引(menuid)所指的菜单的选项被选中时触发目标函数(function)
native register_menucmd(menuid, keys, const function[]);
下面是来自fakemeta.inc的用于添加挂钩,强制触发事件的函数:
// 当一个fakemeta所封装的游戏事件被触发,在它触发之前或之后触发目标函数(由_post参数控制前后)
// _forwardType是事件索引,可选的值被封装为以FM_作为前缀名的枚举常量保存于fakemeta_const.inc
// function是目标函数
native register_forward(_forwardType,const _function[],_post=0);
// 强行执行fakemeta所封装的指定事件
// type是事件索引,可选的值被封装为以EngFunc_作为前缀名的枚举常量保存于fakemeta_const.inc
// ...是执行事件所需参数
native engfunc(type,any:...);
// 强行执行fakemeta所封装的指定事件
// type是事件索引,可选的值被封装为以DLLFunc_作为前缀名的枚举常量保存于fakemeta_const.inc
// ...是执行事件所需参数
native dllfunc(type,any:...);
下面是来自hamsandwich.inc的用于添加挂钩,强制触发事件的函数:
// 当一个hamsandwich所封装的游戏事件被某一类实体触发,在它触发之前或之后触发目标函数(由Post参数控制前后)
// function是事件索引,可选的值被封装为枚举常量保存于ham_const.inc
// EntityClass是实体类名
// Callback是目标函数
native HamHook:RegisterHam(Ham:function, const EntityClass[], const Callback[], Post=0, bool:specialbot = false);
// 和RegisterHam类似,只不过实体类名换成了实体索引,这是因为某些实体没有amxx能获取的类名,比如Bot
native HamHook:RegisterHamFromEntity(Ham:function, EntityId, const Callback[], Post=0);
// 强行执行hamsandwich所封装的事件
// function是事件索引,可选的值被名为Ham的枚举封装于ham_const.inc
// ...是执行事件所需参数
native ExecuteHam(Ham:function, this, any:...);
// 和ExecuteHam类似,唯一不同是能够触发hamsandwich的钩子
native ExecuteHamB(Ham:function, this, any:...);
下面是来自message.inc的用于添加挂钩,强制触发事件的函数:
// 当服务端发送消息索引(iMsgId)所指消息时触发目标函数(szFunction),比如击杀消息
native register_message(iMsgId, const szFunction[]);
// 强制发送消息(由message_begin函数指定消息)
native message_end();
// 强制发送消息(由emessage_begin函数指定消息),并触发钩子
native emessage_end();
如果你想给消息相关的事件添加挂钩,
比如右上角击杀消息,回合开始/结束消息,视锥体范围变更消息,选队消息,弹药量hud刷新消息,
进度条,炸弹掉落消息,生成尸体动画,伤害类型hud,雾天,屏幕着色,屏幕震动等等.
应该详细参阅register_event register_logevent register_message函数的注释,
并上网查阅有哪些消息可以挂钩.头文件中并没有提供这些消息的具体名称,
你可以在amxmodx官方论坛询问:amxmodx官方论坛
如果你想给实体行为添加挂钩,
比如实体资源文件缓存,诞生,重生,被触发(开门,玻璃碎裂,电梯上升,按压机关),
被射击,受伤,被疗伤,被击杀,流血,被添加到玩家武器列表,思考,武器被拔出,武器被丢弃,
武器执行主要攻击,武器执行次要攻击等等.应该详细查阅hamsandwich ham_const头文件内容
如果你无所谓给某一类实体添加挂钩,应详细查阅fakemeta fakemeta_const头文件.
它们没有注释,建议前往amxmodx官网或论坛查询相关解释.
cs相关事件,比如购买武器,道具.应详细参阅cstrike cstrike_const头文件.
如果你能背诵并默写inc文档全文那就再好不过了.
没事多看看amxmodx cstrike fakemeta hamsandwich这几个头文件,
以及它们引用的其他头文件,以及引用的文件所引用的文件.
这些文件提供的接口函数足以完成绝大部分工作.
接下来的教程内容,如果出现了不认识含义的函数或参数,应该自行查阅代码引用的头文件,以及头文件引用的头文件.
即便有些函数或常量没有注释,也应前往官网或论坛查询.
建议复习入门教程中的 &
运算, !
运算,控制代码执行流程,并查阅hlsdk_const头文件.
这段代码的两个功能:
给玩家预思考事件添加前置挂钩,可以看到控制台正在狂刷的文本,每一个玩家实体都在不断触发这个事件(建议让bot参与测试).
给实体受伤事件添加前置挂钩,并使用钩子专属返回值拦截跌落受伤事件.
#include <amxmodx>
#include <fakemeta>
#include <hamsandwich>
public plugin_init()
{
register_plugin("插件名", "1.0.0.0", "作者");
// FM_PlayerPreThink是fakemeta_const.inc中的一个枚举常量,对应玩家预思考事件,是用于添加挂钩的事件索引.
// CS1.6一般都是每秒大约刷新100次屏幕画面,而玩家也会随着这个频率,每秒触发100次预思考事件(bot大约每秒25次).
// 玩家预思考事件很实用,在这里可以实时检查玩家的属性变化,或修改玩家属性.
// 只要将它用作register_forward函数的第1个参数,我们就能为玩家预思考事件添加挂钩.
// 挂钩的目标函数会接收指定事件的所有参数,充当自己的参数.
// 玩家预思考事件只有1个事件参数,就是思考者(玩家的实体索引).
register_forward(FM_PlayerPreThink, "PlayerPreThink_PreHook", 1);
// Ham_TakeDamage是ham_const.inc中Ham枚举的成员,对应实体受伤事件,是用于添加挂钩的事件索引.
// 任何实体只要受伤便会触发受伤事件,但不包括某些直接修改生命值的情况.
// 实体受伤事件的挂钩常常被用于修改伤害值,阻止受伤,统计伤害量等等.
// 只要将它用作RegisterHam函数的第1个参数,我们就能为实体受伤事件添加挂钩.
// RegisterHam函数的第2个参数,用于设定实体原始类名,只有原始类名与其相同,才能触发钩子的目标函数.
// 1.8.2或低版本amxx并没有第4个参数,该参数用于支持bot这种没有类名的玩家实体.
// 实体受伤事件会将自身所有参数(受害者,加害者,攻击者,伤害值,伤害类型位标志)传递给钩子的目标函数.
RegisterHam(Ham_TakeDamage, "player", "PlayerTakeDamage_PreHook", 0, true);
}
// PlayerPreThink_PreHook会接收1个来自玩家预思考事件的参数:玩家实体索引
public PlayerPreThink_PreHook(playerEntId)
{
server_print("实体索引为%d的玩家,触发了一次预思考事件", playerEntId);
}
// PlayerTakeDamage_PreHook会接收5个来自实体受伤事件的参数:受害者,加害者,攻击者,伤害值,伤害类型位标志
public PlayerTakeDamage_PreHook(victimEntId, inflictorEntId, attackerEntId, Float:damage, damageFlags)
{
// 如果保留同位上的1之后不为0,则设定钩子目标函数的计算结果为拥有阻止事件功能的宏定义常量HAM_SUPERCEDE,并提前退出函数
if (damageFlags & DMG_FALL)
{
return HAM_SUPERCEDE;
}
return HAM_IGNORED;
}
实体拥有不少属性都是以二进制形式储存状态,这使得一个属性能储存多个状态,节约空间.
比如pev_button pev_oldbuttons对应的按键状态位标志,总共可为16个按键储存按下或抬起的状态.
比如pev_flags对应的实体状态位标志,总共可为32种实体状态储存缺乏或拥有的状态.
它们都是int类型的属性,我们可以从pev函数的返回值得到它们,
通过下面的代码,便可以观察玩家按下按键时,按键状态发生了改变.或者玩家蹲下,游泳,落地,攀爬梯子导致实体状态改变:
#include <amxmodx>
#include <fakemeta>
public plugin_init()
{
register_plugin("插件名", "1.0.0.0", "作者");
register_forward(FM_PlayerPreThink, "PlayerPreThink_PostHook", 1);
}
public PlayerPreThink_PostHook(playerEntId)
{
// 如果思考者不是活的玩家,退出函数
if (!is_user_alive(playerEntId))
{
return;
}
// 以二进制形式显示pev函数的返回值,显示16位,32位
client_print(playerEntId, print_chat, "[AMXX]按键状态:%016b, 实体状态:%032b", pev(playerEntId, pev_button), pev(playerEntId, pev_flags));
}
比如,接下来的代码是展示如何制作一个高跳插件.
如果在此基础上对xy轴速度进行一定程度的放大,就能变成远跳插件.
如果不检查玩家是否在地上,就能变成空中多重跳插件.
再降低上升高度,还能称呼为扑翼鸟插件.
再删除检查上次按键状态条件,把上升高度降低到2至5左右.可以变成火箭飞行兵插件的核心功能.
如果仅留下检查玩家死活的条件,再将速度方向改为向前,还能变成无足鸟无限向前飞插件的核心功能.
如果仅删除检查玩家是否浮空的条件,再给予玩家指向视线前方的大速度,能变成蜘蛛侠插件的核心功能.
求生之路的smoke僵尸舌头可以把敌人拉向自己,其实是让受害者的速度强制转向攻击者,并保持一个固定大小.
通过下面的代码,可以让玩家在地面按下跳跃键的瞬间获得一个足以让自己上升100高度的速度:
#include <amxmodx>
#include <fakemeta>
new const Float:gJumpHeight = 100.0; // 跳跃高度(cs默认跳跃高度为45.0)
public plugin_init()
{
register_plugin("插件名", "1.0.0.0", "作者");
register_forward(FM_PlayerPreThink, "PlayerPreThink_PostHook", 1);
}
public PlayerPreThink_PostHook(playerEntId)
{
if (!is_user_alive(playerEntId))
{
return;
}
// 如果玩家的实体状态中不含有FL_ONGROUND(在地上)状态,退出函数
if (!(pev(playerEntId, pev_flags) & FL_ONGROUND))
{
return;
}
// 如果上一次思考时,按钮状态不含有IN_JUMP(跳跃键) 而且 这一次思考中按钮状态含有IN_JUMP
if (!(pev(playerEntId, pev_oldbuttons) & IN_JUMP) && pev(playerEntId, pev_button) & IN_JUMP)
{
// 说明检测到玩家按下跳跃键的瞬间
// pev_velocity属性代表实体在x, y, z三个轴上的速度,
// 如果从上往下看,x轴负数代表西边,正数代表东边,y轴负数代表南边,正数代表北边,Z轴负数代表下方,正数代表上方.
new Float:velocity[3];
pev(playerEntId, pev_velocity, velocity); // 获取实体的速度
velocity[2] = floatsqroot(gJumpHeight * 2.0 * 800.0); // 更改z轴速度,使其能让玩家在默认重力下跳跃到gJumpHeight所指的高度
set_pev(playerEntId, pev_velocity, velocity); // 将修改好后的速度,储存到玩家的pev_velocity属性中
}
}
接下来的代码是展示如何强制发送消息给客户端,这也会触发对应的消息事件.
cs1.6的消息机制,是服务端发送一小段数据就能让客户端自动执行一些比较复杂的工作.服务端并不清楚客户端做了什么事情.
消息机制基本上用于让客户端播放3D特效,更改屏幕HUD内容.message_const头文件中提供了58种消息的索引(SVC_开头的宏定义常量).
如果想了解更多可用的消息品种,自己想办法,上网查询(像"ScreenShake"这种消息,AMXX并没有提供参考).
通过下面的代码,可以让玩家在地面按下跳跃键的瞬间显示一个冲击波,并让自己屏幕持续抖动:
#include <amxmodx>
#include <fakemeta>
new gSprId_ShockWave; // 全局变量
new gMsgId_ScreenShake; // 全局变量
public plugin_precache()
{
// 缓存震荡波精灵图标,将其模型文件索引(bsp mdl spr都是模型文件)储存到全局变量,以便后续使用
gSprId_ShockWave = precache_model("sprites/shockwave.spr");
}
public plugin_init()
{
register_plugin("插件名", "1.0.0.0", "作者");
register_forward(FM_PlayerPreThink, "PlayerPreThink_PostHook", 1);
// 获取"ScreenShake"消息(屏幕抖动)的索引,储存到全局变量,以便后续使用
gMsgId_ScreenShake = get_user_msgid("ScreenShake");
}
public PlayerPreThink_PostHook(playerEntId)
{
if (!is_user_alive(playerEntId))
{
return;
}
if (!(pev(playerEntId, pev_flags) & FL_ONGROUND))
{
return;
}
if (!(pev(playerEntId, pev_oldbuttons) & IN_JUMP) && pev(playerEntId, pev_button) & IN_JUMP)
{
new origin[3], axis[3];
get_user_origin(playerEntId, origin);
axis[0] = origin[0];
axis[1] = origin[1];
axis[2] = origin[2] + 350; // 350表示冲击波每秒扩散距离
SendMessage_BeamCyclinder(origin, axis, gSprId_ShockWave, .life = 10, .lineWidth = 72);
SendMessage_ScreenShake(playerEntId, 15.999, 1.5, 15.999);
}
}
// 这个函数用于指定一个坐标,显示一个持续扩大的光环.
SendMessage_BeamCyclinder(const position[3], const axis[3],
spriteId, // 光环需要精灵图标,这里填写精灵图标的模型文件索引
startingFrame = 0, // 从哪一帧开始播放,这里填写帧索引
frameRate = 10, // 每秒播放多少帧
life = 1, // 光环持续事件,1表示光环持续存在0.1秒,10表示持续1秒
lineWidth = 1, // 光环高度(宽度),可参考玩家高度72,宽度32
noise = 0, // 光环扭曲程度
red = 255, // 光环红色浓度
green = 255, // 光环绿色浓度
blue = 255, // 光环蓝色浓度
brightness = 255, // 光环颜色明亮程度
scrollSpeed = 0) // 光环滚动速度
{
// emessage_,ewrite_,message_,write_系列函数,是一套用于执行发送消息事件的函数.emessage_begin和ewrite系列函数用于设定事件相关属性.
// MSG_PVS用于需要填写坐标的临时实体消息,一旦使用它,就必须填写emessage_begin或message_begin函数的第三个参数.
// SVC_TEMPENTITY是消息索引,对应的消息用于命令客户端创建临时实体,这种实体有客户端自行控制,服务端无法知道客户端做了什么,也无法察觉或操控这些实体.
emessage_begin(MSG_PVS, SVC_TEMPENTITY, position);
// 输入第一个事件参数,当消息索引是SVC_TEMPENTITY时,第一个参数表示要创建的临时实体是什么类型
// message_const.inc详细介绍了每一种临时实体类型.以及接下来需要填写的参数
// 参数有coord, short, byte等多种值类型.我只知道这里的byte和short的取值范围是0~255 0~65535
ewrite_byte(TE_BEAMCYLINDER);
ewrite_coord(position[0]);
ewrite_coord(position[1]);
ewrite_coord(position[2]);
ewrite_coord(axis[0]);
ewrite_coord(axis[1]);
ewrite_coord(axis[2]);
ewrite_short(spriteId);
ewrite_byte(startingFrame);
ewrite_byte(frameRate);
ewrite_byte(life);
ewrite_byte(lineWidth);
ewrite_byte(noise);
ewrite_byte(red);
ewrite_byte(green);
ewrite_byte(blue);
ewrite_byte(brightness);
ewrite_byte(scrollSpeed);
// 填写完毕后就可以执行事件了
emessage_end();
}
// 这个函数用于指定一个客户端玩家,令其屏幕抖动(抖动幅度,持续时间,持续频率)
SendMessage_ScreenShake(playerEntId, Float:amplitude = 0.0, Float:duration = 0.0, Float:frequency = 0.0)
{
emessage_begin(MSG_ONE_UNRELIABLE, gMsgId_ScreenShake, .player = playerEntId);
ewrite_short(floatround((1 << 12) * amplitude)); // short值类型的取值范围是0~65535,多了没用
ewrite_short(floatround((1 << 12) * duration));
ewrite_short(floatround((1 << 12) * frequency));
emessage_end();
}
对于上述例子中,两种发送消息的方式.可以简单封装成一个函数,函数内部自动计算合适的参数.调用起来更加方便.
下面代码中出现的函数,在头文件中都有注释.这里就不介绍了.
PlayerShockWave(playerEntId, Float:duration, Float:range)
{
new origin[3], axis[3];
get_user_origin(playerEntId, origin);
axis[0] = origin[0];
axis[1] = origin[1];
new life1 = floatround(duration * 10.0); // 1 life 等于 0.1 秒,他们的比值是10比1.因此duration得乘以10才能转变为可用的life
new life2 = max(life1 - 1, 1); // 第二个震荡波的生命周期比第一个少了0.1秒
new life3 = max(life1 - 2, 1); // 第三个震荡波的生命周期比第一个少了0.2秒(只是示例罢了,这些计算方法可自行更改)
axis[2] = origin[2] + floatround(range / (0.1 * life1)); // 自动计算震荡波的覆盖范围
SendMessage_BeamCyclinder(origin, axis, gSprId_ShockWave, .life = life1, .lineWidth = 36);
axis[2] = origin[2] + floatround(range / (0.1 * life2));
SendMessage_BeamCyclinder(origin, axis, gSprId_ShockWave, .life = life2, .lineWidth = 48);
axis[2] = origin[2] + floatround(range / (0.1 * life3));
SendMessage_BeamCyclinder(origin, axis, gSprId_ShockWave, .life = life3, .lineWidth = 64);
SendMessage_ScreenShake(playerEntId, 15.999, duration, 15.999);
}
接下来的代码是展示如何制作一个践踏伤害插件.
在某一个合适的时机,利用ExecuteHamB强制执行Ham_TakeDamage事件,使某个实体受伤.
并利用engfunc强制执行EngFunc_EmitAmbientSound事件,使某个坐标播放声音.
这都是很实用的功能,制作自定义技能时总会用到.
如果有看不懂的函数和参数,应该查阅头文件中的函数注释,或是常量注释.
如果对于数组不理解,应该复习入门教程中的数组章节.
如果还是不能理解,建议让bot参与注释,并使用client_print函数将实体索引与数组中的数据打印给0号实体观看.
通过下面的代码,可以让玩家在降落时造成伤害:
#include <amxmodx>
#include <fakemeta>
#include <hamsandwich>
#define dSndFilePath "garg/gar_stomp1.wav" // 将接下来代码中出现的dSndFilePath全部替换为cs1.6/valve/sound/声音文件地址
public plugin_precache()
{
precache_sound(dSndFilePath); // 缓存声音文件,只要该文件存在,接下来可以在游戏中播放它.
}
public plugin_init()
{
register_plugin("插件名", "1.0.0.0", "作者");
RegisterHam(Ham_Player_PostThink, "player", "PlayerPostThink_PostHook", 1, true);
}
public PlayerPostThink_PostHook(playerEntId)
{
if (!is_user_alive(playerEntId))
{
return;
}
// 声明静态常量:CBaseMonster类实体的专属属性-下次攻击的冷却时间-的属性索引,
// 玩家实体是CBasePlayer类,属于CBaseMonster的一个子类型,因此继承了这些属性.
// AMXX的头文件中并没有定义这些属性具体的值.只能自己上网查找:CBaseMonster_(CS)
static const m_flNextAttack = 83;
set_pdata_float(playerEntId, m_flNextAttack, 2.0); // 除非玩家客户端与服务器失联超过2秒,否则将不间断的获得2秒攻击冷却时间.永远无法开枪.
// 声明静态数组变量,这两个数组内储存的值不会因为函数退出就重置为0.
// 并且CS1.6最多只支持32个玩家,33的尺寸足以让每一名思考者都拥有自己的位置,用于储存数据.
// 思考者的实体索引将被用于访问数组中的数据.
// 这两个数组的用途:思考者脚下实体记录器, 思考者跌落速度记录器
static lastGroundEntId[33], Float:fallVelocity[33];
if (!(pev(playerEntId, pev_flags) & FL_ONGROUND))
{
lastGroundEntId[playerEntId] = -1; // 浮空时,重置思考者的脚下实体记录器
pev(playerEntId, pev_flFallVelocity, fallVelocity[playerEntId]); // 浮空时,时刻记录思考者跌落速度
return; // 浮空则提前退出函数
}
new currentGroundEntId = pev(playerEntId, pev_groundentity); // 获取思考者脚下实体
if (currentGroundEntId == lastGroundEntId[playerEntId]) // 如果上一刻与此刻的脚下实体索引相同
{
return;
}
lastGroundEntId[playerEntId] = currentGroundEntId; // 不同则登记当前脚下实体索引,下一次思考便无法通过上面的条件检测了.除非利用浮空重置.
new Float:damage = fallVelocity[playerEntId] / 5.0; // 将跌落速度除以5,当做践踏伤害.这点没必要在意,伤害想怎么写都行,这只是示例.
if (damage < 1.0) // 如果伤害低于1
{
return;
}
new Float:takeDamage;
pev(currentGroundEntId, pev_takedamage, takeDamage); // 获取脚下实体的受伤模式
if (takeDamage == DAMAGE_NO) // 如果脚下实体处于无敌模式(比如某些不能打碎但可以被C4爆破的箱子)
{
return;
}
// 强制执行受伤事件,思考者充当攻击者和加害者,对脚下实体造成震动与碎尸类型的伤害
ExecuteHamB(Ham_TakeDamage, currentGroundEntId, playerEntId, playerEntId, damage, DMG_SHOCK | DMG_ALWAYSGIB);
// 强制执行播放声音事件,fakemeta模块的engfunc dllfunc函数执行事件并不会触发挂钩的目标函数.
new Float:flOrigin[3];
pev(playerEntId, pev_origin, flOrigin);
engfunc(EngFunc_EmitAmbientSound, 0, flOrigin, dSndFilePath, VOL_NORM, ATTN_NORM, 0, 200);
engfunc(EngFunc_EmitAmbientSound, 0, flOrigin, dSndFilePath, VOL_NORM, ATTN_NORM, 0, 180);
engfunc(EngFunc_EmitAmbientSound, 0, flOrigin, dSndFilePath, VOL_NORM, ATTN_NORM, 0, 140);
}
有关更改事件参数与返回值的内容还没讲,不过这些看看fakemeta和hamsandwich头文件中的函数注释就能知道.
上面的各种代码示例可以合并为一个以高跳践踏为玩法的插件,看看就好.本篇教程到此结束:
#include <amxmodx>
#include <fakemeta>
#include <hamsandwich>
#define dSndFilePath "garg/gar_stomp1.wav"
new const Float:gJumpHeight = 200.0; // 跳跃高度(cs默认跳跃高度为45.0)
new gSprId_ShockWave;
new gMsgId_ScreenShake;
public plugin_precache()
{
gSprId_ShockWave = precache_model("sprites/shockwave.spr");
precache_sound(dSndFilePath);
}
public plugin_init()
{
register_plugin("插件名", "1.0.0.0", "作者");
register_forward(FM_PlayerPreThink, "PlayerPreThink_PostHook", 1);
RegisterHam(Ham_TakeDamage, "player", "PlayerTakeDamage_PreHook", 0, true);
RegisterHam(Ham_Player_PostThink, "player", "PlayerPostThink_PostHook", 1, true);
gMsgId_ScreenShake = get_user_msgid("ScreenShake");
}
public PlayerPreThink_PostHook(playerEntId)
{
if (!is_user_alive(playerEntId))
{
return;
}
if (!(pev(playerEntId, pev_flags) & FL_ONGROUND))
{
return;
}
if (!(pev(playerEntId, pev_oldbuttons) & IN_JUMP) && pev(playerEntId, pev_button) & IN_JUMP)
{
new Float:velocity[3];
pev(playerEntId, pev_velocity, velocity);
velocity[2] = floatsqroot(gJumpHeight * 2.0 * 800.0);
set_pev(playerEntId, pev_velocity, velocity);
}
}
public PlayerTakeDamage_PreHook(victimEntId, inflictorEntId, attackerEntId, Float:damage, damageFlags)
{
if (damageFlags & DMG_FALL)
{
return HAM_SUPERCEDE;
}
return HAM_IGNORED;
}
public PlayerPostThink_PostHook(playerEntId)
{
if (!is_user_alive(playerEntId))
{
return;
}
static const m_flNextAttack = 83;
set_pdata_float(playerEntId, m_flNextAttack, 2.0);
static lastGroundEntId[33], Float:fallVelocity[33];
if (!(pev(playerEntId, pev_flags) & FL_ONGROUND))
{
lastGroundEntId[playerEntId] = -1;
pev(playerEntId, pev_flFallVelocity, fallVelocity[playerEntId]);
return;
}
new currentGroundEntId = pev(playerEntId, pev_groundentity);
if (currentGroundEntId == lastGroundEntId[playerEntId])
{
return;
}
lastGroundEntId[playerEntId] = currentGroundEntId;
new Float:duration = fallVelocity[playerEntId] / 700.0; // 用跌落速度除以700作为震荡波持续时间
new Float:range = fallVelocity[playerEntId] * 0.15; // 用跌落速度乘以0.15作为震荡波扩散范围
new Float:damage = fallVelocity[playerEntId] / 5.0;
if (duration < 0.1 || range < 18.0 || damage < 1.0)
{
return;
}
PlayerShockWave(playerEntId, duration, range);
new Float:takeDamage;
pev(currentGroundEntId, pev_takedamage, takeDamage);
if (takeDamage == DAMAGE_NO)
{
return;
}
ExecuteHamB(Ham_TakeDamage, currentGroundEntId, playerEntId, playerEntId, damage, DMG_SHOCK | DMG_ALWAYSGIB);
if (is_user_alive(currentGroundEntId)) // 如果思考者脚下实体受伤后,是个活的玩家,给它一点屏幕抖动效果
{
SendMessage_ScreenShake(currentGroundEntId, 15.999, duration, 15.999);
}
}
PlayerShockWave(playerEntId, Float:duration, Float:range)
{
new origin[3], axis[3];
get_user_origin(playerEntId, origin);
axis[0] = origin[0];
axis[1] = origin[1];
new life1 = floatround(duration * 10.0);
new life2 = max(life1 - 1, 1);
new life3 = max(life1 - 2, 1);
axis[2] = origin[2] + floatround(range / (0.1 * life1));
SendMessage_BeamCyclinder(origin, axis, gSprId_ShockWave, .life = life1, .lineWidth = 36);
axis[2] = origin[2] + floatround(range / (0.1 * life2));
SendMessage_BeamCyclinder(origin, axis, gSprId_ShockWave, .life = life2, .lineWidth = 48);
axis[2] = origin[2] + floatround(range / (0.1 * life3));
SendMessage_BeamCyclinder(origin, axis, gSprId_ShockWave, .life = life3, .lineWidth = 64);
SendMessage_ScreenShake(playerEntId, 15.999, duration, 15.999);
new Float:flOrigin[3];
pev(playerEntId, pev_origin, flOrigin);
engfunc(EngFunc_EmitAmbientSound, 0, flOrigin, dSndFilePath, VOL_NORM, ATTN_NORM, 0, 200);
engfunc(EngFunc_EmitAmbientSound, 0, flOrigin, dSndFilePath, VOL_NORM, ATTN_NORM, 0, 180);
engfunc(EngFunc_EmitAmbientSound, 0, flOrigin, dSndFilePath, VOL_NORM, ATTN_NORM, 0, 140);
}
SendMessage_BeamCyclinder(const position[3], const axis[3], spriteId,
startingFrame = 0,
frameRate = 10,
life = 1,
lineWidth = 1,
noise = 0,
red = 255,
green = 255,
blue = 255,
brightness = 255,
scrollSpeed = 0)
{
emessage_begin(MSG_PVS, SVC_TEMPENTITY, position);
ewrite_byte(TE_BEAMCYLINDER);
ewrite_coord(position[0]);
ewrite_coord(position[1]);
ewrite_coord(position[2]);
ewrite_coord(axis[0]);
ewrite_coord(axis[1]);
ewrite_coord(axis[2]);
ewrite_short(spriteId);
ewrite_byte(startingFrame);
ewrite_byte(frameRate);
ewrite_byte(life);
ewrite_byte(lineWidth);
ewrite_byte(noise);
ewrite_byte(red);
ewrite_byte(green);
ewrite_byte(blue);
ewrite_byte(brightness);
ewrite_byte(scrollSpeed);
emessage_end();
}
SendMessage_ScreenShake(playerEntId, Float:amplitude = 0.0, Float:duration = 0.0, Float:frequency = 0.0)
{
emessage_begin(MSG_ONE_UNRELIABLE, gMsgId_ScreenShake, .player = playerEntId);
ewrite_short(floatround((1 << 12) * amplitude));
ewrite_short(floatround((1 << 12) * duration));
ewrite_short(floatround((1 << 12) * frequency));
emessage_end();
}
fakemeta和hamsandwich都是非常重要的模块,提供了相当丰富的功能.
因此我在这里,为它们写了一些中文注释.
/**
* 通过预设的事件索引.为事件添加挂钩.
*
* @param _forwardType 要挂钩的事件的预设索引,请参阅fakemeta_const.inc中FM_开头的枚举成员,它们便是用于访问对应事件所需的事件索引.
* (新手们不要学它这种不设定枚举名称的做法.完全浪费了枚举的存在意义.)
* (如果不是有注释,谁能猜到这个参数该填什么东西.)
* (如果那些FM_枚举常量拥有一个枚举名称作为tag标签,这个参数也使用了tag标签,)
* (就算没有注释,我们也能在看见标签的那一刻明白,该去寻找这个枚举,从而找到相关参数)
* @param _function 挂钩目标函数名称
* @param _post 填0表示添加前置挂钩,否则表示添加后置挂钩.
* (新手们不要学它这种不设定参数tag标签的做法.)
* (如果不是有注释,谁能猜到这个参数该填什么东西.)
* (这种只有真假,是非,是否,对错两种选择的参数,应该设定bool:标签.)
* (即便没有注释,我们也能在看见bool:标签的那一刻明白,该填true或false.)
*
* @return 返回0表示函数运行失败;
* 否则返回挂钩的索引.可作为unregister_forward函数的参数,删除挂钩
* (一个成熟的开发者应该为这种返回值和专享函数的参数添加自定义标签,)
* (这样一来,我们一看见返回值的特殊标签就会知道该去寻找专享函数)
* @error 当使用了不存在的事件索引(毕竟新手乱填完全有可能),抛出错误报告
* 当使用了不存在的挂钩目标函数(名称错误),抛出错误报告
*/
native register_forward(_forwardType, const _function[], _post=0);
/**
* 通过预设的事件索引,为某一类实体添加事件挂钩.
*
* @note 使用方法:
* RegisterHam(Ham_TakeDamage, "player", "player_hurt");
*
* @param function 要挂钩的事件的预设索引,请参阅ham_const.inc中的Ham枚举成员(这才一是个成熟的开发者应有的设计)
* @param EntityClass 实体的原始类名
* @param callback 挂钩目标函数名称
* @param post 填0表示添加前置挂钩,否则表示添加后置挂钩(一个成熟的开发者应该为这种参数添加bool:标签)
* @param specialbot 填true则对没有"player"类名的机器人实体生效,填false则不生效.
* (这才一是个成熟的开发者应有的设计,)
* (AMX Mod X 1.8.2或低版没有这个参数)
*
* @return 返回0表示函数运行失败;
* 否则返回挂钩的索引.用EnableHamForward/DisableHamForward函数可以开启或关闭挂钩.
* (函数返回带有HamHook标签的值,而开启/关闭挂钩的函数需要带有HamHook标签的值作为参数,)
* (HamHook并不是一个自定义枚举,也不是已经存在的tag标签,但依然是允许的,编译器会自动添加这个标签,)
* (这种标签不像枚举标签拥有成员,而开启/关闭挂钩的函数并不需要枚举成员,因此用这种方法创建新的Tag,)
* (这才一是个成熟的开发者应有的设计)
* @error 当使用了不存在的事件索引(比如给某个不相关的值添加Ham标签用作参数1),抛出错误报告
* 当使用了不存在的实体原始类名(名称错误),抛出错误报告
* 当使用了不存在的挂钩目标函数(名称错误),抛出错误报告
*/
native HamHook:RegisterHam(Ham:function, const EntityClass[], const Callback[], Post = 0, bool:specialbot = false);
/**
* 获取实体属性储存的数据.不论实体是哪一种类型,都拥有的通用属性.
*
* @note 函数能够返回带有整数型的数据,用引用参数输出浮点型或字符串型数据.
* 这里的引用参数指的是...符号所在位置的参数(以及后续填写的参数).
* 当你填写一个变量作为参数,变量本体会被输入函数内部,函数内部可以更改它的值.
* (更多有关于引用参数的教学内容,敬请期待)
*
*
* @param entityId 实体索引
* @param propertyId 属性索引,请查阅fakemeta_const.inc中以pev_开头的枚举常量.
* 以start和end,begin和end结尾的枚举常量不可用,它们用于表示两者之间的属性是什么值类型.
*
* @param ... 根据propertyId所指的属性值类型,需要填写不同数量,不同类型的参数:
* 属性总共有以下值类型string,edict,float,int,byte,bytearray,vecarray,string2,edict2类型
*
* string和string2需要用1个带有_:标签,尺寸不明的静态数组变量,和1个代表允许填写多少字节的常量,填入...位置,储存属性值.
* 像这样:
* new text[32]
* pev(entityId, pev_****, text, sizeof(text) - 1)
* 另外,由于实体相关的字符串都被一个全局字符串列表储存,当我们想比较它的属性是否等于某个字符串时,
* 通过检查两个字符串的索引是否相等,可避免逐一比较字符串数组中的每一个字节.
* 对于字符串类型的属性,你可以选择获取字符串的索引.有以下两种方法:
* 1: 通过返回值获取实体类名的索引
* new classNameStrId = pev(entityId, pev_classname)
* 2: 通过引用参数获取实体类名的索引,和类名
* new classNameStrId, classname[32]
* pev(entityId, pev_classname, classNameStrId, classname, sizeof(className) - 1)
*
* float需要用1个带有Float:标签的变量作为引用参数,填入...位置,储存属性值
* bytearray中的pev_controller需要1个带有_:标签,尺寸为4的静态数组变量作为引用参数,填入...位置,储存属性值
* bytearray中的pev_blender需要1个带有_:标签,尺寸为2的静态数组变量作为引用参数,填入...位置,储存属性值
* vecarray需要1个带有Float:标签,尺寸为3的静态数组变量作为引用参数,填入...位置,储存属性值
* 其余的都是通过都是通过pev函数的返回值获取
*
* @return 返回整数型属性值
* @error 如果实体索引无效,抛出一个错误报告,函数运行失败.
* 如果属性索引无效,抛出一个错误报告,函数运行失败.
* 如果参数数量,类型填写错误,抛出一个错误报告,函数运行失败.
*/
native pev(entityId, propertyId, any:...);
/**
* 设置实体属性.
*
* @note 设置字符串数据将通过AllocString自动增加到全局字符串列表(列表尺寸有限,用多了会炸).
* 如果你已经用AllocString将其加入列表,应使用set_pev_string_ptr函数代替set_pev函数,以避免重复增加.
*
* @param entityId 实体索引
* @param propertyId 属性索引,请查阅fakemeta_const.inc中以pev_开头的枚举常量.
*
* @noreturn
* @error 如果实体索引无效,抛出一个错误报告,函数运行失败.
* 如果属性索引无效,抛出一个错误报告,函数运行失败.
* 如果参数数量,类型填写错误,抛出一个错误报告,函数运行失败.
*/
native set_pev(entityId, propertyId, any:...);
fakemeta模块的作者将注释写得稀碎,很多没说清楚.我补全了注释内容,但无法确认上面的注释是完全正确的.
所以大家看看就好,不用往脑子里记.至少教程中用到的部分是正确的.
fakemeta_const.inc提供了各种枚举常量,可用作某些函数的参数,强制执行某些事件,给事件挂钩,查改实体属性.
因此fakemeta模块非常重要,可惜它提供的枚举常量大部分都没有注释.让一代又一代的开发者浪费大量时间研究它们的真正用途.
新手们不要学他这种偷懒的做法.老手也不要学.
如果有一天制作了自己的接口函数,一定要给自己制作的接口函数,相关参数的常量写上详细的文档注释.
在hlsdk_const.inc文件中,你可以看到很多宏定义常量,它们很多都是位标志.
比如pev_button相关的,IN_开头的常量.除了IN_CANCEL和IN_RUN,其它都对应了一个客户端命令,和命令绑定的按键.
#define IN_ATTACK (1<<0) // 表示二进制的0000000000000001,对应+attack命令,一般情况下绑定的是鼠标左键.
#define IN_JUMP (1<<1) // 表示二进制的0000000000000010,对应+jump命令,一般情况下绑定的是空格键.
#define IN_DUCK (1<<2) // 表示二进制的0000000000000100,对应+duck命令,一般情况下绑定的是Ctrl键.
#define IN_ALT1 (1<<14) // 表示二进制的0100000000000000,对应+alt1命令,一般情况下没有绑定按键.
圆括号中的1<<
14表示将0000000000000001向左移动14个位,停留在从右往左数第15个位.也就是0100000000000000.
假如按键同时按住IN_ATTACK和IN_JUMP对应的按键,则按键状态中储存的是0001 &
0010,也就是0011(对应十进制的1 +
2 =
3).
将按键状态属性所储存的值,和单一按键标志进行 &
运算,计算结果保留同位上的1.如果等于该按键标志,说明玩家正按着这个按键.
比如玩家正按着IN_ATTACK和IN_JUMP,按键状态为0011,与IN_JUMP进行 &
运算, 0011 &
0010保留同位上的1,等于0010,等于IN_JUMP.
简单的说,每当实体按住一个按键,该按键对应的位置就会变为1,否则变为0.
pev_button pev_oldbuttons分别代表此次与上次预思考事件中,实体的按键状态.
配合使用,可检查按钮的四个状态:按下瞬间,按住时,松开瞬间,未使用:
public PlayerPreThink_PreHook(playerEntId)
{
if (!is_user_alive(playerEntId))
{
return;
}
// 将此次函数计算结果保存下来,可避免重复使用函数进行计算
new oldButtons = pev(playerEntId, pev_oldbuttons);
new buttons = pev(playerEntId, pev_button);
// 注:&的优先级高于&&,因此左右两边的&运算会先执行
// 如果 上次不含有IN_USE状态 且 此次含有IN_USE状态
if (oldButtons & IN_USE == 0 && buttons & IN_USE)
{
client_print(playerEntId, print_chat, "[AMXX]E键被按下");
}
// 如果 上次含有IN_USE状态 且 此次含有IN_USE状态
if (oldButtons & IN_USE && buttons & IN_USE)
{
client_print(playerEntId, print_chat, "[AMXX]E键一直被按压");
}
// 如果 上次含有IN_USE状态 且 此次不含有IN_USE状态
if (oldButtons & IN_USE && buttons & IN_USE == 0)
{
client_print(playerEntId, print_chat, "[AMXX]E键被松开");
}
// 如果 上次不含有IN_USE状态 且 此次不含有IN_USE状态
if (oldButtons & IN_USE == 0 && buttons & IN_USE == 0)
{
//client_print(playerEntId, print_chat, "[AMXX]E键未被使用");
}
}
灵活使用else可以避免重复执行相同的条件表达式,还能省略** == 0的运算.
public PlayerPreThink_PreHook(playerEntId)
{
if (!is_user_alive(playerEntId))
{
return;
}
new oldButtons = pev(playerEntId, pev_oldbuttons);
new buttons = pev(playerEntId, pev_button);
if (oldButtons & IN_USE)
{
if (buttons & IN_USE)
{
client_print(playerEntId, print_chat, "[AMXX]E键一直被按压");
}
else
{
client_print(playerEntId, print_chat, "[AMXX]E键被松开");
}
}
else
{
if (buttons & IN_USE)
{
client_print(playerEntId, print_chat, "[AMXX]E键被按下");
}
else
{
//client_print(playerEntId, print_chat, "[AMXX]E键未被使用");
}
}
}