ZH-CN/脚本编写介绍

From Multi Theft Auto: Wiki

对于 MTA 服务器而言,“资源” (Resource) 是十分重要的一个概念。一个资源本身表现为一个包含了一系列文件的文件夹或压缩文件 (zip),外包含一个用于指示服务器如何加载资源以及资源所包含文件说明的元数据文件 (Meta File)。可把资源的概念视作是运行在操作系统之上的程序 —— 它可以被启动或停止执行,且多个资源可以同时运行在服务器上。

所谓脚本编程,本质上即是在编写资源。资源可以定义它自身的类型为游戏模式、地图等。MTA 自带一些可供您的游戏模式使用的可选辅助用资源;例如 maplimits,一个用于限制玩家行动范围的资源;或是 deathpickups,用于创建武器拾取物以供辅助死亡竞技 (Deathmatch) 游戏模式。

[[{{{image}}}|link=]] Tip: 您应该使用支持 Lua 格式代码的编辑器以编写脚本,以提高脚本编写的效率。我们建议使用 Notepad++LuaEdit。我们也提供测试版的 MTA 脚本编辑器(目前仍在开发中)。

创建一个脚本

第一步,我们将开始一步一步学习如何编写一个能让玩家到处行走的简单脚本。

脚本都在哪里存放着?

首先让我们来看看脚本的文件结构。打开 MTA 服务器目录,并定位到以下路径:

server/mods/deathmatch/resources/

在该目录下,可以看到一系列 MTA 自带的示例脚本压缩文件。每个压缩文件都是一个 “资源”,在服务器启动时它们会被自动解压并被加载到服务器上运行。若需创建一个新资源,只需在该目录下新建一个任意名称的文件夹。在本教程中我们以 "myserver" 资源作为演示,注意:脚本资源创建时建议使用英文命名的文件夹(英文开头可带阿拉伯数字)。

现在请定位到以下路径:

server/mods/deathmatch/resources/myserver/

定义您的资源

为了能够让服务器得知资源内所包含的数据,每个资源内都必须包含一个位于资源根目录下的 "meta.xml" 文件(本例即 "myserver" 文件夹为资源根目录)。因此我们需要新建该文件,并以记事本方式打开。

在 "meta.xml" 文件内输入以下的代码:

<meta>
     <info author="YourName" type="gamemode" name="My Server" description="My first MTA server" />
     <script src="script.lua" />
</meta>

在 "<info />" 标签中,有一个用于指定资源类型的 "type" 属性。以上代码中指定了该资源类型为 "gamemode"(游戏模式),其他可选的资源类型还有常规包含文件或 "map"(地图),后文将会详细解释。目前只需要知道游戏模式是所有服务器的必不可少的核心,任意服务器都必须要有一个游戏模式。

"<script />" 标签指明了该资源内所包含的脚本文件,后文将会详细解释。

创建一个简单的脚本

注意上例代码中的 "<script />" 标签中并没有将 .lua 文件指定在其他目录路径下,因此现在需要创建一个与 meta.xml 文件同目录下的 .lua 脚本文件;随后,请把以下代码复制到新创建的 script.lua 文件中:

local spawnX, spawnY, spawnZ = 1959.55, -1714.46, 10
function joinHandler()
	spawnPlayer(source, spawnX, spawnY, spawnZ)
	fadeCamera(source, true)
	setCameraTarget(source, source)
	outputChatBox("Welcome to My Server", source)
end
addEventHandler("onPlayerJoin", getRootElement(), joinHandler)

当玩家进入服务器后,脚本会将玩家刷到上例代码所指定的出生点坐标 (x, y, z)。注意必须要使用 "fadeCamera" 函数以避免屏幕全黑的情况;同样,DP2 以后的 MTA 版本中,也同样需要使用 "setCameraTarget" 函数以设置玩家摄像机的目标,否则玩家进入服务器后只会见到游戏内蓝色的天空。

source 变量表示的是事件的触发者。由于以上代码会在玩家进入服务器时被触发,因此您需要使用该变量以确定是哪位玩家进入了服务器。因此上例代码只会影响到新进入了服务器的玩家,而非服务器内的所有玩家。

