然后,开拍...
一切都是动作与反应

Yaml
为了节省时间,我不想写游戏编辑器,于是决定使用 yaml 文件。它的好处是可以用引用来避免重复。而且将来我也许可以做一个导出 yaml 文件的编辑器。
yaml 文件里要描述每个场景、每个角色可能发生的每一个动作。下面是 scene20.yml 的一个片段。你可以看到会下5秒钟的雨,场景开始时会出现一段文字。还能看到一些悬停动作,以及当你选择比如“打开”“信息牌”这个动作时会发生什么。
# START scene
# ==================================================
*action-scene-entities-ready-after:
- <<: *particle-system
<<: *particle-system-settings-rain
id: 200143
ukey: *particle-rain-20
stopTime: 5
*action-scene-start:
- <<: *trigger
id: 200144
run_once: true
methods:
- *particle-rain-20:
- start: nil
- <<: *text
id: 200104
run_once: true
text: "The Netherlands, Drente\n52°52'3.95\"N, 6°52'15.01\"E"
<<: *tween-text-bottom-left-fade-in-fade-out
# HOVER
# ==================================================
*action-hover:
*information-board1: Information board
*portal-key: An old stone with inscriptions
*branch: A tree root
*pole: Pole
*broken-information-board1: Broken information board
*dolmens: Dolmens
*stone-circle: Stone circle
*hotspot-20-exit: Go home
*hotspot-20-stone-circle: The center of the stone circle
# GLOBAL OPEN
# ==================================================
*action-open:
*information-board1:
- <<: *say
id: 200007
text: You cannot open an information board, it is a wooden panel with text and images
*portal-key:
- <<: *walk
id: 200105
- <<: *say
id: 200008
text: I cannot open the stone
*action-lookat:
*portal-key:
- <<: *walk
id: 200121
- <<: *say
id: 200041
text: It looks like an old stone with something written on it, it looks very interesting
某个 entity 也可以拥有不同于场景全局动作的其他动作,只需在文件底部加上下面的内容。
*diana:
*action-pickup:
*dolmens:
- <<: *walk
id: 200094
target:
x: 600
y: 175
framesetIndex: *move-up
- <<: *say
id: 200095
text: What do you think? Do you really think I could lift that?
所有 entity 的引用,比如 *dolmens,都写在 assets/refs.yaml 里,可以在任何地方重复使用。text: 文本会被转换成键,需要时还会被翻译。下面是 refs.yaml 的一部分
...
# tweens
# fade in, show text 3 seconds, fade out and move to the left
tween-text-bottom-left-fade-in-fade-out: &tween-text-bottom-left-fade-in-fade-out
textArguments:
color: *color-white-hidden # going to fadein, using the tweens
scale: .50 # @todo *scale-half-size
font: *font-ui-text
xOffset: *offset-left
yOffset: *offset-bottom
yOffsetIncludeUi: true # offset including menus (above menu)
offsetMargin: [0,0,5,5] # top, right, bottom, left @todo global margin
tweens:
- duration: 2
properties:
color: *color-white
- duration: 1
properties:
color: *color-white-hidden
x: -10
wait: 3
# particle schortcuts
particle-system-settings-rain: &particle-system-settings-rain
emissionRate: 200
speed:
- 300
- 400
linearAcceleration:
- 0
- 800
- 0
- 1000
sizes:
- 1
- 0.5
- 0.2
- 0.1
- 0.8
sizeVariation: 1
emitterLifetime: -1
particleLifetime:
- 0.8
- 1.5
colors:
- 0 # r
- 0 # g
- 0.3 # b
- 0.8 # a
- 0 # r
- 0 # g
- 0.8 # b
- 0.8 # a
# characters
nikki: &nikki 10
martin: &martin 11
diana: &diana 12
# -- scene 20
information-board1: &information-board1 20002
portal-key: &portal-key 20003
branch: &branch 20004
broken-information-board1: &broken-information-board1 20005
pole: &pole 20006
dolmens: &dolmens 20007
stone-circle: &stone-circle 20008
hotspot-20-stone-circle: &hotspot-20-stone-circle 20009
hotspot-20-exit: &hotspot-20-exit 20010
scene-menu-20: &scene-menu-20 20011
particle-rain-20: &particle-rain-20 20012
...
# -- scene 21
...
任何东西我都可以做成自定义动作,再在 Lua 中处理。目前的动作列表如下。
action-open: &action-open 1
action-close: &action-close 2
action-give: &action-give 3
action-pickup: &action-pickup 4
action-lookat: &action-lookat 5
action-talkto: &action-talkto 6
action-push: &action-push 7
action-pull: &action-pull 8
action-use: &action-use 9
action-hover: &action-hover 30
action-walkto: &action-walkto 31
action-say: &action-say 32
action-say_options: &action-say_options 34
action-inventory: &action-inventory 35
action-cannot-walk-to-point: &action-cannot-walk-to-point 36
action-popup: &action-popup 37
action-animation: &action-animation 38
action-scene-start: &action-scene-start 39
action-scene-entities-ready-after: &action-scene-entities-ready-after 40
action-camera: &action-camera 41
action-text: &action-text 44
action-modify-action: &action-modify-action 45
action-hotspot: &action-hotspot 46
action-trigger: &action-trigger 47
action-statement: &action-statement 48
action-world-loaded: &action-world-loaded 49
action-use-direct: &action-use-direct 50
action-audio: &action-audio 51
action-particle-system: &action-particle-system 52
动作的处理
yaml 文件会先转换成 json,再转换成 messagepack 文件,因为后者小得多,处理起来也快得多。
之后才是真正的工作,不过这里很难清楚地展示,因为 action.lua 大约有600行代码,另外还有 action-event.lua 和 action-status.lua。
action-event.lua 里是判断玩家是否点击了“捡起”之类的动作按钮,然后又点击了比如一个苹果(‘selectedItem’)的逻辑。或者“给予”> selectedActor。这里描述了玩家能做的所有事情的逻辑(配合 yaml 中描述的动作)。最后动作会被执行。
local activeActor = entityManager:find({movableByUserInput = true}, "Player")
action:execute(
{
action = tonumber(self.selectedActionButton.name),
initiator = #activeActor > 0 and activeActor[1] or nil,
selectedActionButton = self.selectedActionButton,
selectedActor = self.selectedActor,
selectedItem = self.selectedItem,
selectedInventoryItem = self.selectedInventoryItem,
selectedInventoryItem2 = self.selectedInventoryItem2
}
)
action.lua 里的内容太多了,下面是几个重要的部分。我为每种 yaml 动作类型创建一个包含全部逻辑的特殊对象。这个对象会被反复使用。
-- intial only once for the whole game
function Action:new()
self.actionObject = {}
self.actionObject[Action.WALK_TO] = ActionWalkTo()
self.actionObject[Action.SAY] = ActionSay()
self.actionObject[Action.SAY_OPTIONS] = ActionSayOptions()
self.actionObject[Action.INVENTORY] = ActionInventory()
self.actionObject[Action.POPUP] = ActionPopup()
self.actionObject[Action.ANIMATION] = ActionAnimation()
self.actionObject[Action.CAMERA] = ActionCamera()
self.actionObject[Action.TEXT] = ActionText()
self.actionObject[Action.ACTION_MODIFY] = ActionModify()
self.actionObject[Action.TRIGGER] = ActionTrigger()
self.actionObject[Action.STATEMENT] = ActionStatement()
self.actionObject[Action.AUDIO] = ActionAudio()
self.actionObject[Action.PARTICLE_SYSTEM] = ActionParticleSystem()
end
function Action:processActionList(actionList, actionListIndex, triggerData, afterProcessActionListCallback)
...
-- maybe this action may run once
if self:didRunOnce(actionId, actionData) then
-- maybe there is another (next) action in the list that don't have run_once
self:processActionList(actionList, actionListIndex + 1, triggerData, afterProcessActionListCallback)
return
end
-- going to start this action
-- maybe there are children, update the status to QUEUE and set this actionId on WORKING
-- this will create all needed action status objects
for _, actionListObj in pairs(actionList) do
local actionStatusObj = self:getActionStatusFromList(actionListObj.id)
if actionListObj.id == actionId then
self:getActionStatusFromList(actionListObj.id):setStatus(ActionStatus.WORKING)
elseif actionStatusObj:getStatus() == ActionStatus.WORKING or actionStatusObj:getStatus() == ActionStatus.DONE then
-- do nothing
else
self:getActionStatusFromList(actionListObj.id):setStatus(ActionStatus.QUEUE)
end
end
-- start the action
self.actionObject[actionData.action]:execute(
triggerData,
actionData,
actionList,
actionListIndex,
actionId,
afterProcessActionListCallback
)
end
声音
一个例子是动作类型 audio。它的逻辑类在 framework.core.action.audio.lua 里,并通过 action/.yml 文件以如下方式调用
*action-use-direct:
*inventory-strange-whistle:
*martin:
- <<: *audio
id: 100072
audiosetName: martin-play-strange-whistle.ogg
audiosetStatus: *audioset-playing
---@class ActionAudioActionData
---@field wait? integer The time to wait before starting the next action, default 0 sec.
---@field run_once? boolean If you want to run this action once
---@field audiosetName string The audioset name "something.ogg"
---@field audiosetStatus integer<Audioset.IDLE|Audioset.PLAYING|Audioset.PAUSE>
---@field dontWaitForCallback? boolean Wait until the audioset is done playing default false
---@param triggerData triggerData
---@param actionData ActionAudioActionData
---@param actionList table
---@param actionListIndex integer
---@param actionId integer
---@param afterProcessActionListCallback function
function ActionAudio:execute(
triggerData,
actionData,
actionList,
actionListIndex,
actionId,
afterProcessActionListCallback)
---@type Audioset
local audioset =
entityManager:findOne(
{
name = actionData.audiosetName
},
"Audioset"
)
-- create the callbacks first
if actionData.dontWaitForCallback ~= true then
audioset.callbackAfterStopped["actionAudiosetCallbackAfterStopped"] = function(s, framekey)
action:afterProcessActionListAction(
self,
triggerData,
actionData,
actionList,
actionListIndex,
actionId,
afterProcessActionListCallback
)
end
end
audioset:setAudiosetStatus(actionData.audiosetStatus)
audioset:play()
if actionData.dontWaitForCallback == true then
action:afterProcessActionListAction(
self,
triggerData,
actionData,
actionList,
actionListIndex,
actionId,
afterProcessActionListCallback
)
end
end
return ActionAudio
最后
一切都设计成 engine3 和游戏相互分离。这是保持可管理性所必需的。如果把所有动作都以固定方式写死在游戏里,你会遇到巨大的问题。更好的做法是写出能够处理所有动作的通用函数。