前言

我们这一章节将会再深入了解amxx插件的开发,了解实体创建createentity、实体碰撞touch和实体思考think等内容。有疑问可以在文章底部留言。

CreateEntity创建实体

创建实体有什么用

游戏中创建实体,可以做各种各样的事情,比如AI(NPC),导弹(或追踪导弹),带抛物线的榴弹,以及各种各样的实体类武器,比如弓弩射出去的带延迟飞行的箭

虽然上面看上去可以实现许多不同的功能,但我们必须要从最基础的实体开始创建

基础创建实体的方法

我们需要用到的是include/fakemeta_util.inc这个模块的fm_create_entity函数

代码看起来像这样

1
fm_create_entity("实体名")

其中实体名部分是需要创建什么实体,一般来说创建的实体名,如果是自定义的第三方实体,那么一般是info_target

所以代码就是这样

1
fm_create_entity("info_target")

现在我们假设已经创建了实体,可是应该怎么设置呢?

这时候我们需要用到之前学到的变量来存储这个实体

1
new ent //新建变量,名字为 ent

这样的一个定义,即新建了一个变量
那么我们应该怎么存储下来?

配合=就可以让这个变量存储指定的东西

1
2
//将 ent 变量存储为 `创建的这个实体`
new ent = fm_create_entity("info_target")

除了基础创建部分,还需要加入一个判断,来判断是否有效以及无效

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//判断写在 `参数的那个东西` 是否是有效的
//如果无效直接使用会造成游戏崩溃,以及别的一些什么
//返回值为 (1=有效,0=无效)
// 代码为:
pev_valid(实体)
//判断有效
//不需要特地加个 `== 1` ,因为默认就是1
if( pev_valid(实体) )
{
}
//如果判断无效的话,在`if`判断的括号那加个 `!` 反义符号即可
if(!pev_valid(实体) )
{
}

所以代码就是这样

1
2
3
4
//新建 ent变量,用来存储创建的那个实体
new ent = fm_create_entity("info_target")
//如果ent变量中的这个实体是无效的话 那么返回,不继续往下执行
if(!pev_valid(ent)) return

当然,这样只是创建了一个实体,它不具备任何功能,所以如果编译成为插件后,触发这个代码部分,在游戏中也不会显示出来,也没有任何变化

SetModel设定实体外观

我们现在已经创建出来了一个实体,但是它不具备任何外观,所以在游戏中是看不见的

那么我们需要做什么?才能让他在游戏中显示出来
那就是赋予外观,通俗的说,他没有模型,当我们赋予实体模型后他就能看见了

所以我们把上面的代码,原封不动的写到这里

1
2
new ent = fm_create_entity("info_target")
if(!pev_valid(ent)) return

现在要做的就是让这个实体,能在游戏中看见

赋予模型的代码可以从多个模块里面获取到,这里我们用fakemeta模块来着重讲解,其代码为:

1
EngFunc_SetModel

这个代码是用来设置模型的(模块引用于fakemeta.inc)

我们可以用SetModel来修改模型,但对于玩家本身是没有效果的,常用于自定义的第三方实体,也可以修改一些特定实体(但是对于地图中的玻璃似乎没效果)

通过EngFunc_SetModel来修改模型,很简单。

1
2
3
4
//修改模型(SetModel)
//参数1:实体
//参数2:需要设定模型的路径
engfunc(EngFunc_SetModel, 实体, "模型路径")

其中,模型路径不是特指的models/, 也包含sprites/

我们以手雷模型(路径为:models/w_hegrenade.mdl) 做演示,可推出代码为

1
engfunc(EngFunc_SetModel, 实体, "models/w_hegrenade.mdl")

当我们把之前的创建实体部分组合后,就会变成下面这样

1
2
3
new ent = fm_create_entity("info_target")
if(!pev_valid(ent)) return
engfunc(EngFunc_SetModel, ent, "models/w_hegrenade.mdl")

这样就会赋予实体正确的模型了

需要注意:不论如何模型,都需要缓存(Precache)再用,如果不缓存直接设定模型的话,会导致游戏报错出现No Precache(未缓存)的致命错误,大家写代码的时候要注意这一点。

set_pev设定实体坐标

此时,我们很有可能看不到我们的实体。为什么呢?因为这个实体没有设定坐标,没有设置坐标的实体 他们的初始坐标都是0轴原点,即:

1
`X`: 0.0  `Y`:0.0  `Z`: 0.0

所以我们必须设置一个能让我们看见的坐标才可以