上例代码中,我们可以看到一个 addEventHandler 函数;从中可看见 3 个参数:'onPlayerJoin',其表明何时代码会被触发;getRootElement(),其表明了可能的事件触发者(getRootElement() 表示的是游戏内的一切事物或玩家,因此该事件可能是由游戏内的任意事物或玩家所触发的)。以及 joinHandler,其表明哪个函数在该事件 ('onPlayerJoin') 触发后会被执行。其他的代码细节会在后文中详细介绍,现在就启动服务器然后测试一下代码效果吧!

执行脚本

只需要执行 server/ 目录下的可执行文件即可启动服务器。启动服务器后,首先可以从服务器后台上看到一系列的服务器基本信息;注意 port number(端口数)信息,在进入服务器前需要使用到该信息。随后服务器会加载所有位于 mods/deathmatch/resources 目录下的资源,最后会看到提示 "ready to accept connections!",即表明服务器启动成功。

在玩家进入服务器前,必须要首先加载游戏模式。在服务器后台输入指令 "start myserver" 并按下回车键发送指令。服务器便会立即启动您刚刚所创建的游戏模式,并列出该游戏模式内所有的脚本代码性错误及警告提示。现在可以启动 MTA 客户端,点击 "Quick Connect" 并输入您在服务器后台上所看到的服务器 IP 地址及端口数。如果没有错误,几秒过后玩家就会出生在 Los Santos。

接下来,我们将会在脚本中添加一条用于在玩家附近刷出交通工具的游戏指令。您可以跳过本章节并阅读更为高级的地图管理器教程;您也可以阅读图形操作界面 (GUI) 脚本编写介绍教程以学习如何在 MTA 下创建图形操作界面 (GUI) 及对其进行脚本编写。

创建一个简单的游戏指令

打开先前我们所创建的 "script.lua" 文件。上文提到,我们需要创建一个可在玩家附近刷出交通工具的游戏指令。首先我们需要定义一个用于处理玩家输入指令的函数,以及一个用于创建玩家可从客户端控制台输入游戏指令的指令处理器。

-- 定义一个由指令处理器所调用的函数,并提供三个参数:thePlayer, command, vehicleModel
function createVehicleForPlayer(thePlayer, command, vehicleModel)
   -- 创建交通工具的代码
end

-- 创建一个指令处理器
addCommandHandler("createvehicle", createVehicleForPlayer)

"提示:您可以点击示例代码中的函数名以查看该 API 函数的文档。"

有关指令处理器

addCommandHandler 的第一个参数表明了玩家可以从客户端输入的游戏指令(同时也表明新建了这个游戏指令);第二个参数是该游戏指令发送到服务端后所调用的函数,本例中该函数名为 "createVehicleForPlayer"。

如果您有一定的编程经验,您应当知道调用函数的方法是这样的:

functionName(参数1, 参数2, 参数3, ..)
functionName(thePlayer, commandName, argument3, ..)

看看上例代码,我们发现参数1 为 thePlayer,参数2 为 commandName。thePlayer 表明哪位玩家发送了该游戏指令;而 commandName 表明游戏指令字符串。因此如果玩家发送了指令 "/greet",commandName 便是 "greet"(不包含指令前面的斜杠 "/")。参数3 是玩家额外的输入,后文中将会提到。 请记住前两个参数(thePlayer 及 commandName)是必不可少的参数,但是您可以随意命名这些参数以符合您的代码风格。

我们已经调用了 addCommandHandler 函数;而由于 createVehicleForPlayer 也同样是一个函数,它也可以以相同的方式被内部所调用。

例如:某玩家输入了游戏指令 "createvehicle 468" 以刷出 Sanchez,指令处理器随后会调用 createVehicleForPlayer 函数,然后我们看看这个 createVehicleForPlayer 函数是如何被调用的:

createVehicleForPlayer(thePlayer,"createvehicle","468") -- thePlayer 表示的是输入了 createvehicle 指令的玩家

