gale.cutscene
A small toolkit for scripted, non-interactive scenes, built on
gale.sequence: Cutscene (a sequence of beats that
also ticks/renders any actors involved every frame) and the beats
themselves – ShowImage, PlayAnimation, MoveActor,
SetActorAnimation, Dialogue, and Wait. Every beat is a
gale.sequence.Step, so duration/advance_on_input (and
gale.sequence.StepGroup, to run several beats at once) all just work.
from gale.cutscene import Cutscene, Dialogue, MoveActor, ShowImage
cutscene = Cutscene(
[
ShowImage(pygame.image.load("village_establishing_shot.png"), duration=2.0),
MoveActor(elder, target=(400, 200), duration=1.5),
Dialogue("Elder", "The forest holds many secrets...", font, advance_on_input="confirm"),
Dialogue("Hero", "I'll be careful.", font, advance_on_input="confirm"),
],
actors=[elder, hero],
on_finished=lambda: state_machine.change("play"),
)
# In the game loop:
cutscene.update(dt)
cutscene.render(surface)
cutscene.on_input(input_id, input_data) # forward from wherever input is dispatched
actors are ticked (update(dt))/drawn (render(surface)) every
frame regardless of which beat is currently active, so a character keeps
idling/animating in the background even while another one is delivering
a line – exactly the “characters that are in the scene at that moment
move on their own” a conversation cutscene needs. MoveActor/
SetActorAnimation are duck-typed: any object with a .position
(a pygame.Vector2, or a value pygame.Vector2(...) accepts) and/or
.animation attribute works, whatever character class a game already
has (an Agent, a custom Player class, …).
Running several beats at once
Use gale.sequence.StepGroup to have a beat play while another one
does, instead of one after the other:
from gale.sequence import StepGroup
Cutscene([
StepGroup([
MoveActor(hero, target=(400, 200), duration=1.5),
Dialogue("Hero", "This way!", font),
]),
...
])
“Playing a video”
PlayAnimation plays a gale.animation.Animation as a cutscene beat
– gale’s stand-in for video playback. This is deliberately not real
video-codec (mp4/webm) decoding: gale never depends on a decoder library
(ffmpeg, OpenCV, …), the same way gale.tilemap never depends on
gale.physics. Pre-render the actual footage as a frame sequence (most
video editors, and ffmpeg itself, can export a PNG-per-frame sequence)
and load those the same way any other sprite animation would:
from gale.animation import Animation
from gale.cutscene import PlayAnimation
frames = [pygame.image.load(f"intro_{i:03d}.png") for i in range(90)]
PlayAnimation(Animation(frames, time_interval=1 / 30, loops=1))
Writing your own beats
Anything game-specific (a screen shake, a particle burst, a sound cue
that must finish before continuing, …) is just a Step subclass:
from gale.sequence import Step
class PlaySound(Step):
def __init__(self, sound, **kwargs) -> None:
super().__init__(**kwargs)
self.sound = sound
def enter(self, *args, **kwargs) -> None:
super().enter(*args, **kwargs)
self.channel = self.sound.play()
def is_complete(self) -> bool:
return not self.channel.get_busy() or super().is_complete()