Wurst 入门指引

Last updated: November 14, 2017
Author: Frotty, Cokemonkey11, peq

介绍 (Part 1)

  还没有安装Wurst?跟随 安装指引 来安装并创建你的Wurst项目。

  这个指引需要读者对函数和变量有基本的理解。


欢迎

欢迎来到WurstScript。本指南是对Wurst工作流程的非正式介绍,也是验证您的安装是否正确的简单方法。 我们将介绍如何构建代码、使用标准库、通过代码创建数据对象以及在魔兽争霸3中运行最终完成的地图。 本指南不会解释编程的核心概念,即函数和变量是什么、怎么工作,以及我们应该如何使用它们。

项目结构

如果您使用安装程序正确设置了项目,那么您选择的项目文件夹中应该已经生成了许多文件夹和文件。 否则,请现在设置您的项目。

通过再VSCode中打开项目文件夹 (File -> Open Folder ...) 来加载项目。 打开项目文件夹是很重要的,这样Wurst才能检测到项目。 打开后,您应该能看到如下所示的内容:

我们来看看所有的文件:

  • /_build 包含动态生成的内容,不应该手动更改。
  • /.vscode 包含将vSCode链接到Wurst工具的 settings.json 文件。您可以在此添加其他与项目相关的配置。
  • /imports 此文件夹内的所有文件将在保存时导入到您的地图中。
  • /wurst 该文件夹中以 .wurst .jurst .j 结尾的所有文件都将被解析为代码。
  • .gitignore 如果你想让你的wurst项目被作为一个git仓库,这会是一个有用的文件。
  • MyMap.w3x 一个包含血魔法师的w3x演示地图。
  • wurst_run.args 定义从VSCode运行地图时使用的一些参数。
  • wurst.build 包含项目相关的信息。
  • wurst.dependencies 自动生成的链接库的文件,不应该手动更改。
  • wurst/Hello.wurst 演示包。

  确保始终至少有一个.wurst文件处于打开状态。否则,vscode内的Wurst插件将不会运行。

Hello 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 语句,该语句在所有玩家的屏幕上显示给定的文本。 所有块自动在文件末尾结束,例如本例中的 initpackage 块。

介绍 (Part 2)


说明

让我们进一步详细分析我们刚刚做了什么。

标准库

如果您以前使用过Jass/vJass,您可能会想知道print是从哪里来的。

它既不是针对BJDebugMsg()特别编写的封装,也不是wurst的内部函数,而是一个定义在标准库(又名stdlib/stl)中的函数。

WurstScript提供了一个标准库,其中包含大量方便的封装、实用的程序包和强大的系统,它们能够帮助几乎所有的地图,并帮助维护高级编程的一致语法。

当您使用安装工具创建项目时,它已被自动导入,并且现在位于项目根目录下的_build/dependencies/wurststdlib2文件夹中。由于这是外部依赖项,因此您不应该修改这些文件。但是您可以随时打开、查看其中的内容并可以通过它们来学习。

您可以通过按住Ctrl键然后单击函数(如单词print上的任何位置)来访问vscode中的Printing包。

Printing包中的函数引用了原始的魔兽争霸3的native函数(位于common.jblizzard.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会给你提供哪些函数。
  • Avoid writers block. Sometimes it’s better to write it the jass way and come back afterwards in case you can’t find the wurst equivalent. After all, there’s no reason wurst would disallow this! Keep in mind as well that wurst doesn’t have fancy API equivalents for every single jass call.

如果您正在寻找能够替代hiveworkshop上某个vJass库功能的Wurst库或函数,您可能会需要使用一些不同的方法。

  • 考虑是否可能有更好的东西。例如,我们鼓励Wurst用户使用通用的HashMap<k, v>,而不是Table,这使得你能够使用更安全的强类型代码。
  • 问问其他人。有时候,我们其中的一人已经翻译了你喜欢的vJass库。比如Cokemonkey11已经维护了许多先前使用vJass制作的地图。
  • 自己写一个!我们期待贡献者。

介绍 (Part 3)


来做个技能吧

我们制作了一个技能样例,你可以点击这里查看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)

导入

为了让技能创建的过程更加的方便,我们导入了不少包.

闭包单位组 和 闭包计时器模块,代替了魔兽原版的单位组和计时器,无需排泄,代码书写更简洁

ClosureTimersClosureForGroups能帮助我们更好的处理在 (doAfter) 和单位组选取(forUnitsInRange)中的代码.

闭包事件,代替了触发器的注册/动作的添加,无需排泄,代码书写更简洁

ClosureEvents 给了我们更好的事件API, 包括闭包.

哈希地图,是哈希表的一小部分,无需排泄,代码书写更简洁

最后 HashMap 使得我们为其他数据绑定任意数据 - 在这儿我们用来将buff效果绑定在目标单位上.

Buff效果

这个技能是这样工作的:

注册闭包事件

我们使用EventListener.onPointCast监听一个特殊的技能释放事件.在lambda区块内写上我们的事件回调( 解释:触发器事件的动作闭包).

编写事件的动作函数

回调函数中我们使用flashEffect在合适的位置创建了一个彗星特效.

由于技能特效需要花费一定的时间掉落在地上,我们同样希望能够延迟技能的伤害效果.

闭包计时器,延迟显示特效

因此用doAfter(SPELL_EFFECT_DURATION) ->启用了一个计时器

这个紧跟着的新lambda区块内的代码会在一段时间后被执行.

现在假设特效已经着陆了,我们就可以给目标点圆范围内的所有单位施加伤害.

闭包单位组,选取范围单位做动作

我们用 forUnitsInRange(tpos, SPELL_RADIUS) u -> 选取目标范围内的所有单位,并传递给接下来的lambda函数. 注意SPELL_RADIUS) u ->这儿有个 u在箭头前 ->.

在Lambda区块中,我们现在就可以通过u来引用这些在范围内的单位,并应用我们的特效.

If选取的单位有buff,就伤害它

这种情况下,如果有的单位已经受到了buff的影响,我们施加一些额外的伤害,并且在接下来的If语句中创建一个即刻的特效.之后我们做常规的操作,施加普通的技能伤害,并给目标添加buff的技能马甲.(如果某个单位已经有了这个技能,就不会有任何影响.)

If buffMap有当前单位,销毁闭包计时器

接下来的分支中我们判断了buff map中是否有一个条目是当前单位,如果是,我们销毁当前存在的计时器,以实现后来的重新计时.

嵌套一个闭包计时器,用于删除buff

之后,我们新建一个计时器,用来在buff持续时间结束后销毁buff.

译注:这个技能会在单位拥有buff的时候造成双倍伤害.

Buff Map

教你使用基本依赖库自带的数据结构——HashMap

let buffMap = new HashMap<unit, CallbackSingle>() 在这儿我们创建了一个之前提到的buffmap.基本上来说它让我们为每个单位保存一个ClosureTimers的闭包实例,这个闭包实例指代一个用来控制buffs持续时间的计时器.

结语

我们由衷的希望这篇指南能够帮助你开始wurst地图开发之旅.如果你还有更多的疑问,或者想要和更多的wurst开发者交朋友,欢迎戳这里Chat.