可以看到,它提供了几个参数:输入了指令的玩家,指令字符串本身(不包括前面的斜杠 '/')以及核心指令 createvehicle 以后的任意文本,本例中 "468" 为核心指令以后的文本,其为 Sanchez 交通工具的 ID。所有的指令处理器中,前两个参数都是一样且必须有的(参见 addEventHandler 文档)。因此,任意的指令处理器中,您都必须得要先定义这两个基本参数,再附加任意数量的附加参数。(附加参数有多少个,核心指令后的附加指令信息就有多少;本例中,createvehicle 有一个表示交通工具 ID 的附加指令信息,因此附加参数数量同为一个)

"注意:必须要在定义了指令处理函数之后再创建指令处理器,否则指令处理器届时将无法找到其处理函数。"

编写函数

为了完成我们所创建的函数,我们得先想想我们要在该函数内完成的一些事:

  • 获取玩家位置以便知道在何处刷出交通工具(我们希望在玩家身边刷出)
  • 计算最理想的交通工具刷出点(我们不希望玩家会卡在车辆内)
  • 刷出交通工具
  • 检查交通工具是否成功刷出,如果没有则输出错误信息

为了实现以上的需求,我们需要使用一些 API 函数。可以在服务端函数列表中找到我们所需的函数。首先我们需要一个用于取得玩家位置的函数。由于玩家属于 “元素” (Element),我们找到 针对元素的 API 函数,并找到 getElementPosition 函数。单击该函数名便可阅读该函数的文档。文档内详细说明了函数的调用语法,返回值以及其示例代码。函数调用语法告诉我们在调用函数时应该提供什么参数。

getElementPosition 的调用语法为:

float, float, float getElementPosition ( element theElement )

函数名前的三个 "float" 表示的是返回值类型。本例中表示该函数返回三个浮点数(x、y 及 z)。括号内,可以看到调用该函数时所需提供的参数。本例中只需要传递一个表示欲获取其位置的元素,在这里即玩家。

function createVehicleForPlayer(thePlayer, command, vehicleModel)
	-- 获取玩家位置并将信息放入 x、y 及 z 变量中
	-- (local 的意思是定义一个局部变量,在其作用域以外的地方无法访问,本例中即函数作用域)
	local x,y,z = getElementPosition(thePlayer)
end

下一步,我们希望确保交通工具不会直接刷在玩家所在的位置(这样会玩家卡在车辆内),因此我们把 "x" 变量的值增加了少许,让车辆在距离玩家更东的位置被刷出。

function createVehicleForPlayer(thePlayer, command, vehicleModel)
	local x,y,z = getElementPosition(thePlayer) -- 获取玩家位置
	x = x + 5 -- x 坐标增加 5 个单位
end

现在我们需要另一个用于刷出车辆的函数。再次打开 服务端函数列表,这次因为要找的是和交通工具有关的,所以定位到 交通工具函数,并阅读 createVehicle 的文档。根据该函数的调用语法,该函数只有一个返回类型(最常见的情况),表示的是创建了的交通工具元素。同样,我们可以看到在调用语法中有一些用方括号 [ ] 括起来的参数,这类参数说明它们是可选的,调用时可以不提供。

在我们所定义的函数中,一切需要传递给 createVehicle 函数的参数都准备好了:计算好的 "x, y, z" 变量以及通过玩家输入的游戏指令 "createvehicle 468" 所得到的交通工具 ID,可以通过 vehicleModel 参数得到该 ID。

function createVehicleForPlayer(thePlayer, command, vehicleModel)
	local x,y,z = getElementPosition(thePlayer) -- 获取玩家位置
	x = x + 5 -- x 坐标增加 5 个单位
        -- 创建交通工具并将返回值存入到 ''createdVehicle'' 变量内
	local createdVehicle = createVehicle(tonumber(vehicleModel),x,y,z)
end

当然这些代码还有改善的余地,但至少我们还可以添加一个用于检测交通工具是否成功创建的代码。阅读 createVehicle 函数文档的 返回值 部分,该部分说明:若该函数返回 false,则说明交通工具创建失败。因此,我们需要检查 createVehicle 变量的值以检测交通工具是否创建成功。

