Og action...

Alt er aksjon og reaksjon

Ja, jeg bruker en hvit bakgrunn

Yaml

Siden jeg ikke ville skrive en spilleditor for å spare tid, bestemte jeg meg for å bruke yaml-filer. Fordelen er at de også kan bruke referanser for å unngå gjentakelser. Og i fremtiden kunne jeg eventuelt lage en editor som eksporterer yaml-filer.

I yaml-filene måtte hver handling som kunne skje per scene og figur, beskrives. Under ser du en slik bit av scene20.yml. Her ser du at det regner i 5 sekunder, og at en tekst vises når scenen starter. Du ser også noen hover-handlinger og hva som skal skje når du for eksempel velger handlingen “åpne” “informasjonstavle”.

# 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

Det er også mulig å gi en entity andre handlinger enn de globale handlingene for scenen. Det gjør man ved å legge til følgende nederst i filen.

*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?

Referansene til alle entities, for eksempel *dolmens, er beskrevet i assets/refs.yaml, og dem kan jeg gjenbruke overalt. Tekstene text: gjøres om til nøkler og blir eventuelt oversatt. Under ser du en del av 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
...

Jeg kan lage en egen handling av hva som helst og fange den opp i Lua. Listen over handlinger ser akkurat nå slik ut.

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

Behandling av handlingene

yaml-filene gjøres om til json og deretter til messagepack-filer, som er mange ganger mindre og behandles mange ganger raskere.

Så kommer det egentlige arbeidet, men det lar seg ikke vise tydelig her, siden action.lua er omtrent 600 linjer kode, og det finnes også action-event.lua og action-status.lua.

I action-event.lua ligger logikken for om spilleren har klikket på en handlingsknapp som ’ta’ og så for eksempel et eple, ‘selectedItem’. Eller ‘gi til’ > selectedActor. Her er all logikken for hva en spiller kan gjøre beskrevet (sammen med en beskrevet yaml-handling). Til slutt blir handlingen utført.

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
  }
)

Det står for mye i action.lua, men under er noen viktige deler. Jeg lager et spesielt objekt for hver yaml-handlingstype, med all logikken i. Dette objektet gjenbrukes hele tiden.

-- 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

Lyd

Et eksempel er handlingstypen audio. Logikklassen for den ligger i framework.core.action.audio.lua og styres slik fra action/.yml-filene

*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

Til slutt

Alt er satt opp slik at engine3 og spillet er adskilt. Det trengs for å holde det håndterbart. Hvis du programmerte alle handlinger på en fast måte i spillet, ville du støtt på enorme problemer. Det er bedre å lage generiske funksjoner som kan behandle alle handlinger.