本篇文章只是作为理论说明,让大家对amxx插件开发有个初步的认识,并不具备实践效果。如果按照本篇文章来操作的话,可能会出现一些错误。

什么是AMXX插件

AMXX插件即Amx Mod X插件,是基于半条命Half-Life这款游戏开发的可扩展的模块,可安装到到反恐精英cs1.6上,可通过编写插件来拓展原本反恐精英cs1.6的功能。例如:

  • 修改皮肤
  • 会员系统
  • 火箭筒、导弹、追踪类导弹
  • 游戏数据修改(生命值、攻击力、防御力等)
  • 新增武器

插件核心模块

什么是插件的核心模块?核心模块是插件开发的基础,我们可以利用这些基础代码来我们自己的插件。比如说建立大楼需要有地基,那么插件也是一样,如果没有核心模块就无法安装插件。

使用控制台进行测试

  1. 打开 反恐精英cs1.6
  2. 以任意地图创建房间,然后进入游戏中
  3. 按~键 打开控制台,接着输入amxx

接下来你会看见控制台返回amxx的信息

前提是你的cs1.6客户端安装了amxx插件,纯净版的cs1.6是需要另外自己安装amxx插件的

返回数据

通常,控制台会返回的信息只有两种

  • 无效的命令/无反应
  • 有反应

控制台检查模块是否安装

显示无效的命令或无反应

这表示反恐精英cs1.6没有安装插件。这时,你就需要自己安装了。

  1. 首先到官方网站下载插件安装包
  2. 找到反恐精英cs1.6游戏所在文件夹(如D:/cs1.6)
  3. 将压缩包里面的文件替换到D:/cs1.6/cstrike,选择全部替换

至此,你就完成了amxx核心插件模块的安装了,当然,官方提供了很多种安装方法,可自行查阅选择合适的安装方式

显示有反应

这说明反恐精英cs1.6的amxx插件模块已经安装完成。接下来你可以安装单独的插件了,安装独立插件的这个过程分为两步:

第一步 -> 放置插件

打开cs1.6安装目录然后找到cstrike文件夹

在上述位置找到并打开addons/amxmodx/plugins文件夹

将插件(后缀名 .amxx)的文件放入上述文件夹中

第二步 -> 修改插件配置文件

打开cs1.6然后找到cstrike文件夹

在上述位置找到并打开addons/amxmodx/configs文件夹。然后找到plugins.ini配置文件

使用记事本打开配置文件,在该文件中最后一行通过回车新建一行写入插件的名字

例如:111.amxx这样的名字

注意:配置文件中,插件的名字不能写中文名,不然会无法读取插件。

我们知道插件可以安装进去,可是插件是怎么得到的?

这里我们接下来就需要先了解一下编译器了

插件与由来

我们知道AMXX是插件文件,可是他是怎么来的呢,你也许会说:“这么简单,网上下载就是”,那有没有想过网上的插件是怎么来的呢?

其实,任何插件都是通过编译器得到的。

什么是编译器?

编译器就是将我们写的程序,比如用C, JAVA, C++, 汇编等语言写的程序,经过编译器的转换,把这些语言转换成计算机或者微型处理器能够识别的机器代码,它是由0和1以组成的序列,说白了,就是相当于英语翻译成中文的工具一样。

如何获取编译器?

在addons/amxmodx/scripting中,有amxxpc.exe和compile.exe这两个文件,这个就是编译器了

编译器如何使用?

  1. 首先确定源代码和编译器处于同在目录的状态,反之请复制源代码文件到编译器目录中
  2. 将源代码文件(.sma)用鼠标拖拽到编译器目录下的 compile.exe 文件上,然后放开鼠标
  3. 若成功编译,你会在编译器目录看见 compiled 文件夹。
  4. 在这个文件夹中就可以找到你编译后的插件

代码组成与结构

主结构

每个源代码中由不同的代码块组成,大致包含以下内容:

  • 模块定义
  • 变量
  • 函数
  • 事件
  • 判断
  • 表达式以及运算符

