Starbound mod制作 阅读lua(一) terraformer.lua

决定来了解下地形转换器的工作原理,所有的地形转换器都使用一个lua脚本,而不是各用各的,这个脚本就是terraformer.lua,在objects/terraformer/这个位置。

这玩意好长啊,感觉还是一边写教程一边理解比较稳。

先上代码,直接翻到下面吧,我会分解来读的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
require "/scripts/vec2.lua"

function init()
self.minPregenerateTime = config.getParameter("minPregenerateTime", 5)
self.basePregenerateTime = config.getParameter("basePregenerateTime", 10)
self.pregenerateTimePerTile = config.getParameter("pregenerateTimePerTile", 0.1)

self.planetTypeChangeThreshold = config.getParameter("planetTypeChangeThreshold")
self.terraformInterferenceBuffer = config.getParameter("terraformInterferenceBuffer", 50)

self.biome = config.getParameter("terraformBiome")
self.planetType = config.getParameter("terraformPlanetType")

self.guiConfig = root.assetJson("/interface/scripted/terraformer/terraformergui.config")
self.guiConfig.gui.windowtitle.title = " " .. config.getParameter("shortdescription")
self.guiConfig.planetType = self.planetType

local terraformOffset = config.getParameter("terraformOffset", {0, 0})
self.terraformPosition = vec2.add(entity.position(), terraformOffset)

storage.uuid = storage.uuid or sb.makeUuid()
storage.size = storage.size or 0
storage.targetSize = storage.targetSize or storage.size

if storage.activated then
register()
end

updateInteractive()
updateAnimation()

message.setHandler("getStatus", function()
return {
active = not not (storage.addTimer or storage.expandTimer),
currentSize = storage.size or 0
}
end)

message.setHandler("activate", function(_, _, targetSize)
local success, errorCode = activate(targetSize)
return {
success = success,
errorCode = errorCode,
expandAmount = math.max(0, targetSize - storage.size)
}
end)

message.setHandler("confirmActivation", function()
storage.activationConfirmed = true
end)

message.setHandler("cancelActivation", function()
storage.addTimer = nil
storage.expandTimer = nil
storage.targetSize = storage.size
updateAnimation()
updateInteractive()
end)
end

function onInteraction(args)
return {"ScriptPane", self.guiConfig}
end

function update(dt)
if storage.addTimer then
storage.addTimer = storage.addTimer + dt

if not self.pregenerationFinished then
self.pregenerationFinished = world.pregenerateAddBiome(self.terraformPosition, storage.targetSize)
-- if self.pregenerationFinished then sb.logInfo("pregeneration to add biome finished after %s seconds", storage.addTimer) end
end

if storage.addTimer >= self.minPregenerateTime and self.pregenerationFinished or (storage.addTimer >= storage.pregenerateTimeLimit) then
world.addBiomeRegion(self.terraformPosition, self.biome, "largeClumps", storage.targetSize)

storage.size = storage.targetSize

-- sb.logInfo("added biome %s at %s with size %s after %s seconds of pregeneration", self.biome, self.terraformPosition, storage.targetSize, storage.addTimer)

storage.addTimer = nil

animator.playSound("deactivate")
updatePlanetType()
updateInteractive()
updateAnimation()
end
elseif storage.expandTimer then
storage.expandTimer = storage.expandTimer + dt

if not self.pregenerationFinished then
self.pregenerationFinished = world.pregenerateExpandBiome(self.terraformPosition, storage.targetSize)
-- if self.pregenerationFinished then sb.logInfo("pregeneration to expand biome finished after %s seconds", storage.expandTimer) end
end

if storage.expandTimer >= self.minPregenerateTime and self.pregenerationFinished or (storage.expandTimer >= storage.pregenerateTimeLimit) then
world.expandBiomeRegion(self.terraformPosition, storage.targetSize)

storage.size = storage.targetSize

-- sb.logInfo("expanded biome at %s to size %s after %s seconds of pregeneration", self.terraformPosition, storage.targetSize, storage.expandTimer)

storage.expandTimer = nil

animator.playSound("deactivate")
updatePlanetType()
updateInteractive()
updateAnimation()
end
end
end

function die()
deregister()
end

function updateInteractive()
object.setInteractive(not (storage.addTimer or storage.expandTimer))
end

function updateAnimation()
if storage.activated then
if storage.addTimer or storage.expandTimer then
animator.setAnimationState("baseState", "active")
animator.setAnimationState("beamState", "active")
else
animator.setAnimationState("baseState", "idle")
animator.setAnimationState("beamState", "idle")
end
else
animator.setAnimationState("baseState", "inactive")
animator.setAnimationState("beamState", "inactive")
end
end