以下是完整的脚本代码:

function createVehicleForPlayer(thePlayer, command, vehicleModel)
	local x,y,z = getElementPosition(thePlayer) -- 获取玩家位置
	x = x + 5 -- x 坐标增加 5 个单位
	local createdVehicle = createVehicle(tonumber(vehicleModel),x,y,z)
	-- 检查返回值是否为 ''false''
	if (createdVehicle == false) then
                -- 如果是,将错误信息发送给该特定玩家
		outputChatBox("Failed to create vehicle.",thePlayer)
	end
end
addCommandHandler("createvehicle", createVehicleForPlayer)

上例代码中,还介绍了另一个用于发送客户端信息的 outputChatBox 函数。现在,您应该有能力可以自行阅读 API 函数文档了。若需要进一步学习高级脚本编写,请阅读地图管理器教程。

必知

目前为止,您已经接触到及知悉了有关资源、指令处理器及如何在文档中寻找 API 函数,但要学的还有很多。本节将粗略告诉您有关这些学习内容,您可以自行阅读相关教程。

客户端及服务端脚本

您可能曾经了解或是看到过所谓 “客户端及服务端脚本” (Server/Client Script) 的概念或名称了。事实上,您不仅能够编写典型的运行在服务器之上的,用于提供游戏指令处理等需求的脚本;还能够编写运行在 MTA 玩家客户端之上的脚本。之可以编写客户端脚本,原因是一些 MTA 所提供的特殊功能只能在客户端上执行(例如图形操作界面),或是在客户端上执行一些代码比在服务端上执行效率要高些。

大部分您所制作的资源(游戏模式、地图)可能都是服务端脚本,例如本教程前面所介绍的脚本。但如果您所编写的代码无法在服务端上执行,则您可能需要令其可在客户端上运行(即编写客户端脚本)。若要开始尝试编写一个客户端脚本,您需要创建一个脚本文件(例如,把它命名为 client.lua),并修改 meta.xml 如下:

<script src="client.lua" type="client" />

type 属性默认值为 'server'(即默认服务端脚本),因此只有在需要指定其是客户端脚本时才显式指定 type 属性。一个脚本文件一旦其 type 属性的值设置为 client,则玩家连接到服务器时,该脚本文件会自动下载到玩家客户端。有关更多信息,请阅读客户端脚本教程。

更复杂的资源

上一节简单介绍了如何添加一个客户端脚本,但其实您什么都可以做到。正如本教程开头提到过,资源可以是任何类型的游戏模块。这些资源本身做什么或提供什么功能,便决定了它们的类型是什么。首先我们来分析一下下面的这些资源:

第一个示例资源 —— 辅助类资源

/admin_commands
	/meta.xml
	/commands.lua
	/client.lua
<meta>
	<info author="Someguy" description="admin commands" />
	<script src="commands.lua" />
	<script src="client.lua" type="client" />
</meta>
  • commands.lua 提供一些管理员指令,例如封禁玩家、玩家禁言等
  • client.lua 提供图形操作界面以简化玩家操作

该示例资源可以随时被使用(也可以设置为随服务器而自动启动),原因是该资源对于整个游戏流程而言有一定的帮助(管理服务器)且不会影响到正常的游戏过程。

第二个示例资源 —— 游戏模式

/counterstrike
	/meta.xml
	/counterstrike.lua
	/buymenu.lua
<meta>
	<info author="Someguy" description="Counterstrike remake" type="gamemode" />
	<script src="counterstrike.lua" />
	<script src="buymenu.lua" type="client" />
</meta>
  • counterstrike.lua 包含有类似于以下功能的实现:
    • 允许玩家选择其团队并刷出玩家
    • 把武器、目标及游戏指示提供给玩家
    • 规定游戏规则。例如:一盘游戏的时间、玩家死后会发生什么等
    • 等等等等...
  • buymenu.lua 是一个客户端脚本,其用于创建武器购买菜单(使用到了图形操作界面,因此必须写为客户端脚本)