坐标设定代码看上去像这样

1
set_pev(实体, pev_origin, 坐标三轴[X][Y][Z])

那怎么确定坐标?

比如要获取我们玩家自己的所在位置可以这样写

1
2
new Float:org[3]
pev(id, pev_origin, org)

其中通过数组org所存储的[0],[1],[2]也就是坐标基础系轴

那么我们稍稍延伸一下,如果这样可以获取到当前我们所在的位置,那么我们可不可以 把实体也设置到这个坐标来?

看上去这样的思路没什么问题,但是实际我们会遇到一个问题

这个实体和玩家卡在一起了,因为实体的出生坐标是取定于玩家所在位置

那么我们怎么解决?

虽然看上去这样会冲突,那么其实我们只需要把坐标稍稍向前或者偏移一点就可以让实体创建后不和我们自己卡在一起

至于怎么偏移,方法有很多,这里举两种简单的方法

方法(1):向自己瞄准的方向进行偏移

这里我们会用到velocity_by_bim来计算一个向量

1
velocity_by_aim(目标索引, 长度, 输出的向量)

这怎么理解?

  • 首先目标索引是代表需要让谁来计算这个,因为是我们自己所以这里可以写成id(也可以是其他,只要自己记得住就行)
  • 中间的参数是需要填写一个向量长度,比如200,300什么的,但是由于服务器参数sv_maxvelocity的限制,所以尽量不要超过2000
  • 第三个参数,其实也就是输出,它输出的方式和pev_origin差不多,也是三维数组

所以,如果我们要计算一个,200长度的前方偏移向量可以这样写

1
2
new Float:vel[3]
velocity_by_aim(id, 200, vel)

其中导出的vel,就是我们需要的偏移向量

但仅仅这样还不够,因为这只是偏移向量,我们需要的最终目的是偏移后的坐标

所以我们还需要让坐标和偏移进行相乘

我们可以用xs_vec_add来完成数组相乘

这个模块在include/xs.inc,以后经常会用到的

如果我们需要用vel和org这两个数组来相乘,代码看上去像这样

1
xs_vec_add(vel, org, vec3) //相乘以计算偏移 vec3 得出结果

上方演示所用到的vec3其实可以不用写,因为那个代表最终输出,其实可以直接写成org,因为最终计算后需要使用的就是org

简单的代码看上去是这样

1
xs_vec_add(vel, org, org) //相乘以计算偏移 org 得出结果

那么,我们把上面的代码进行组合可以得到如下

1
2
3
4
5
6
7
8
9
new ent = fm_create_entity("info_target") //创建 info_target 实体
if(!pev_valid(ent)) return //如果无效就返回
new Float:org[3]
pev(id, pev_origin, org) //取玩家自己的坐标存储到 org 数组变量中
new Float:vel[3] //新建数组变量 vel ,用来存储计算后的偏移
velocity_by_aim(id, 200, vel) // 通过玩家瞄准方向,计算200的偏移长度,将结果存储到 vel 变量中
xs_vec_add(vel, org, org) //将 偏移后的vel 和 玩家当前坐标进行相乘,得到偏移后的坐标坐标(也就是玩家瞄准方向再加200长度的一个点)
set_pev(ent, pev_origin, org) //设定实体到这个位置
engfunc(EngFunc_SetModel, ent, "models/w_hegrenade.mdl") //为实体设定一个外观

这个方法1看上去可能会稍稍复杂一些

方法 (2):直接偏移

这次,我们可以不用velocity_by_aim,我们可以用取到的org数组中的值来直接加一点偏移

1
2
new Float:org[3]
pev(id, pev_origin, org) //获取id索引的坐标系位置,将结果存储到 org数组中

上面这样是一个标准的坐标系获取方法,那么我们怎么偏移呢,因为方法2不用到velocity_by_aim,我们知道坐标是由三个数组组成的,所以我们可以直接将某个数组里面的值加一点或者减去一些

代码看上去是这样

1
2
3
new Float:org[3]
pev(id, pev_origin, org) //获取id索引的坐标系位置,将结果存储到 org数组中
org[1] += 33.0 // org[1] 代表 XYZ轴中的 Y 轴,对Y轴加一点值,以产生偏移

现在就可以得到一个 已经被偏移的org坐标数组,虽然没有偏移多少,但是已经不会和玩家处于同一个坐标,所以不会卡住

所以完整代码是这样