模块定义

Include头文件

头文件定义通常在源代码中的最顶部,关键字为include<模块名>的内容,这就是模块定义。

他至关重要,是整个源码的心脏。插件的代码由头文件所定义的代码来编写,不在头文件包含的代码,是无法被编译器编译的。

通常常见为:

1
#include <模块名>

插件使用什么模块中的代码进行工作,就在这里定义

常用模块:

  • amxmodx
  • fun
  • engine
  • fakemeta
  • hamsandwich
  • cstrike

常用模块解释

AmxModx

基础模块,必须定义。

他内置了大量代码来让开发者无比强大

代码:

1
#include <amxmodx>

Fun

这个模块比较适合新手,可以用简单的代码实现常用效果

代码:

1
#include <fun>

Engine

不太常用的模块,他提供的代码非常复杂,不推荐使用这个

代码:

1
#include <engine>

FakeMeta

这个模块我们简称为FM(取自FakeMeta的大写首字母)

可实现绝大部分的功能,是一个非常常见的模块

代码:

1
#include <fakemeta>

HamSanwich

此模块我们简称为Ham(取Hamsandwich的前三个字母)

可实现绝大部分的功能,是一个常用的模块

代码:

1
#include <hamsandwich>

cstrike

可实现大部分功能,是一个比较常用的模块

1
#include <cstrike>

变量

变量是对“值”的引用,使用变量等同于引用一个值。每一个变量都有一个变量名。

1
new a

上面的代码声明新建变量a

变量类型

为了方便理解,我们可以把变量想象成一款水杯。

当然并不是所有水杯都一样,比如 “玻璃杯”,“不锈钢杯”,等等。

所以变量也分为几种可定义的类型,分别是:

  • 临时变量 (temporary)
  • 全局变量 (global)
  • 布尔变量 (bool)
  • 浮点变量 (Float)

临时变量

这是一种在函数内部的变量,通常其他函数无法读取

临时变量看起来像这样:

1
2
3
4
public test( )
{
new abc
}

上述例子中的abc即为临时变量,如果不特殊处理。将无法被其他函数获取值

全局变量

全局变量不同于临时变量,他可以被其他函数所获取,通常用于变量判断,如:是否这局已经使用过了、当前累计的造成的伤害是多少、保存以及存储的坐标等等。

这种变量通常写在注册部分的上面,并且不处于函数内部

一般看起来像这样:

1
2
3
4
5
6
#include <amxmodx>
new LaveSpace //定义的全局变量
public plugin_init()
{
register_plugin("test", "1.0", "Test Plugin")
}

常见位置处于#include和plugin_init()的中间

上面的LaveSpace就是全局变量

而除了上面两种变量类型,还有其他一种变量类型可以来组合搭配

浮点变量

除了临时变量和全局变量,这两种外。

还有一种叫浮点变量,他可以配合临时变量和全局变量来使用,是一种组合型的变量。

浮点是很常用的一种变量类型,和普通变量的区别是:这种变量他只能存储浮点数(带小数的数,比如 12.5)

代码看起来像这样:

1
new Float:变量名

技巧:浮点变量标记Float的大小写不要写错了,不然会出现编译错误。

布尔

这种变量又不同于浮点变量,但他可以以临时变量或全局变量的形式存在。

此变量仅有两个标记方式:

  • true
  • false
1
new bool:变量名

bool变量的标记方式

true代表是的、真

false代表否定、假

我们可以通过下面的代码进行标准型判断

1
2
3
4
5
6
7
8
9
10
11
12
new bool:atp
public aa()
{
//如果atp变量为真
if(atp == true)
{
}
//反之如果atp变量为假
else if( atp == false)
{
}
}

注意:
布尔变量和浮点变量是独立的两个变量类型。
要么用布尔变量,要么用浮点,仅仅只能选择一种

函数

什么是函数

函数的英文名为 function,也就是功能。

打一个简单的比方:

在超市买散装食品的时候,你需要一个口袋,将口袋把散装食品装起来,然后去称称。然后就可以得到一个标签,贴在这个口袋上,收银员会扫描这个标签上的条形码,来获取称称时的价格。

理解方法:散装食品我们就可以理解为许多许多的单个代码,而口袋就是将单个代码封装了起来,变成了一个方便拿走的东西(即功能)。

在源代码中,函数一般以public开头,例如:

1
public 函数名()

同理,函数名也可以像变量名那样由你自己去定义。

函数类型

函数可简单分为三种类型

  • 公用函数(public)
  • 非公用函数
  • 待调用函数(stock)

公用函数

这是很常见的函数类型。

比如说:

1
public 函数名()

函数名可以由你自己去写,但需要你自己记得住

非公用函数

这种变量是一种比较特殊的变量

他不同于公用函数,这种变量没有public定义部分

他的代码仅仅是:

1
函数名()

有疑惑?不用担心,这种方式比较少见,一般不这样写

待调用函数

这是一种特殊的函数,他不同于非公用函数和公用函数
他只在被其他代码调用的时候才被读取,除此之外他都以注释代码段的方式存在。

1
stock 函数名()

注意:

  • stock函数的内容,只在有其他函数调用的时才会被读取并触发。
  • stock函数在没有被触发的时候,即使里面有错误也不会影响编译。

索引(index)

什么是索引

就如一个人的名字一样,可以让你知道“他”是谁

索引是用于函数传递信息用的一种标识

一般由“触发者”所引用,他的位置常见处于函数名字后面的括弧内

1
public test(索引)

对了,有时索引不止一个

因为可能函数会同时传递多个参数,所以不止一个。

比如说:

1
2
3
public addhealth(id, health)
{
}

提示:索引又可以称为参数,但参数不一定是索引。

字符串

字符串或串(String)是由数字、字母、下划线组成的一串字符

在代码中,含有””(双引号)的内容就是字符串

比如说:

1
2
3
4
5
"你好"
"Hatsune Miku"
"厉害了我的哥6666"
"你在这局已经购买了这个道具,无法再次购买"
"139659650"

提示:对于字符串没什么太多限制,不过要注意的是,字符串必须两边都有”” (双引号),不然会造成编译错误。

数组

什么是数组

数组是一种存放一组值的集合,这组值可以是数字、字符串或者其他东西

之前我们说了变量,可以用水杯来代替,那数组怎么通过这种方式理解呢

那就是水杯里面装的水。

水装的多少可以理解为数组内的大小

数组中可以存储值,而数组又是在变量基础上来存储的,所以必须要有变量才能存储数组。

数组的作用是来存储每个玩家或每个实体的单独数据。

为什么要单独存储?因为如果仅仅用一个变量来存储所有玩家的数据,那么就会导致,上一个玩家的数据被下一个玩家的数据所覆盖,这是无论如何也不想发生的事情。

这时候就要用到数组来解决

一个变量可以存储一名玩家的数据,如果想存储多个玩家的数据,并且同时使用这个变量,就会为这个变量添加数组来存储。

数组是什么样子

在代码里,在变量命中后面写上[],视为数组。
数组一般和变量共同使用,他的作用是让“同一个变量存储更多的东西”

例如:

1
new plane[]

上例代码中[]就是数组的容器,
这个容器可以设定一个大小,比如:[3]

数组的大小

数组是一个从0开始的容器,在[]里面设定的值叫数组最大值

例如:

1
ArrayTest[3]

这个变量中的数组,最大值是3

所以数组从0开始,包含了[0],[1],[2],[3]

函数传递及触发

什么是传递

在学校运动会中,有一种比赛叫:接力跑,由一名同学跑完规定一圈后,将手中的棒子传递给下一个待跑的同学,这位同学接到之前同学拿过来的棒子后,就会开始奔跑,为胜利而战。

就如接力棒一样,函数之间可以传递一些数据,让其他函数跟着执行未完成的任务。

比如说:

1
2
3
4
public aaa()
{
bbb()
)

即aaa函数触发了bbb函数

什么是触发