function canActivate(newRegionSize)
if not world.inSurfaceLayer(self.terraformPosition) then
return false, "needSurface"
end

local checkRadius = (newRegionSize / 2) + self.terraformInterferenceBuffer
local activeTerraformers = world.getProperty("activeTerraformers") or {}
for k, pos in pairs(activeTerraformers) do
if k ~= storage.uuid and world.magnitude(self.terraformPosition, pos) < checkRadius then
return false, "tooClose"
-- return false, string.format("%s too close to another active terraformer %s at %d, %d", storage.uuid, k, pos[1], pos[2])
end
end

return true, ""
end

function activate(targetSize)
if storage.size >= world.size()[1] then
return false, "worldComplete"
end

if targetSize <= storage.size then
return false, "noExpansion"
end

if storage.addTimer or storage.expandTimer then
return false, "alreadyActive"
end

if storage.activated then
local success, reason = canActivate(targetSize)
if success then
triggerExpand(targetSize)
updateInteractive()
else
-- return { "ShowPopup", { title = "Activation Failed!", message = string.format("^red;Terraformer failed to activate: %s.", reason), sound = "" } }
end
return success, reason or ""
else
local success, reason = canActivate(targetSize)
if success then
storage.activated = true
register()
triggerAdd(targetSize)
else
-- return { "ShowPopup", { title = "Activation Failed!", message = string.format("^red;Terraformer failed to activate: %s.", reason), sound = "" } }
end
return success, reason or ""
end
end

function triggerAdd(targetSize)
storage.targetSize = targetSize
storage.activationConfirmed = false
storage.addTimer = 0
storage.pregenerateTimeLimit = pregenerateTimeLimit(targetSize)
self.pregenerationFinished = false
animator.playSound("activate")
animator.setAnimationState("baseState", "activate")
animator.setAnimationState("beamState", "activate")
end

function triggerExpand(targetSize)
storage.targetSize = targetSize
storage.activationConfirmed = false
storage.expandTimer = 0
storage.pregenerateTimeLimit = pregenerateTimeLimit(targetSize)
self.pregenerationFinished = false
animator.playSound("activate")
updateAnimation()
end

function pregenerateTimeLimit(targetSize)
return self.basePregenerateTime + (targetSize - (storage.size or 0)) * self.pregenerateTimePerTile
end

function updatePlanetType()
if not storage.planetTypeChanged then
local sizeRatio = storage.size / world.size()[1]
if sizeRatio >= self.planetTypeChangeThreshold then
storage.planetTypeChanged = true
world.setPlanetType(self.planetType, self.biome)
world.setLayerEnvironmentBiome(self.terraformPosition)
end
end
end

function register()
local activeTerraformers = world.getProperty("activeTerraformers") or {}

activeTerraformers[storage.uuid] = self.terraformPosition

world.setProperty("activeTerraformers", activeTerraformers)
end

function deregister()
local activeTerraformers = world.getProperty("activeTerraformers") or {}

activeTerraformers[storage.uuid] = nil

world.setProperty("activeTerraformers", activeTerraformers)
end

嘛就是这么长,但是分解开来一步一步读总能读懂的。
先按执行流程来吧,之前的教程里说过,屎大棒的lua代码都是先执行init()的,然后updata()是每一帧执行一次。那么我们先看init函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
function init()
self.minPregenerateTime = config.getParameter("minPregenerateTime", 5)
self.basePregenerateTime = config.getParameter("basePregenerateTime", 10)
self.pregenerateTimePerTile = config.getParameter("pregenerateTimePerTile", 0.1)

self.planetTypeChangeThreshold = config.getParameter("planetTypeChangeThreshold")
self.terraformInterferenceBuffer = config.getParameter("terraformInterferenceBuffer", 50)

self.biome = config.getParameter("terraformBiome")
self.planetType = config.getParameter("terraformPlanetType")

self.guiConfig = root.assetJson("/interface/scripted/terraformer/terraformergui.config")
self.guiConfig.gui.windowtitle.title = " " .. config.getParameter("shortdescription")
self.guiConfig.planetType = self.planetType

local terraformOffset = config.getParameter("terraformOffset", {0, 0})
self.terraformPosition = vec2.add(entity.position(), terraformOffset)

storage.uuid = storage.uuid or sb.makeUuid()
storage.size = storage.size or 0
storage.targetSize = storage.targetSize or storage.size

if storage.activated then
register()
end

updateInteractive()
updateAnimation()

message.setHandler("getStatus", function()
return {
active = not not (storage.addTimer or storage.expandTimer),
currentSize = storage.size or 0
}
end)