1
2
3
4
5
6
7
new ent = fm_create_entity("info_target")
if(!pev_valid(ent)) return
new Float:org[3]
pev(id, pev_origin, org) //取玩家自己的坐标存储到 org 数组变量中
org[1] += 33.0
set_pev(ent, pev_origin, org) //设定实体到已经偏移的坐标系中
engfunc(EngFunc_SetModel, ent, "models/w_hegrenade.mdl") //为实体设定一个外观

这样我们就可以做出一个没有任何功能,但是可以看见的实体,外观被赋予是手雷,而且由于我们没有设置pev_solid所以在游戏中测试中,这个实体是可以被玩家穿过去的,也就是看得见摸不着的存在

设置实体模型尽量在pev_origin之后进行,最大限度避免创建出来看不见的问题

Touch碰撞

我们会在游戏中看见一些的碰撞效果,比如:手雷扔出去后撞击在障碍物上弹开

这里我们用fakemeta模块来演示

1
2
3
4
public plugin_init()
{
register_forward(FM_Touch, "碰撞事件触发函数")
}

碰撞触发的函数中,需要两个参数

我们用弩箭来飞出去撞击到目标来表示这两者的关系:

  1. 弩箭实体
  2. 弩箭实体撞击到了谁

所以代码是这样

1
2
3
4
5
6
7
public plugin_init()
{
register_forward(FM_Touch, "碰撞事件触发函数")
}
public 碰撞事件触发函数(实体, 被撞击者)
{
}

我们用之前学到的创建实体,来做个实体撞击效果吧

首先我们上面的代码抄下来

这里我们不用到方法1或方法2的偏移,我们直接使用我们自己的坐标

那你可能会说,这样做不是会让实体和玩家卡在一起吗
没错,所以这里我们要用另一个解决方法,在不偏移坐标的情况下,让实体正常出现,并且不和玩家卡住

这里我们要用到的是pev_owner的owner翻译过来的话是主人

也就是我们需要为实体设置一个主人,只要任何一个实体有了owner就不会和主人碰撞,如果那个实体有阻挡效果,作为主人的你也可以轻易穿过去

那应该怎么写?

很简单,为实体赋予主人是你自己即可

1
set_pev(ent, pev_owner, id)

当然,仅此这样还不够。为了做出碰撞效果我们还需要设置pev_solid也就是阻挡类型

pev_solid有几个参数,我们在设置的时候可以直接设置这些参数

1
2
3
4
5
6
7
8
9
10
//对其他实体没有任何作用,例如不碰撞
SOLID_NOT = 0
//与实体边缘发生碰撞效果,但不会挡住实体
SOLID.TRIGGER = 1
//与实体边缘发生碰撞效果,挡住实体
SOLID.BBOX = 2
//与实体边缘发生碰撞效果,但这个实体必须不在地上(被撞的那个实体)
S0LID_SLIDEB0X = 3
// BSP clip,与实睞的边缘发生碰撞效果,挡住实体 注:BSP dp似乎是可以穿过一些固阼,例如仓库门口对面的[中间有好多洞]的门(桥下方)
SOLID.BSP = 4

那么如果我们要写个,可以产生碰撞并且和主人不会发生碰撞的效果应该怎么写?

我们需要用到SOLID_BBOX他可以产生阻挡效果,大部分导弹或者榴弹实体都是用这个

注意:SOLID_BBOX必须全部大写。当然,如果你觉得记住这么多英文太麻烦,也可以直接用他所对应的数字来表示,它对应的是2

标准写法:

1
set_pev(ent, pev_solid, SOLID_BBOX)

比较容易的写法:

1
set_pev(set_pev(ent, pev_solid, 2))

仅此而已就可以做出一个阻挡的实体了吗?

并不是这样,还需要为这个实体设置一个大小,才可以产生真正的碰撞效果,设想一下,如果一个实体没有大小,即使他属性是可以碰撞,那么也撞不到什么东西

数组一共有三个,也就是[0], [1], [2]

如果用代码表示可以是这样

1
2
3
4
new Float:MinSize[3]
MinSize[0] = -1.0
MinSize[1] = -1.0
MinSize[2] = -1.0

但我们通常不这样写,这样会出现多行代码
像下面这样是标准写法

1
engfunc(EngFunc_SetSize, ent, {-1.0, -1.0, -1.0}, {1.0, 1.0, 1.0})

怎么理解?

比如玩家的大小是 16 16 36

那么如果用负和正来表达就是

  • 负 -16.0, -16.0, -36.0
  • 正 16.0,16.0,36.0

当然为了准确性,必须添加小数点以成为浮点值

