ゲームでのカットシーンと一連のアクションの実装

この投稿では、ビデオゲームに一連のアクションとカットシーンを実装する方法について説明します。 この記事はこの記事の翻訳であり、同じトピックについてはモスクワのルアでプレゼンテーションを行ったので、ビデオをご覧になりたい場合は、こちらでご覧いただけます



記事のコードは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を呼び出すと、コルーチンのステータスを取得できます。 コルチンは次の状態にある可能性があります。





今、この知識の助けを借りて、コルーチンに基づいてアクションとカットシーンのシーケンスのシステムを実装できます。



コルチンを使用してカットシーンを作成する



新しいシステムでの基本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番目のパートを実行します。このパートでは、別のコルーチンを使用します。 など...ロードするとき、プレイヤーが通過しなければならない部分に対応するコルーチンの実行を開始します。



おわりに



ご覧のとおり、一連のアクションとカットシーンを実装するには、いくつかの異なるアプローチがあります。 コルーチンアプローチは非常に強力であり、開発者と共有できることをうれしく思います。 この問題に対する解決策があなたの人生を楽にし、あなたのゲームに壮大なカットシーンを作ることを可能にすることを願っています。



All Articles