像我们家里电灯开关一样,当我们打开开关的时候,电灯会发亮,反之会熄灭。这就是电灯开关触发了电灯泡,让其开启或关闭。

那在反恐精英cs1.6这款游戏中,有什么可以明显看出的触发吗?

有的,比如说:在 cs_assault(72街仓库)这张地图中,作为CT的你去解救人质,进入人质房后,对着人质按“E”键,他们便会跟随你。

除此之外,有一些地图中,会有一种开关按“E”才能开门或开灯。

也比如:一些道具插件,输入某些命令才能打开菜单或者得到道具。

一些相同,一些不同

触发的方式和传递无比接近,但传递是由前一个函数传递给另一个函数,而触发时由一个命令或者一个行为来传递

判断以及表达式

在代码中可以做各种判断,来实现你需要的表达

判断是以if开头然后接上括号,在括号内的内为表达式

1
if(表达式)

基础表达式

表达式 释义
&& 并且
> 大于
< 小于
>= 大于或等于
<= 小于或等于
!= 不等于
== 绝对等于

例如:

若要判断World变量大于4并且小于10

代码为:

1
2
3
if( World > 4 && World < 10 )
{
}

若要判断Earth变量大于6或者小于10

代码为:

1
2
3
if( Earth > 6 || Earth < 10 )
{
}

运算符

代码中的运算符就像计算器中的加减乘除一样

常用运算符

运算符 释义
+ 加上
- 减去
* 乘以
/ 除以
+= 加上指定值
-= 减去指定值
*= 乘以指定值
/= 除以指定值

比如要表达5 + 4 > 6这种判断,可配合判断写出如下代码

1
2
3
if( 5 + 4 > 6 )
{
}

大括号组合

在代码中,当函数里的内容超过一行,这时候我们需要用到大括号{}把内容包围起来。

比如说:

1
2
3
4
5
public test (id)
{
new hello
next_function()
}

但如果在一个函数中出现了多个条件,这种情况下括号就需要出入的括号数量统一

出入括号统一

1
2
3
4
5
6
public test(id)
{ //入1
if(abc)
{ //入2
} //出1
} //出2

在上述示例中,有两个{号,所以结尾也要两个},也就是说需要将代码内容完全包围起来

代码注释

以//开头直到行末的字符被视为行注释,被注释的代码编译器会自动忽略

1
2
//这是一行注释
if(!is_user_alive(id)) //这也是注释

除了行注释,还有一种是用/*...*/将多行代码包裹起来,把一大块视为一个注释

1
2
3
4
/* 从这里开始是块注释
仍然是注释
仍然是注释
注释结束 */

插件初始化

每个插件在游戏载入地图的时候,会初始化。
所触发的函数就是plugin_init,我们可以在这个初始化函数中写上需要为这个插件所注册的信息

通常我们需要注册的基本信息就是,“插件的名字”,“版本号”,”作者名字”

这里我们要用到下面这个代码:

1
register_plugin(注册的信息以及参数 ) //基本信息注册

这个代码拥有三个可供填写的参数,他们以字符串存在,并且每个参数会以,逗号来隔开。

这三个参数分别是:

  • 插件名字
  • 插件版本号
  • 插件作者

如果以字符串配合逗号来分隔,就是下面这样

