記事のコードはLuaで記述されていますが、他の言語で簡単に記述できます(コルーチンはすべての言語ではないため、コルーチンを使用する方法を除きます)。
この記事では、次の形式のカットシーンを作成するメカニズムを作成する方法を示します。
local function cutscene(player, npc) player:goTo(npc) if player:hasCompleted(quest) then npc:say("You did it!") delay(0.5) npc:say("Thank you") else npc:say("Please help me") end end
エントリー
アクションシーケンスは、ビデオゲームでよく見られます。 たとえば、カットシーンでは、キャラクターは敵に会い、彼に何かを言い、敵が答えます。 アクションのシーケンスは、ゲームプレイで見つけることができます。 このgifを見てください:
1.ドアが開きます
2.キャラクターが家に入る
3.ドアが閉まる
4.画面が徐々に暗くなる
5.レベルの変更
6.画面が明るく消える
7.キャラクターがカフェに入る
アクションのシーケンスを使用して、NPCの動作のスクリプトを作成したり、ボスが次々にいくつかのアクションを実行するボス戦を実装したりすることもできます。
問題
標準的なゲームループの構造により、アクションシーケンスの実装が困難になります。 次のゲームループがあるとします。
while game:isRunning() do processInput() dt = clock.delta() update(dt) render() end
次のカットシーンを実装します:プレイヤーがNPCに近づくと、NPCが「あなたはやった!」と言い、少し間をおいて「ありがとう!」と言います。 理想的な世界では、次のように書きます。
player:goTo(npc) npc:say("You did it!") delay(0.5) npc:say("Thank you")
そして、ここで私たちは問題に直面しています。 手順を完了するには時間がかかります。 一部のアクションは、プレーヤーからの入力を待つこともあります(たとえば、ダイアログボックスを閉じるため)。
delay
機能の代わりに、同じ
sleep
呼び出すことはできません-ゲームがフリーズしたように見えます。
問題を解決するためのいくつかのアプローチを見てみましょう。
bool、enum、ステートマシン
一連のアクションを実装する最も明白な方法は、bool、line、またはenumに現在の状態に関する情報を保存することです。 コードは次のようになります。
function update(dt) if cutsceneState == 'playerGoingToNpc' then player:continueGoingTo(npc) if player:closeTo(npc) then cutsceneState = 'npcSayingYouDidIt' dialogueWindow:show("You did it!") end elseif cutsceneState == 'npcSayingYouDidIt' then if dialogueWindow:wasClosed() then cutsceneState = 'delay' end elseif ... ... -- ... end end
この方法では、スパゲッティコードとif-else式の長いチェーンが簡単に発生するため、この方法で問題を解決することは避けてください。
アクションリスト
アクションリストは、ステートマシンに非常に似ています。 アクションリストは、次々に実行されるアクションのリストです。 ゲームループでは、現在のアクションに対して
update
関数が呼び出されます。これにより、アクションに時間がかかっても、入力を処理してゲームをレンダリングできます。 アクションが完了したら、次へ進みます。
実装するカットシーンでは、GoToAction、DialogueAction、およびDelayActionのアクションを実装する必要があります。
さらなる例として、LuaのOOPにミドルクラスライブラリを使用します。
DelayAction
実装方法は
DelayAction
。
-- function DelayAction:initialize(params) self.delay = params.delay self.currentTime = 0 self.isFinished = false end function DelayAction:update(dt) self.currentTime = self.currentTime + dt if self.currentTime > self.delay then self.isFinished = true end end
ActionList:update
関数は次のようになります。
function ActionList:update(dt) if not self.isFinished then self.currentAction:update(dt) if self.currentAction.isFinished then self:goToNextAction() if not self.currentAction then self.isFinished = true end end end end
そして最後に、カットシーン自体の実装:
function makeCutsceneActionList(player, npc) return ActionList:new { GoToAction:new { entity = player, target = npc }, SayAction:new { entity = npc, text = "You did it!" }, DelayAction:new { delay = 0.5 }, SayAction:new { entity = npc, text = "Thank you" } } end -- ... - actionList:update(dt)
注 :Luaでは、
someFunction({ ... })
呼び出しは次のように行うことができます:
someFunction{...}
。 これにより、
DelayAction:new({delay = 0.5})
代わりに
DelayAction:new({delay = 0.5})
を記述できます。
それはずっと良く見えます。 コードはアクションのシーケンスを明確に示しています。 新しいアクションを追加する場合は、簡単に実行できます。
DelayAction
ようなクラスを作成して、カットシーンの作成をより簡単にします。
アクションリストに関するショーンミドルディッチのプレゼンテーションをご覧になることをお勧めします。アクションリストには、より複雑な例が記載されています。
通常、アクションリストは非常に便利です。 私はかなり長い間ゲームにそれらを使用し、全体的に満足でした。 しかし、このアプローチには欠点もあります。 少し複雑なカットシーンを実装したいとしましょう:
local function cutscene(player, npc) player:goTo(npc) if player:hasCompleted(quest) then npc:say("You did it!") delay(0.5) npc:say("Thank you") else npc:say("Please help me") end end
if / elseシミュレーションを実行するには、非線形リストを実装する必要があります。 これはタグを使用して実行できます。 一部のアクションにはタグを付けることができ、その後、条件によっては、次のアクションに移動する代わりに、目的のタグを持つアクションに移動できます。 これは機能しますが、上記の関数ほど読み書きが簡単ではありません。
Luaコルーチンは、このコードを現実のものにします。
コルーチン
ルアのコルアの基本
コルチンは、一時停止してから再開できる機能です。 コルーチンは、メインプログラムと同じスレッドで実行されます。 コルーチン用の新しいスレッドは作成されません。
coroutine.yield
を一時停止するには、
coroutine.yield
を呼び出して、resume-
coroutine.resume
を再開する必要があります。 簡単な例:
local function f() print("hello") coroutine.yield() print("world!") end local c = coroutine.create(f) coroutine.resume(c) print("uhh...") coroutine.resume(c)
プログラム出力:
こんにちは うーん... 世界
仕組みは次のとおりです。 まず、coroutine.createで
coroutine.create
を作成します。 この呼び出しの後、コルチンは開始しません。 これを実現するには、
coroutine.resume
を使用して実行する必要があります。 次に、
f
関数が呼び出され、「hello」を書き込み、
coroutine.yield
一時停止します。 これは
return
と似ていますが、
coroutine.resume
を使用して
f
実行を再開できます。
coroutine.yield
呼び出すときに引数を渡すと、それらは「メインストリーム」の
coroutine.resume
への対応する呼び出しの戻り値になります。
例:
local function f() ... coroutine.yield(42, "some text") ... end ok, num, text = coroutine.resume(c) print(num, text) -- will print '42 "some text"'
ok
は、コルーチンの状態を知ることができる変数です。
ok
が
true
、コルーチンですべてが正常で、内部でエラーは発生していません。 それに続く戻り値(
num
、
text
)は、
yield
に渡したのと同じ引数です。
ok
が
false
場合、コルーチンで何か問題が
error
ています。たとえば、その内部で
error
関数が呼び出されました。 この場合、2番目の戻り値はエラーメッセージになります。 エラーが発生するコルーチンの例:
local function f() print(1 + notDefined) end c = coroutine.create(f) ok, msg = coroutine.resume(c) if not ok then print("Coroutine failed!", msg) end
結論:
コルーチンが失敗しました! 入力:4:nil値で算術を実行しようとします(グローバル「notDefined」)
coroutine.statusを呼び出すと、コルーチンのステータスを取得できます。 コルチンは次の状態にある可能性があります。
- 「実行中」-コルーチンは現在実行中です。 coroutine.statusはコルチン自体から呼び出されました
- 「一時停止」-Corutinは一時停止されたか、開始されたことがない
- 「正常」-コルチンはアクティブですが、実行されていません。 つまり、コルチンはそれ自体の中に別のコルチンを放出した
- 「死」-コルチンは実行を完了する(つまり、コルチン内の機能が完了する)
今、この知識の助けを借りて、コルーチンに基づいてアクションとカットシーンのシーケンスのシステムを実装できます。
コルチンを使用してカットシーンを作成する
新しいシステムでの基本
Action
クラスは次のようになります。
function Action:launch() self:init() while not self.finished do local dt = coroutine.yield() self:update(dt) end self:exit() end
このアプローチはアクションリストに似ています。アクションの
update
関数は、アクションが完了するまで呼び出されます。 ただし、ここではコルーチンを使用し、ゲームループの各反復で
yield
を実行します(
Action:launch
何らかのコルーチンから
Action:launch
が呼び出されます)。 ゲームループの
update
どこかで、次のように現在のカットシーンの実行を再開します。
coroutine.resume(c, dt)
そして最後に、カットシーンを作成します。
function cutscene(player, npc) player:goTo(npc) npc:say("You did it!") delay(0.5) npc:say("Thank you") end -- - ... local c = coroutine.create(cutscene, player, npc) coroutine.resume(c, dt)
delay
機能の実装方法は次のとおりです。
function delay(time) action = DelayAction:new { delay = time } action:launch() end
このようなラッパーを作成すると、カットシーンコードの可読性が大幅に向上します。
DelayAction
ように実装されます。
-- Action - DelayAction local DelayAction = class("DelayAction", Action) function DelayAction:initialize(params) self.delay = params.delay self.currentTime = 0 self.isFinished = false end function DelayAction:update(dt) self.currentTime = self.currentTime + dt if self.currentTime >= self.delayTime then self.finished = true end end
この実装は、アクションリストで使用したものと同じです! それでは、
Action:launch
functionをもう一度見てみましょう。
function Action:launch() self:init() while not self.finished do local dt = coroutine.yield() -- the most important part self:update(dt) end self:exit() end
ここでの主なものは、アクションが完了するまで実行される
while
です。 次のようになります。
goTo
関数を見てみましょう。
function Entity:goTo(target) local action = GoToAction:new { entity = self, target = target } action:launch() end function GoToAction:initialize(params) ... end function GoToAction:update(dt) if not self.entity:closeTo(self.target) then ... -- , AI else self.finished = true end end
コルーチンはイベントに適しています。
WaitForEventAction
クラスを実装し
WaitForEventAction
。
function WaitForEventAction:initialize(params) self.finished = false eventManager:subscribe { listener = self, eventType = params.eventType, callback = WaitForEventAction.onEvent } end function WaitForEventAction:onEvent(event) self.finished = true end
この関数には、
update
メソッドは必要ありません。 必要なタイプのイベントを受け取るまで実行されます(何もしませんが...)。 このクラスの実用的なアプリケーションは次のとおりです
say
関数の実装:
function Entity:say(text) DialogueWindow:show(text) local action = WaitForEventAction:new { eventType = 'DialogueWindowClosed' } action:launch() end
シンプルで読みやすい。 ダイアログボックスが閉じると、「DialogueWindowClosed」タイプのイベントが送出されます。 sayアクションが終了し、次のアクションの実行が開始されます。
コルーチンを使用して、ノンリニアカットシーンとダイアログツリーを簡単に作成できます。
local answer = girl:say('do_you_love_lua', { 'YES', 'NO' }) if answer == 'YES' then girl:setMood('happy') girl:say('happy_response') else girl:setMood('angry') girl:say('angry_response') end
この例では、
say
関数は前に示した関数よりも少し複雑です。 ダイアログでプレイヤーの選択を返しますが、実装するのは難しくありません。 たとえば、
WaitForEventAction
は
WaitForEventAction
で使用できます。これはPlayerChoiceEventイベントをキャッチし、プレーヤーの選択を返します。プレーヤーの情報はイベントオブジェクトに含まれます。
少し複雑な例
コルーチンを使用すると、チュートリアルや小さなクエストを簡単に作成できます。 例:
girl:say("Kill that monster!") waitForEvent('EnemyKilled') girl:setMood('happy') girl:say("You did it! Thank you!")
コルーチンはAIにも使用できます。 たとえば、モンスターが何らかの軌道に沿って移動する関数を作成できます。
function followPath(monster, path) local numberOfPoints = path:getNumberOfPoints() local i = 0 -- while true do monster:goTo(path:getPoint(i)) if i < numberOfPoints - 1 then i = i + 1 -- else -- i = 0 end end end
モンスターがプレイヤーを見ると、コルーチンの実行を停止して削除できます。 したがって、
followPath
内の無限ループ(
while true
)は実際には無限ではありません。
corutinを使用すると、「並行」アクションを実行できます。 両方のアクションが完了するまで、カットシーンは次のアクションに進みません。 たとえば、女の子と猫が異なる速度で友人のポイントに行くカットシーンを作成します。 彼らが彼女に来た後、猫は「ニャー」と言います。
function cutscene(cat, girl, meetingPoint) local c1 = coroutine.create( function() cat:goTo(meetingPoint) end) local c2 = coroutine.create( function() girl:goTo(meetingPoint) end) c1.resume() c2.resume() -- waitForFinish(c1, c2) -- cat:say("meow") ... end
ここで最も重要な部分は、
WaitForFinishAction
クラスのラッパーである
waitForFinish
関数です。これは、次のように実装できます。
function WaitForFinishAction:update(dt) if coroutine.status(self.c1) == 'dead' and coroutine.status(self.c2) == 'dead' then self.finished = true else if coroutine.status(self.c1) ~= 'dead' then coroutine.resume(self.c1, dt) end if coroutine.status(self.c2) ~= 'dead' then coroutine.resume(self.c2, dt) end end
N番目のアクションの同期を許可する場合、このクラスをより強力にすることができます。
すべてのコルーチンの実行が完了するのを待つ代わりに、コルーチンの1つが完了するまで待機するクラスを作成することもできます。 たとえば、ミニゲームのレースで使用できます。 コルーチン内では、ライダーの1人がフィニッシュラインに到達し、一連のアクションを実行するまで待機します。
コルチンの長所と短所
コルーチンは非常に便利なメカニズムです。 それらを使用して、カットシーンやゲームプレイコードを記述し、読みやすく変更しやすくすることができます。 この種のカットシーンは、改造者やプログラマーではない人(ゲームやレベルのデザイナーなど)によって簡単に作成できます。
そして、これらはすべて1つのスレッドで実行されるため、同期や競合状態に問題はありません。
このアプローチには欠点があります。 たとえば、保存に問題がある可能性があります。 ゲームにコルーチンを使用した長いチュートリアルが実装されているとします。 このチュートリアルでは、プレーヤーは保存できません。 これを行うには、コルーチンの現在の状態(スタック全体と内部の変数の値を含む)を保存する必要があります。これにより、保存からさらにロードしたときにチュートリアルを続行できます。
( 注 : PlutoLibraryライブラリを使用すると、コルーチンをシリアル化できますが、ライブラリはLua 5.1でのみ機能します)
この問題は、カットシーンでは発生しません。 通常、カットシーンの途中でのゲームでは許可されていません。
長いチュートリアルの問題は、細かく分割すれば解決できます。 プレーヤーがチュートリアルの最初の部分を通過し、チュートリアルを続行するには別の部屋に移動する必要があるとします。 この時点で、チェックポイントを作成するか、プレーヤーに保存する機会を与えることができます。 保存では、「プレイヤーはチュートリアルのパート1を完了しました」などのように書きます。 次に、プレーヤーはチュートリアルの2番目のパートを実行します。このパートでは、別のコルーチンを使用します。 など...ロードするとき、プレイヤーが通過しなければならない部分に対応するコルーチンの実行を開始します。
おわりに
ご覧のとおり、一連のアクションとカットシーンを実装するには、いくつかの異なるアプローチがあります。 コルーチンアプローチは非常に強力であり、開発者と共有できることをうれしく思います。 この問題に対する解決策があなたの人生を楽にし、あなたのゲームに壮大なカットシーンを作ることを可能にすることを願っています。