1
engfunc(EngFunc_SetSize, ent, {-16.0, -16.0, -16.0}, {16.0, 16.0, 36.0})

注意{}和()的区别,不要写反了,不然编译器会报错

这个就是碰撞的实体?

是的,不过显然我们要做的碰撞面积没有那么大
我们可以在这个范围上缩小一些

1
engfunc(EngFunc_SetSize, ent, {-1.0, -1.0, -1.0}, {1.0, 1.0, 1.0})

你可能也注意到了,参数组内的{}负和正的值大小范围都是一致的
是的,这样就是一个标准的正方形,对于碰撞的物体来说,这样显然是很完美的

注意:前面是 -1.0, -1.0, -1.0 ,那么后面也应该是 1.0, 1.0, 1.0
当然也有例外,但是不能前面为正数开头

如果像下面这样写,在编译时候不会出现错误,但是游戏中创建实体的时候,游戏会崩溃出现一个错误对话框(Box Min/Max)什么的,然后游戏崩溃。

1
engfunc(EngFunc_SetSize, ent, {1.0, 1.0, 1.0}, {1.0, 1.0, 1.0})

所以要注意一下值,不要写错了。

那么我们把上面的代码和当前所讲到的进行组合,就可以得出下面的代码

1
2
3
4
5
6
7
8
9
10
new ent = fm_create_entity("info_target")
if(!pev_valid(ent)) return
new Float:org[3]
pev(id, pev_origin, org) //取玩家自己的坐标存储到 org 数组变量中
set_pev(ent, pev_origin, org) //设定实体到已经偏移的坐标系中
set_pev(ent, pev_solid, 2) // 设定实体的穿透类型,这里的2 代表 `SOLID_BBOX` ,只要能记住那么为了省事可以这样写数字
set_pev(ent, pev_owner, id) //设定实体的主人为 `id` 也就是 主人为 `谁创建了这个实体` 那个人
engfunc(EngFunc_SetModel, ent, "models/w_hegrenade.mdl")
//为实体设定一个外观
engfunc(EngFunc_SetSize, ent, {-1.0, -1.0, -1.0}, {1.0, 1.0, 1.0}) //为实体设定一个大小

为实体设置的属性完成了?

看上去是的,不过这个实体只是拥有碰撞属性,而且我们也没有设置 pev_movetype所以默认是0,换句话说,这个手雷会浮在空中,不移动,既然他不移动,换句话说

在被其他物体撞击这个手雷实体的时候才会触发Touch(碰撞),所以我们需要想

提示:设定实体大小尽量在设置模型外观之后进行,不然有可能会出现实体大小异常,比如很小或者很大

Think思考

当一个物体有了自己的思考,那么他就可以移动了,这样想的话的确不错

可是我们应该怎么让这个物体拥有思考

这里用fakemeta模块来做示范

代码为FM_Think,是一个回调钩子,我们可以用register_forward来勾住他

像这样即可:

1
2
3
4
public plugin_init()
{
register_forward(FM_Think, "EntityThink")
}

和碰撞不同的是,思考只有一个参数,所以代码是这样

1
2
3
4
5
6
7
public plugin_init()
{
register_forward(FM_Think, "思考触发函数")
}
public 思考触发函数(实体)
{
}

因为每个实体都有自己的实体名,所以这里我们需要添加一个判断
判断是不是由那个实体的思考

1
2
new class[33]
pev(ent, pev_classname, class, sizeof(class)-1) //获取ent的实体名并存储到class数组变量中

判断字符串的方法是用equal

equal是用来不区分大小写判断字符串的,他有两个参数需要填写

  1. 获取什么数组中的字符串
  2. 字符串内容是什么呢

比如我们要判断一个 abc的实体,配合上面的 class 数组变量 我们可以写出如下代码

1
2
3
4
5
new class[33]
pev(ent, pev_classname, class, sizeof(class)-1)
if(equal(class, "abc"))
{
}

说到这里,我们可能发现我们所创建的手雷实体,没有实体

那我们应该怎么设置实体名?

很简单,在创建部分中添加一个

1
set_pev(ent, pev_classname, "shoulei") //引号里面的内容,根据你自己的喜爱决定

我们所创建的实体的实体名,必须要和FM_Touch或FM_Think那边所触发的函数中,判断中的equal第二个参数相吻合,不然我们可能会发现,没有撞击效果或者没有思考等问题

该系列文章到此就完结了,其实代码是不够完善的,欢迎大家留言讨论。

参考文献

Lv.6 高级·轻 — AMXX插件编写教程