Spent some time today wrapping my head around another approach to cutscenes using coroutines based on this blog post by Elias Daler: Redirecting to https://edw.is/
It's a pretty slick approach. The main problem I have with it is that I it doesn't feel as linear (easy to think through) as behavior trees. Perhaps in time it'll be second nature but, for now, it's a real head-tilter thinking through the code path of each call to playdate.update().
With that said, here is the same cutscene but implemented with this new library built on this new coroutine approach.
function warp.cutscene(dt)
local ship <const> = game_state.ship
if game_state.current_system == DATA_SYSTEMS.monto then
current_scene.hud:setInspector("Warp Engaged", "Sena System")
else
current_scene.hud:setInspector("Warp Engaged", "Monto System")
end
-- Reverse direction.
WarpReverseShipDirectionAction(ship)
-- Stop the ship.
WarpStopShipAction(ship)
-- Rotate towards selected system.
WarpRotateShip(45, ship)
-- Accelerate to warp speed.
WarpSpeedAction(40, ship)
-- First warp flash.
FlashScreen(0.1)
-- Pause before next warp flash.
kubrick.Wait(0.1);
-- Flash, load new system, and coast.
kubrick.Together(
function() FlashScreen(0.05) end,
function()
utils.setVectorMagnitude(ship.velocity, 2)
if game_state.current_system == DATA_SYSTEMS.monto then
current_scene:loadSystem(DATA_SYSTEMS.sana);
else
current_scene:loadSystem(DATA_SYSTEMS.monto);
end
current_scene.hud:clearInspector()
end
)
-- Pause before displaying system information.
kubrick.Wait(0.1);
-- Display system information.
current_scene.hud:setInspector(game_state.current_system.name .. " System", "Federation")
end
To run this code I first wrap the function in a coroutine which I've front-ended by under the kubrick namespace:
self.warp_cutscene = kubrick.create(warp.cutscene)
Then on each call to playdate.update I simply check if I should be running my cutscene and if so, update it, then check if it's finished and move on:
if game_state.hyperspace_active then
kubrick.update(self.warp_cutscene, dt)
if kubrick.finished(self.warp_cutscene) then
game_state.hyperspace_active = false
end
end
That's it.
Each of the actions in the cutscene is actually an object that runs from its initializer. I know, yikes, but it's much cleaner than creating the object and then needing to call a method to start the action running. Plus, it makes these actions look more like simple functions.
An implementation of an action is quite simple:
class("WarpSpeedAction").extends(kubrick.Action)
function WarpSpeedAction:update(dt, acceleration, ship)
local a <const> = (acceleration * dt)
ship.velocity.x += math.cos(ship.rotation) * a
ship.velocity.y += math.sin(ship.rotation) * a
local velocity_magnitude <const> = ship.velocity:magnitude()
if velocity_magnitude >= 100 then
self.finished = true
end
end
Each action is assumed to be a process that requires multiple frames. So, each action has an update function (that is typically all you override), perform the logic you need to perform on each frame until complete. Once complete, set Action.finished to true and the coroutine behind the scenes will die and this update function will no longer be called.
So, I like the look of this vs the behavior tree definition from earlier. It still breaks my brain a bit when debugging, but I suspect I'll come around. I'm going to stick with this implementation for now and move on. I named the library Kubrick
because, I don't know, cutscenes are like films or something?
If anyone is interested in this library I expect I'll post it soon. It's surprisingly simple (60 lines) which is another thing to like about this approach.