1
register_plugin("插件名字", "版本号", ”作者名")

配合基础代码组合后

1
2
3
4
5
6
7
#include <amxmodx> //定义所需头部模块
//插件初始化
public plugin_init()
{
//注册插件基本信息
register_plugin("插件名字", "版本号", "作者名")
}

提示:

  • 上面的这个就是代码的基础,任何代码都会以这种形式展开。
  • 双引号是必须是切换至英文输入法才可以,不能写中文输入法的双引号,不然会无法编译插件(报错)。

赋值

变量是能赋值的,例如将Test变量赋值为4

1
new Test = 4

当赋值后,做判断的时候就可以判断这个变量中的值,例如

1
if( Test > 0)

除此之外,还有另一种赋值方式,和上面的代码基本一样,区别只是不在建立变量的时候直接赋值,看上去像这样

1
2
3
4
5
new Test
if(abc < 0)
{
abc = 6
}

通常来说第二种非立即赋值,大部分情况下会先做一个判断,然后再来赋值

提示:

  • 任何变量在创建后,他的值都为0。如果在创建变量时直接赋值,那么这个变量在创建后值就会发生变化,而不是默认的0
  • 注意,不可以给Bool(布尔)变量赋于其他的值(因为他只拥有false以及true)

多重判断

超过两个(包含两个)的判断视为多重判断。

多重判断可以让我们写出的代码更精确的执行我们需要的效果。

常见写法有两种,一种是堆叠多重

将两个或两个以上的判断堆在一起,这种情况下就不用写 大括号{}来包括起来

1
2
3
4
5
if( iTestB > 2) //这里不需要写 {        
if(TestA < 3)
{
//做些什么
}

上面代码中,由于从第二个判断后没有更多判断表达,所以需要加上{}大括号

单行多重判断

除了堆叠,还有另一种是单行多重,也就是将所有的表达式都写在一个判断中

1
2
3
4
5
//表达式为:iTest > 2 并且 TestA < 3 或者 iTestC 刚好等于 5
if( iTestB > 2 && TestA < 3 || iTestC == 5)
{
//要做些什么
}

判断的时机

一般会在一些特殊的时候进行判断,例如说:

  • 玩家持有手雷的时候身上是否拥有AK47
  • 玩家生命值是否小于60,并且该玩家是否拿着M4A1
  • 玩家在地上的时候是否是蹲着的
  • 玩家在空中的时候是否按住了鼠标左键没有放开
  • 玩家在蹲着的时候是否还拿着AWP狙击枪
  • 玩家的当前持有SG550武器,且子弹小于10发的时候是否他是站着的
  • 更多就不列举了…

提示:多利用多重判断可以让判断更精确、更有效、减少BUG错误

循环

要计算1+2+3,我们可以直接写表达式:1 + 2 + 3; // 6

要计算1+2+3+…+10,勉强也能写出来。

但是,要计算1+2+3+…+10000,直接写表达式就不可能了。

为了让计算机能计算成千上万次的重复运算,我们就需要循环语句。

for循环

AMXX的循环有两种,一种是for循环,通过初始条件、结束条件和递增条件来循环执行语句块。

1
2
3
4
5
6
new i
new n = 1000
for(i=1; i<33; i++)
{
n = n - 17
}

让我们来分析一下for循环的控制条件:

  • i=1 这是初始条件,将变量i置为1;
  • i<33 这是判断条件,满足时就继续循环,不满足就退出循环;
  • i++ 这是每次循环后的递增条件,由于每次循环后变量i都会加1,因此它终将在循环后不满足判断条件(i>=33)而退出循环;
  • n 为定义的变量,默认是1000

当这段循环开始执行后,会执行32次,每次会触发n = n - 17,也就是32 x 17 = 544,而默认变量n创建时赋值为1000,也就是说这段代码将会变成下面这样计算

1
1000 - (32 x 17) = 456

while循环

for循环在已知循环的初始和结束条件时非常有用。

而上述忽略了条件的for循环容易让人看不清循环的逻辑,此时用while循环更佳。

while循环只有一个判断条件,条件满足,就不断循环,条件不满足时则退出循环。比如我们要计算100以内所有奇数之和,可以用while循环实现:

1
2
3
4
5
6
new x = 0
new n = 99
while (n > 0) {
x = x + n
n = n - 2
}

循环是让计算机做重复任务的有效的方法,有些时候,如果代码写得有问题,会让程序陷入“死循环”,也就是永远循环下去。AMXX插件的死循环会让游戏无法执行当前代码的逻辑,某些游戏会直接挂掉或闪退(卡死未响应等),因此,要特别注意死循环的问题。

在编写循环代码时,务必小心编写初始条件和判断条件。

参考文献

Lv.1 — AMXX插件编写教程