message.setHandler("activate", function(_, _, targetSize)
local success, errorCode = activate(targetSize)
return {
success = success,
errorCode = errorCode,
expandAmount = math.max(0, targetSize - storage.size)
}
end)

message.setHandler("confirmActivation", function()
storage.activationConfirmed = true
end)

message.setHandler("cancelActivation", function()
storage.addTimer = nil
storage.expandTimer = nil
storage.targetSize = storage.size
updateAnimation()
updateInteractive()
end)
end

不管变量声明,我们碰到用的时候再说,第一个执行的函数是updateInteractive(),看名字大概是更新interactive属性的,这里说下interactive属性是object的一个属性,决定了这个object是否可以按e互动。于是我们去找找这个函数的定义。

1
2
3
function updateInteractive()
object.setInteractive(not (storage.addTimer or storage.expandTimer))
end

就一句,这个object是个公有模块,其setinteractive函数就是用来控制一个物体是否可互动的_(不懂的话请自行去doc文件夹或者wiki去查这些公用函数的意思)_。这里的意思就是当这个函数执行的时候,如果addtimer或者expandtimer都不存在,那就让转换器可交互,但凡两者其一有实际的值_(根据变量名,这两个应该都是给update函数做计时器的,如果有值,意思大概是正在工作中)_,则使其无法互动。
这应该很好理解,用过地形转换器的都知道你一旦按e互动过一次这东西就开始运行了,然后就不能再和它互动了。那么这个函数在init()中执行,意思就是初始化的时候使其可互动,毕竟这个时候addtimer和expandtimer都没被赋值呢。

继续看init(),紧跟着updateInteractive的就是updateAnimation,那这个更好猜了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function updateAnimation()
if storage.activated then
if storage.addTimer or storage.expandTimer then
animator.setAnimationState("baseState", "active")
animator.setAnimationState("beamState", "active")
else
animator.setAnimationState("baseState", "idle")
animator.setAnimationState("beamState", "idle")
end
else
animator.setAnimationState("baseState", "inactive")
animator.setAnimationState("beamState", "inactive")
end
end

先判断了activated的值,看名字就知道是用来判断是否激活的。也就是激活了之后根据addtimer和expandtimer的值来切换动画状态,如果没激活则切换到“inactive”动画状态。还是那句话,animator是公有模块,函数意思自己查。

接下来调用了几个message.setHandler函数,这也是公有的函数,要理解它得先知道另一个函数world.sendEntityMessage(entityId,messageType,[args]),用它可以向特定id的物体发送一个消息messageType,使其调用和这个名字对应的message.setHandler里的函数。
举个例子,第一个setHandler里的参数是 “getStatus” 和一个函数(我们暂时叫他X函数)。那么我可以在另一个物体的脚本里调用world.sendEntityMessage,并在参数里指定这个地形转换器的id,然后指定”getStatus”,则这个world.
sendEntityMessage就会调用X函数,也会返回X函数的返回值。
那么这里的“getStatus”对应的函数内容是“返回active和currentSize两个变量的值”,然而我也不知道那个not not是要干嘛,算了pass掉了,我们继续。

第二个setHandler的函数是返回了三个值,其中两个是函数activate的返回值。我们去下面看看这个函数的定义。

哇好长啊你们自己复制出来看吧,这个函数包含多个判断,每个判断成功后都会直接返回true或false和一个字符串,结合调用处的那个接收用的变量名,很容易理解意思。
第一个判断用到了world.size,是返回当前世界大小,有个[1]是因为返回值是个数组,包含xy两个值。
后面几个判断自己看。
然后遇到了个 canActivate 函数,正好就定义在activate的上面,看了一下意思应该是检查一些限制来判断这个地形转换器是否能够开启,比如在不在星球表面,周围有没有其他正在工作的地形转换器等。
这一段看起来蛮费劲的if else结构意思应该是,自上面三个先行判断(基本上是在判断自己的属性值是否符合激活条件)之后,再进行一些判断(基本上是在判断外部环境是否符合激活条件),判断之后可以激活就设置 storage.activated = true ,并执行 register 函数,这个函数是在当前world数据中声明自己的存在,也解释了之前在
canActivate 函数里看到的 activeTerraformers 属性是哪来的。
还执行了 triggerAdd 函数,这里面可以看到对 storage.targetSize 等之前看见的一些变量的声明,总之解决了很多疑惑,顺便播放了激活的音效和动画,结合游戏就能理解,当我们听到激活音效的时候,lua脚本就执行到这里了。
不过这里还判断了一下另一种情况,即 storage.activated 已经为true了,但 activate 函数仍然被调用,而且也通过了之前的一堆激活条件检查的情况,我也不清楚这种情况会怎样发生,但应该并非正常流程,所以先pass掉。

