そしてアクション...

すべてはアクションとリアクション

そう、背景は白を使っています

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 とゲームが分かれるように作ってあります。管理しやすくするためには、これが必要です。もしすべてのアクションをゲームの中に決め打ちでプログラムしたら、とんでもない問題にぶつかります。すべてのアクションを処理できる汎用的な関数を作るほうがいいのです。