Und Action...

Alles ist Aktion und Reaktion

Ja, ich benutze einen weißen Hintergrund

Yaml

Da ich keinen Spieleditor schreiben wollte, um Zeit zu sparen, hatte ich beschlossen, yaml-Dateien zu verwenden. Der Vorteil ist, dass sie auch Referenzen nutzen können, um Wiederholungen zu vermeiden. Und in Zukunft könnte ich eventuell einen Editor bauen, der yaml-Dateien exportiert.

In den yaml-Dateien musste jede Aktion beschrieben werden, die pro Szene und Figur passieren kann. Unten siehst du so einen Ausschnitt aus scene20.yml. Darin siehst du, dass es 5 Sekunden regnet und beim Start der Szene ein Text erscheint. Außerdem siehst du ein paar Hover-Aktionen und was passieren soll, wenn du zum Beispiel die Aktion “öffne” “Informationstafel” wählst.

# 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

Eine Entity kann auch andere Aktionen bekommen als die globalen Aktionen der Szene. Dafür ergänzt man unten in der Datei Folgendes.

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

Die Referenzen aller Entities, zum Beispiel *dolmens, sind in assets/refs.yaml beschrieben, und ich kann sie überall wiederverwenden. Die Texte text: werden in Schlüssel umgewandelt und bei Bedarf übersetzt. Unten siehst du einen Teil von 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
...

Ich kann für alles eine eigene Aktion anlegen und in Lua abfangen. Die Liste der Aktionen sieht im Moment so aus.

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

Verarbeitung der Aktionen

Die yaml-Dateien werden in json umgewandelt und danach in messagepack-Dateien, denn die sind um ein Vielfaches kleiner und werden um ein Vielfaches schneller verarbeitet.

Danach kommt die eigentliche Arbeit, aber die lässt sich hier nicht gut zeigen, denn action.lua hat etwa 600 Zeilen Code, und dazu gibt es noch action-event.lua und action-status.lua.

In action-event.lua steht die Logik, ob der Spieler einen Aktionsbutton wie ’nimm’ angeklickt hat und dann zum Beispiel einen Apfel, ‘selectedItem’. Oder ‘gib’ > selectedActor. Hier ist die ganze Logik beschrieben, was ein Spieler alles tun kann (zusammen mit einer beschriebenen yaml-Aktion). Am Ende wird die Aktion ausgeführt.

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

In action.lua steht zu viel, aber unten ein paar wichtige Teile. Für jeden yaml-Aktionstyp erstelle ich ein spezielles Objekt mit der ganzen Logik. Dieses Objekt wird ständig wiederverwendet.

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

Sound

Ein Beispiel ist der Aktionstyp audio. Die Logikklasse dafür steht in framework.core.action.audio.lua und wird so aus den action/.yml-Dateien angesteuert

*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

Zum Schluss

Alles ist so aufgebaut, dass engine3 und das Spiel voneinander getrennt sind. Das ist nötig, damit es überschaubar bleibt. Wenn man alle Aktionen fest im Spiel programmieren würde, stieße man auf riesige Probleme. Besser ist es, generische Funktionen zu bauen, die alle Aktionen verarbeiten können.