现在已经看完了activate函数了,我们回到setHandler那里,现在我们就理解这个sethandler函数的意思了,其实就是激活地形转换器。可以看出来,既然这玩意是sethandler里写的,那么地形转换器的激活是由其他脚本去调用的,不过我还是不知道到底是哪个脚本会调用这个。。按理不应该是玩家按E就激活了么。。难道是玩家按E就是在调用world.sendmessage并指定activate了么?不过这个并不影响我们理解地形转换器的工作逻辑,先pass。

继续看下一个sethandler,下面有个 confirmActivation ,没啥好说的,在下面是个 cancelActivation ,里面把几个关键变量都初始化了,这下我大概明白之前activate函数里为啥还有加那种 activated = true但是又没有激活的情况了,应该是考虑到地形转换器运行到一半时由于意外情况导致中断。

下面有个函数 onInteraction ,然而没找到调用的地方,那它可能是个类似init一样的函数,会被屎大棒在特定情况下调用,查wiki后,发现这个就是按e互动时调用的函数,我们来看下它写了啥。
return {“ScriptPane”, self.guiConfig}
对照wiki可以看出这句是调用了guiconfig对应的JSON内容,这个guiconfig变量在init函数里已经定义了,我们去看看这个文件。位置是interface/scripted/terraformer/terraformergui.config,打开后可以看到这个config文件配置的是一个UI面板,原谅我没用过原版的地形转换器。。FU版的居然没有UI。
UI里配置了一些按钮(btn),其 callback 对应同目录下的terraformergui.lua脚本里的函数,我们去看看。
这里就不贴内容了,也不用细看,总之关键在于我们的确发现,terraformergui .config的按钮对应的函数通过world.sendEntityMessage方法调用了 pane.sourceEntity() 即UI面板所属物体,即正在操作的地形转换器的脚本函数,这些函数,刚才已经讲过了,正是用setHandler函数定义的,如果对message系统还是不理解,可以返回去看我对sethandler函数的解释。

接下来轮到update了,这个函数每一帧会被执行一次,所以里面肯定会有基本的计时器结构。比如这个 storage.addTimer = storage.addTimer + dt ,就是在每次调用的时候累加addTimer变量用以计时,然后下面就会根据这个timer的值来做一些判断。
这里注意 world.pregenerateAddBiome 这个函数,查看world的文档和wiki可知,这玩意会调用一个 asynchronous 过程,即“异步”,意思是,当脚本执行到异步函数的时候,会不等这个函数执行完就继续执行下面的函数,举个例子。

1
2
3
a = 1
执行异步函数,内容为a = a+1
输出a

那么这个时候,我们在执行输出a的时候,并不知道a=a+1完成没有。但是好处是如果异步函数的内容非常耗时间(比如上面那个生成世界,显然非常耗时)的话,那么我们就可以让这个函数慢慢执行,脚本先不管它的结果,处理一些其他事情。
那当我们需要异步函数的结果的时候,如何知道异步函数执行完了呢,我们使用一个变量接受异步函数的返回值,然后在需要的时候判断该变量是否为true,如果是true,就说明执行完毕了,如果是false,说明还得等等。在update函数里,由于每帧都会执行,只要在update里去判断,那么判断异步函数的结果可以十分及时。
在本脚本中,使用了 self.pregenerationFinished 来接收异步函数 world.pregenerateAddBiome 的结果。后面紧接着就是判断该结果的if语句。意思是如果生成ok 并且 时间满足一定条件,则重置 storage.size storage.addTimer 变量,并播放关闭音效,刷新星球类型_( updatePlanetType 函数,在下面有定义,意思大概是如果转化的生态范围达到一定比例,就改变该星球的类型)_、转化器状态和动画状态。
elseif的意思是如果不是用的常规的 addTimer ,而是 expandTimer (还记得 expandTimer 在哪里被赋值的么?可以回去看看)被使用的话,则执行基本相同的逻辑。
总之,update的整体意思很简单,如果计时器被赋值了,也就是说被激活了,那么开始生成相应的地形生态,生成完毕后或者到时间限制后,将生成结果应用到当前世界中,随后关闭生成器。

总结下就是,这个脚本会在按e互动的时候调出UI,UI中可以设置一些属性,按钮则会调用脚本的一些函数,例如启动激活流程和关闭激活状态,之后update函数就会负责开始生成对应的地形,并结束激活状态。

感谢观看。