Och action...

Allt är aktion och reaktion

Ja, jag använder en vit bakgrund

Yaml

Eftersom jag inte ville skriva en speleditor för att spara tid bestämde jag mig för att använda yaml-filer. Fördelen är att de också kan använda referenser för att undvika upprepning. Och i framtiden skulle jag kunna göra en editor som exporterar yaml-filer.

I yaml-filerna behövde varje handling beskrivas som kunde hända per scen och figur. Här nedanför ser du en sådan bit av scene20.yml. Där ser du att det regnar i 5 sekunder och att en text visas när scenen startar. Du ser också några hover-handlingar och vad som ska hända när du till exempel väljer handlingen “öppna” “informationstavla”.

# 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 går också att ge en entity andra handlingar än de globala handlingarna för scenen. Det gör man genom att lägga till följande längst ner 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?

Referenserna till alla entities, till exempel *dolmens, finns beskrivna i assets/refs.yaml och kan återanvändas överallt. Texterna text: görs om till nycklar och översätts vid behov. Här nedanför 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
...

Jag kan göra en egen handling av vad som helst och fånga upp den i Lua. Listan med handlingar ser just nu ut så här.

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

Bearbetning av handlingarna

yaml-filerna görs om till json och sedan till messagepack-filer, eftersom de är mångdubbelt mindre och bearbetas mångdubbelt snabbare.

Sedan kommer det riktiga arbetet, men det går inte riktigt att visa tydligt här, eftersom action.lua är ungefär 600 rader kod och det dessutom finns action-event.lua och action-status.lua.

I action-event.lua finns logiken för om spelaren har klickat på en handlingsknapp som ’ta’ och sedan till exempel ett äpple, ‘selectedItem’. Eller ‘ge till’ > selectedActor. Här beskrivs all logik för vad en spelare kan göra (tillsammans med en beskriven yaml-handling). Till slut utförs handlingen.

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 för mycket i action.lua, men här nedanför finns några viktiga delar. Jag gör ett särskilt objekt av varje yaml-handlingstyp, med all logik i. Det objektet återanvänds hela 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

Ljud

Ett exempel är handlingstypen audio. Logikklassen för den finns i framework.core.action.audio.lua och styrs så här från action/.yml-filerna

*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

Till sist

Allt är uppbyggt så att engine3 och spelet är åtskilda. Det behövs för att det ska gå att hantera. Om man programmerade alla handlingar på ett fast sätt i spelet skulle man stöta på enorma problem. Det är bättre att göra generiska funktioner som kan hantera alla handlingar.