We make small mobile puzzle games in Godot, and we need a steady supply of vertical gameplay clips for YouTube Shorts and TikTok. Screen recorders gave us dropped frames, desktop notifications in the shot and a different result on every take. So we moved the camera inside the engine. The game renders into an offscreen viewport, and every frame is saved to disk. Then ffmpeg turns the frames into a video.
The idea
- Run Godot with a fixed timestep:
--fixed-fps 30 --disable-vsync. The game advances exactly 1/30 s per frame no matter how slow the capture is, so animations never stutter. - Start the game from a
SceneTreescript (--script res://tools/capture.gd) instead of the main scene. The script builds the scene, drives it and quits when done. - Put the real game scene inside a
SubViewportat the output size (for example 1080×1920), withrender_target_update_mode = SubViewport.UPDATE_ALWAYS. - On each frame, call
RenderingServer.force_draw()and saveviewport.get_texture().get_image().
Minimal capture script
extends SceneTree const OUT := "user://capture" const FRAMES := 300 # 10 s at 30 fps var viewport: SubViewport func _initialize() -> void: DirAccess.make_dir_recursive_absolute(OUT) viewport = SubViewport.new() viewport.size = Vector2i(1080, 1920) viewport.render_target_update_mode = SubViewport.UPDATE_ALWAYS root.add_child(viewport) viewport.add_child(load("res://scenes/main.tscn").instantiate()) record.call_deferred() func record() -> void: for i in FRAMES: await process_frame RenderingServer.force_draw() var img := viewport.get_texture().get_image() img.save_jpg("%s/f_%05d.jpg" % [OUT, i], 0.92) quit(0) Run it:
godot --path . --fixed-fps 30 --disable-vsync --audio-driver Dummy --script res://tools/capture.gd ffmpeg -framerate 30 -i f_%05d.jpg -c:v libx264 -pix_fmt yuv420p -movflags +faststart out.mp4 Driving real input
A recording of an idle menu is boring. Our capture script calls the same functions the drag-and-drop UI uses (begin drag, move, drop) with eased positions, so the clip shows real input, real line clears and the real animations. Every placement and clear is logged to a JSON file with its frame number. A small Python script then adds captions and overlays and lines up sound effects with the logged frames.
Things that bit us
- Saves. The capture plays the game for real, so it would write to the player's save file. We run it on a throwaway copy of the project with private save paths, and the script checks that real saves are never touched.
- Splash screens and first-run tutorials. Skip them in code before you start recording. Otherwise the first seconds of every clip show the same intro.
- Determinism. Seed the piece generator so a "good" run can be re-recorded exactly when you change a caption or the music.
- Disk. 30 fps at 1080×1920 is a lot of JPGs. Delete the frames after encoding.
Result
Every run produces the same stutter-free footage, so re-editing never needs a new recording. We use the same script for Play Store screenshots in several languages and screen sizes.
The clips end up in our Shorts for Blokito, a block puzzle with 300 Adventure levels. If you want to see the output, our games and free browser games are at https://lamian.dev. Questions about the script are welcome in the comments.