欢迎来到WurstScript。本指南是对Wurst工作流程的非正式介绍,也是验证您的安装是否正确的简单方法。 我们将介绍如何构建代码、使用标准库、通过代码创建数据对象以及在魔兽争霸3中运行最终完成的地图。 本指南不会解释编程的核心概念,即函数和变量是什么、怎么工作,以及我们应该如何使用它们。
如果您使用安装程序正确设置了项目,那么您选择的项目文件夹中应该已经生成了许多文件夹和文件。 否则,请现在设置您的项目。
通过再VSCode中打开项目文件夹 (File
-> Open Folder ...
) 来加载项目。 打开项目文件夹是很重要的,这样Wurst才能检测到项目。 打开后,您应该能看到如下所示的内容:
我们来看看所有的文件:
打开 wurst 文件夹内的 Hello.wurst 文件来启动Wurst插件。 通过使用 >runmap
命令来运行项目。
文本 Hello World 将会显示在屏幕上。
让我们看看代码:
package Hello
/* Hello and welcome to Wurst!
This is just a demo package.
You can modify it to tests things out
and then later delete it and create your own, new packages.
Enjoy WurstScript? Consider starring our repo https://github.com/wurstscript/WurstScript */
init
print("Hello World")
第一行
package Wurst
是包声明。每个文件只包含一个名称类似于文件名但没有后缀的包。除开注释以外,在任何.wurst文件中,包都必须在顶部声明。
// Wurst demo package
init
print("Hello World")
第三行是注释,第四行通过包含 init 关键字而不缩进来声明一个 init 块。 在关键字后面缩进的所有内容都位于其内容块中,例如第5行中的 print 语句,该语句在所有玩家的屏幕上显示给定的文本。 所有块自动在文件末尾结束,例如本例中的 init 和 package 块。
让我们进一步详细分析我们刚刚做了什么。
如果您以前使用过Jass/vJass,您可能会想知道print是从哪里来的。
它既不是针对BJDebugMsg()
特别编写的封装,也不是wurst的内部函数,而是一个定义在标准库(又名stdlib/stl)中的函数。
WurstScript提供了一个标准库,其中包含大量方便的封装、实用的程序包和强大的系统,它们能够帮助几乎所有的地图,并帮助维护高级编程的一致语法。
当您使用安装工具创建项目时,它已被自动导入,并且现在位于项目根目录下的_build/dependencies/wurststdlib2
文件夹中。由于这是外部依赖项,因此您不应该修改这些文件。但是您可以随时打开、查看其中的内容并可以通过它们来学习。
您可以通过按住Ctrl键然后单击函数(如单词print上的任何位置)来访问vscode中的Printing包。
Printing
包中的函数引用了原始的魔兽争霸3的native函数(位于common.j和blizzard.j中)。
您可以使用vscode的全局搜索或文件搜索(ctrl+p
)来搜索您要查看的包。
在Wurst中,我们通常不建议直接调用大多数Native函数。相反,你应该扩展函数,使代码更加简洁、拥有更好的可读性、一致性并且便于撰写文档。扩展函数也可以通过函数自动填充功能更容易找到。
我们来看一些例子。
Jass:
local integer id = GetPlayerId(GetOwningPlayer(GetTriggerUnit()))
在Jass语句中,即便代码是从左向右读的,但实际的执行顺序却是反过来的。GetTriggerUnit()
在这里是第一格被执行的函数。
与Wurst对比:
let id = GetTriggerUnit().getOwner().getId()
在Wurst中,代码的执行顺序与其阅读顺序相同,并且没有嵌套括号。Java和其他高级编程语言的用户会觉得这很自然,新用户也可能会觉得这种编码方式更舒服。
这些功能来自另一个自动导入的标准库 - “Unit”API。
Jass:
local unit u = GetTriggerUnit()
call SetUnitX(u, 1000.)
call SetUnitY(u, 2000.)
call UnitAddAbility(u, 'hble')
call SetUnitPaused(u, true)
Jass不支持在多个目标上的 级联/链接 功能。你必须依赖于局部变量或重复的本地调用的帮助。
Wurst支持链式调用,基于 级联 操作符(..
):
GetTriggerUnit()..setX(1000.)
..setY(2000.)
..addAbility('hble')
..setPaused(true)
拓展函数 不仅仅可以封装现存的native函数,还提供了更便利的函数。上方代码的改进版如下:
GetTriggerUnit()..setXY(1000., 2000.)
..addAbility('hble')
..pause()
向量是三维计算中的非常有用的结构。但是,如果以class
的形式实现,它们会增加很多开销,这就是为什么vJass不会在任何公共库中使用它们。尽管魔兽提供了location
类型 - 但使用location
,并且处理点泄露是非常痛苦的。
Wurst通过元组类型实现了向量,不需要像类、结构体那样分配和解除分配,但仍然可以提供优雅的API。
vJass演示:
struct PreviousPointTracker
real x = 0.
real y = 0.
real angleToLast = 0.
method add(real addX, real addY) returns nothing
local real oldX = x
local real oldY = y
set x = x + addX
set y = y + addY
set angleToLast = Atan2(y - oldX, x - oldY)
endmethod
endstruct
在下方的wurst示例中,你可以看到向量类型使用了重载运算符来实现对向量数学的使用。
class PreviousPointTracker
var pos = vec2(0., 0.)
var angleToLast = angle(0.)
function add(vec2 addedVec)
let oldPos = pos
pos += addedVec
angleToLast = pos.angleTo(oldPos)
注意我们使用了 angle
类型来替代使用普通的 real
类型。这是一个元祖类型,并且和普通的实数类型一样高效,但它提供了用于处理角度的特定方法来协助使用。此外它还能够避免一些常见的错误,比如弧度和角度的混淆。
«««< HEAD 把 MyMap.w3x
视为一个“地形”图。这张地图是你编辑场景、装饰物和可破坏物的地方。Wurst使用这张地图作为编译一张测试地图或者正式发布的地图的起始点。其他的一些变化(比如游戏逻辑相关的常量)也可以在地形图中,但编译的起始点始终是相同的。 ======= Think of ExampleMap.w3x
as a “terrain” map. It’s the place you go to make terrain, doodad, and destructable changes; and wurst uses that as a starting point when compiling a mapfile for test or release. Other changes like gameplay constants also go in the terrain map, but the point remains the same.
mainsite/master
地形图对于Wurst来说是“只读”的,这意味着Wurst绝对不会修改或者覆盖地形图。这是一个很好的特性,意味着你可以在地图编辑器中编辑地形的同时,在VSCode中编辑你的代码。
当你在vscode中执行 buildmap
时,最终的地图文件(基于地形图和wurst.build文件,以及你的所有编译后的代码)会被输出到 /_build
文件夹中。你可以把这里的文件发布或者分发给其他人。 runmap
命令是类似的,不过地图名字始终是 WurstRunMap.w3x,并且Wurst将会把该地图复制到你的魔兽地图文件夹下,然后魔兽会直接运行这个地图。
你仍然可以将生成的地图拿去发布,不过runmap
更多时候只用于测试地图。
另一个很强大的特性是,Wurst会自动导入 imports/
文件夹内的内容到构建的地图文件中。这个特性可以协助你快速添加模型、贴图文件等等。
比如,如果你想要在地形图中使用导入的资源,将被生成到/_build
目录下的地图复制到项目根目录,随后使用WE打开它、清除Wurst生成的代码后,然后保存它。你就拥有了一个带有被导入的模型、贴图等的地形图。
正如你所看到的,Wurst拥有一个强大的API来基于Jass提供的类型和函数编写等级更高的代码。作为一个初学者,你的第一个问题可能是如何找到合适的函数库的代码来做你想做的事,而不是重新使用Jass的native函数。这里有3个提示来让你快速上手:
u
,并且你想要杀死这个单位,你可以试试输入u.k
后,按下ctrl-space
键来查看vscode会给你提供哪些函数。如果您正在寻找能够替代hiveworkshop上某个vJass库功能的Wurst库或函数,您可能会需要使用一些不同的方法。
HashMap<k, v>
,而不是Table
,这使得你能够使用更安全的强类型代码。我们制作了一个技能样例,你可以点击这里查看march’s blog post.
让我们进一步看一下这些代码.
package Conflagration
import ConflagrationObjects
import ClosureTimers
import ClosureForGroups
import HashMap
import ClosureEvents
let buffId = BUFF_OBJ.abilId
let buffMap = new HashMap<unit, CallbackSingle>()
//初始化运行
init
//闭包事件——任意单位施法【技能id】——提供参数【caster施法者, tpos施法点】
EventListener.onPointCast(SPELL_ID) (caster, tpos) ->
flashEffect(SPELL_EFFECT_PATH, tpos)
//闭包计时器——延迟一会做动作
doAfter(SPELL_EFFECT_DURATION) ->
//闭包单位组——选取单位园范围做动作【圆心-施法点,半径】——提供参数【u选取单位】
forUnitsInRange(tpos, SPELL_RADIUS) u ->
//判断选取单位有buff
if u.hasAbility(buffId)
caster.damageTarget(u, BONUS_DAMAGE)
flashEffect(BONUS_EFFECT_PATH, tpos)
caster.damageTarget(u, BASE_DAMAGE)
u.addAbility(buffId)
//判断buffmap有这个单位,就删除计时器
if buffMap.has(u)
destroy buffMap.get(u)
//再新建一个计时器cb,删除掉单位的buff
let cb = doAfter(BUFF_DURATION) ->
if buffMap.has(u)
buffMap.remove(u)
u.removeAbility(buffId)
//把计时器cb和单位绑定
buffMap.put(u, cb)
为了让技能创建的过程更加的方便,我们导入了不少包.
闭包单位组 和 闭包计时器模块,代替了魔兽原版的单位组和计时器,无需排泄,代码书写更简洁
ClosureTimers
和 ClosureForGroups
能帮助我们更好的处理在 (doAfter
) 和单位组选取(forUnitsInRange
)中的代码.
闭包事件,代替了触发器的注册/动作的添加,无需排泄,代码书写更简洁
ClosureEvents
给了我们更好的事件API, 包括闭包.
哈希地图,是哈希表的一小部分,无需排泄,代码书写更简洁
最后 HashMap
使得我们为其他数据绑定任意数据 - 在这儿我们用来将buff效果绑定在目标单位上.
这个技能是这样工作的:
我们使用EventListener.onPointCast
监听一个特殊的技能释放事件.在lambda区块内写上我们的事件回调( 解释:触发器事件的动作闭包).
回调函数中我们使用flashEffect
在合适的位置创建了一个彗星特效.
由于技能特效需要花费一定的时间掉落在地上,我们同样希望能够延迟技能的伤害效果.
因此用doAfter(SPELL_EFFECT_DURATION) ->
启用了一个计时器
这个紧跟着的新lambda区块内的代码会在一段时间后被执行.
现在假设特效已经着陆了,我们就可以给目标点圆范围内的所有单位施加伤害.
我们用 forUnitsInRange(tpos, SPELL_RADIUS) u ->
选取目标范围内的所有单位,并传递给接下来的lambda函数. 注意SPELL_RADIUS) u ->
这儿有个 u
在箭头前 ->
.
在Lambda区块中,我们现在就可以通过u来引用这些在范围内的单位,并应用我们的特效.
这种情况下,如果有的单位已经受到了buff的影响,我们施加一些额外的伤害,并且在接下来的If语句中创建一个即刻的特效.之后我们做常规的操作,施加普通的技能伤害,并给目标添加buff的技能马甲.(如果某个单位已经有了这个技能,就不会有任何影响.)
接下来的分支中我们判断了buff map中是否有一个条目是当前单位,如果是,我们销毁当前存在的计时器,以实现后来的重新计时.
之后,我们新建一个计时器,用来在buff持续时间结束后销毁buff.
译注:这个技能会在单位拥有buff的时候造成双倍伤害.
教你使用基本依赖库自带的数据结构——HashMap
let buffMap = new HashMap<unit, CallbackSingle>()
在这儿我们创建了一个之前提到的buffmap.基本上来说它让我们为每个单位保存一个ClosureTimers
的闭包实例,这个闭包实例指代一个用来控制buffs持续时间的计时器.
我们由衷的希望这篇指南能够帮助你开始wurst地图开发之旅.如果你还有更多的疑问,或者想要和更多的wurst开发者交朋友,欢迎戳这里Chat.