E ação...
Tudo é ação e reação

Yaml
Como eu não queria escrever um editor de jogo para economizar tempo, decidi usar arquivos yaml. A vantagem é que eles também podem usar referências para evitar repetição. E no futuro eu poderia criar um editor que exporte arquivos yaml.
Nos arquivos yaml era preciso descrever cada ação que poderia acontecer por cena e por personagem. Abaixo você vê um trecho do scene20.yml. Nele dá para ver que vai chover por 5 segundos e que um texto aparece quando a cena começa. Você também vê algumas ações de hover e o que deve acontecer quando você escolhe, por exemplo, a ação “abrir” “painel de informações”.
# 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
Também é possível que uma entity tenha ações diferentes das ações globais descritas para a cena. Para isso, basta adicionar o seguinte no fim do arquivo.
*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?
As referências de todas as entities, por exemplo *dolmens, estão descritas em assets/refs.yaml e posso reutilizá-las em qualquer lugar. Os textos text: são convertidos em chaves e, se for o caso, traduzidos. Abaixo você vê uma parte do 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
...
Posso criar uma ação personalizada para qualquer coisa e tratá-la em Lua. A lista de ações no momento é esta.
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
Processamento das ações
Os arquivos yaml são convertidos para json e depois para arquivos messagepack, que são muito menores e processados muito mais rápido.
Depois vem o trabalho de verdade, só que não dá para mostrar isso direito aqui, porque o action.lua tem cerca de 600 linhas de código, e ainda existem o action-event.lua e o action-status.lua.
No action-event.lua fica a lógica que verifica se o jogador clicou em um botão de ação como ‘pegar’ e depois, por exemplo, em uma maçã, ‘selectedItem’. Ou ‘dar para’ > selectedActor. Ali está descrita toda a lógica do que um jogador pode fazer (junto com uma ação descrita em yaml). No fim, a ação é executada.
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
}
)
O action.lua tem coisa demais, mas abaixo estão algumas partes importantes. Para cada tipo de ação yaml crio um objeto especial com toda a lógica. Esse objeto é reutilizado o tempo todo.
-- 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
Som
Um exemplo é o tipo de ação audio. A classe com a lógica fica em framework.core.action.audio.lua e é acionada pelos arquivos action/.yml desta forma
*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
Para terminar
Tudo foi montado para que a engine3 e o jogo fiquem separados. Isso é necessário para manter tudo gerenciável. Se você programasse todas as ações de forma fixa no jogo, teria problemas enormes. É melhor criar funções genéricas que consigam processar todas as ações.