该示例资源便是一个游戏模式,因为它不但影响到了游戏过程,还实际规定了游戏规则。该资源的 type 属性表明了该资源是运行在地图管理器上的,但 QA 团队编写的另一个资源可以用于帮助您管理游戏模式以及地图加载。强烈建议您在该辅助资源的基础上编写您的游戏模式。

这可能也说明了游戏模式在没有地图的情况下无法运行。游戏模式本身就应该包含有至少一个地图。下一个示例将会介绍地图。

第三个示例 —— 地图

/cs-airport
	/meta.xml
	/airport.map
	/airport.lua
<meta>
	<info author="Someguy" description="Counterstrike airport map" type="map" gamemodes="counterstrike" />
	<map src="airport.map" />
	<script src="airport.lua" />
</meta>
  • XML 格式的 airport.map 文件向游戏模式提供了有关该地图的信息,包括:
    • 玩家的出生点、出生时配备的武器,以及玩家所属的团队
    • 玩家的目标
    • 天气、世界时间、时间限制
    • 提供交通工具
  • airport.lua 可能包含了一些辅助地图自身的功能:
    • 当特定的事情发生时打开特定的门或使某物爆炸
    • 创建或移动一些自定义对象,或是控制通过 .map 文件所创建的对象
    • 等等等等...

正如您所见的,type 属性改为了 'map',告诉地图管理器该资源为地图;而 gamemodes 属性指明了该地图只有在什么游戏模式下才能够运行,在本例中所指定的游戏模式即上面所创建的。 地图资源内也可以包含脚本。当然,地图并不一定需要包含脚本,但是这可以在游戏模式所设计的游戏规则以外再允许地图创造者设计这些地图自己的游戏规则。

airport.map 文件内容可能如下:

<map mode="deathmatch" version="1.0">
	<terrorists>
		<spawnpoint posX="2332.23" posY="-12232.33" posZ="4.42223" skins="23-40" />
	</terrorists>
	<counterterrorists>
		<spawnpoint posX="2334.23443" posY="-12300.233" posZ="10.2344" skins="40-50" />
	</counterterrorists>

	<bomb posX="23342.23" posY="" posZ="" />
	
	<vehicle posX="" posY="" posZ="" model="602" />	
	<vehicle posX="" posY="" posZ="" model="603" />	
</map>

当游戏模式及地图同时被加载,地图管理器会自动启动地图资源,随后游戏模式资源会开始读取地图信息;当切换到其他地图时,当前的地图资源会被停止,然后轮到下一张地图资源被加载。请阅读编写游戏模式教程以学习更深入的地图资源管理模式。

事件

MTA 会以触发事件的形式告知脚本发生了什么。例如,当任意玩家死亡时,onPlayerWasted 事件会被触发。为了能够及时地在任意玩家死亡时能够执行一些操作,您需要自定义一个用于处理该类型事件的事件处理器。

下列示例代码会在任意玩家死后,向所有玩家发送该玩家死亡的信息:

function playerDied(totalAmmo, killer, killerWeapon, bodypart)
	outputChatBox(getPlayerName(source).." died!")
end
addEventHandler("onPlayerWasted",getRootElement(),playerDied)

事件文档列举了不同事件触发后将会传递到事件处理函数的不同参数。请留意 source 变量,虽然其并非是参数,但每个处理函数都隐式包含有该变量。在不同的事件中,该变量表示的概念不同;例如玩家事件的处理函数中,该变量表示触发了事件的玩家元素。

然后呢?

现在,您应该已经大概了解了 MTA 脚本编写的一些基础知识以及如何阅读文档了。首页提供有更多的信息、教程以及参考链接。您可以随时阅读这些文章以更深入地学习 MTA 脚本编写。

[[{{{image}}}|link=]] Note: 强烈建议您开始阅读调试脚本教程。编写脚本时,调试是十分有必要的;我们也建议您参考预定义变量列表中的信息以帮助您更高效率地编写 MTA 脚本。

相关参考: