そしてアクション...
すべてはアクションとリアクション

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