Godot Cutscene Basics: Build a 6-Second Boss Entrance With AnimationPlayer and Make It Skippable

Created: 2026-09-23Last updated: 2026-10-10

Build a 6-second boss entrance with AnimationPlayer, and make it end in the same state whether you watch it to the end or skip it.

A presentation lasting a few seconds, like a boss's entrance (a cutscene), is built in AnimationPlayer by lining up the door, the camera, and the boss's movements on a single timeline. It's standard to also add a skip, so players can jump past it from the second time on.

But AnimationPlayer doesn't stop input, so the player can walk around in the middle of the cutscene. Build the skip with stop(), and control comes back with the door still shut and the boss still gone.

This article covers building a 6-second boss entrance so that it ends in the same state even when skipped.

Along a 6-second timeline, the door opens, the camera moves in on the boss, and the boss roars. From the Enter key at the side, an arrow jumps straight to the last moment

What You'll Learn

  • How to split what you leave to the animation (the state that remains at the end) from what you switch and restore yourself (input and the camera)
  • When to use property tracks and when to use method tracks
  • Why a skip sets seek()'s third argument to true, and why it doesn't use stop()
  • Ending with the same function whether the cutscene is watched to the end or skipped

Tested with: Godot 4.7

Sponsored

What you leave to the animation and what you switch yourself

In AnimationPlayer, you can line up as many tracks as you like, each deciding a node property over time. Put the door's position, the camera's position, and the boss's visibility on one timeline, and the 6-second cutscene becomes a single animation. How to set keyframes is in AnimationPlayer Basics.

But the animation only decides the properties you put in tracks. It doesn't touch the player's input or which camera is showing the scene. You switch these two in the function that starts the cutscene, and put them back in the function that ends it.

Four tracks on a 6-second timeline: door, cutscene camera, boss, and subtitle. Outside it, the start function switches input and the active camera, and the end function puts them back

When an animation plays to the end, the keyed properties stay at their last values. The door stays open and the boss stays out. Only write code to put things back when you want them put back.

In other engines: for building presentations, AnimationPlayer, which lines up multiple nodes' movements and function calls on a timeline, plays a role close to Unity's Timeline or Unreal Engine's Sequencer.

Property tracks (the state that remains at the end)

The door has opened, the boss has appeared, the subtitle has disappeared. Things you want to remain after the cutscene ends go in property tracks. The door uses position, the boss uses visible, and the subtitle uses visible too.

The door, boss, and subtitle are property tracks, and the last key's value remains through 6 seconds. The roar sounds at a single point at 3 seconds on a method track

Properties with no in-between values, like visible, get tracks created with the Discrete update mode (switching instantly at the key's time). For the boss, set a false key at 0 seconds and a true key at 2 seconds, and it appears at 2 seconds.

Avoid having a method track call add_child() (the function that adds a node to the scene) to bring the boss out. With the skip explained later, that call gets skipped and the boss is missing. Place the boss in the room from the start and show it with visible. visible only controls whether it's drawn. For a boss with collision or movement, keep its Process Mode (whether the node runs every frame) at Disabled until the cutscene ends, then turn it back on in _finish() to start the fight.

Method tracks (events at a single moment)

Events that only need to happen once at a given moment, like the roar sound effect, go in a method track (a track that calls a function at a set time). The key at 3 seconds calls Main's play_roar().

# called from the method track at 3.0 seconds
func play_roar() -> void:
	if running:                       # only play it while the cutscene is running
		roar_sound.play()

if running: is there because by default, a method track's call isn't executed on the spot but arrives a little later (AnimationPlayer's Callback Mode › Method defaults to Deferred). Even if the call arrives right after a skip, nothing plays once the cutscene has ended.

play_roar() is called from the method key at 3 seconds. The sound plays if the cutscene is still running, and doesn't if it already ended with a skip

Switching input and the camera

In the function that starts the cutscene, stop the player and switch to the cutscene camera. In the function that ends it, put those two back.

func _start() -> void:
	seen = true                        # mark it "seen" as soon as it starts
	running = true
	player.set_physics_process(false)  # keep the player still during the cutscene
	cutscene_camera.make_current()     # switch to the cutscene camera
	cutscene.play("boss_intro")

set_physics_process(false) stops that node's _physics_process(). If movement is kept in _physics_process(), this one line stops the player. Only one camera shows the screen: the camera that last called make_current(). Call make_current() on the player's camera in the end function and it switches back.

The start function stops input and switches the camera, and whether you watch to the end or skip with Enter, the same end function puts them back

Call the end function both when the cutscene plays to the end (animation_finished) and when it's skipped. So that it only runs once even if called twice, check running at the top. Set the "seen" flag, seen, when the cutscene starts, not when it ends. Set it the moment it starts, and whatever happens partway through, it never starts a second time.

Sponsored

Skipping (jumping to the last moment with seek)

To skip, move the animation to its last moment, then call the end function.

# jump to the last moment. The third true skips processing method and audio tracks
cutscene.seek(cutscene.current_animation_length, true, true)
_finish()

current_animation_length is the length of the animation currently playing (6 seconds here). The second argument (update) set to true applies the values at the destination right away. The door goes to its fully open position, and the boss's visible becomes the last key's true.

The third argument (update_only) set to true skips processing method and audio tracks. Leave it out, and the last method key up to the destination time (here, play_roar() at 3 seconds) is called once, and audio tracks are processed too. This article's play_roar() stays silent thanks to the running check. Functions without such a check, and audio tracks, run the moment you skip.

Three ways to skip. With seek's third argument true too, the door opens, the boss is out, and it's quiet. Leave out the third and a method from the middle gets called. With stop(), the door closes and the boss disappears

Use stop() to skip, and the door goes back to its closed position, with the boss and subtitle hidden. stop() with no argument reapplies the values from the start of the animation and then stops. When stopping in the end function, use stop(true) to stop with the last values kept.

Seeking to the last moment doesn't emit the animation_finished signal. That's why the skip handling calls _finish() itself.

Since you jump with the third argument set to true, method keys in the skipped stretch are never called. So only put events that are fine to skip in method tracks. Things that must happen after the cutscene, like starting the boss fight, go in _finish(), not in a key at the last moment.

Hands-On: Build the 6-Second Boss Entrance and Skip It With Enter

A boss fight in an action game, a key event in an RPG, the moment a door opens in a horror game—showing a fixed few seconds and then handing control back is built the same way. Pass the room's entrance, and the door opens, the camera moves in on the boss to the right, the boss roars, and the camera comes back. Press Enter (ui_accept; Space works too) and it jumps to the final state on the spot.

Passing the entrance, the door opens upward, the camera moves right, the boss appears to the right of the door and roars, and the camera comes back

① What to place

Start with a new 2D project. The root of main.tscn is a Node2D (named Main, set as the main scene), with these as children.

  • Player (CharacterBody2D, position (200, 300)). Children: Sprite2D (icon.svg, Scale 0.5), CollisionShape2D (CircleShape2D, Radius 32), and PlayerCamera (Camera2D)
  • Trigger (Area2D, position (450, 300)). Child: CollisionShape2D (RectangleShape2D, Size (40, 400)). An invisible line at the entrance
  • Door (Sprite2D, position (900, 300), icon.svg). A door that's visuals only
  • Boss (Sprite2D, position (1100, 300), icon.svg, Visible off)
  • CutsceneCamera (Camera2D, position (450, 300))
  • UI (CanvasLayer, a layer that stays at the same place on screen even when the camera moves). Child: Subtitle (Label, Text "ROAARR!", Position (500, 520), Visible off)
  • RoarSound (AudioStreamPlayer). Put a sound effect you have (.wav or .ogg) in Stream. The cutscene works without one
  • Cutscene (AnimationPlayer)

② Put it together

The player only moves with the arrow keys.

# player.gd (attach to Player)
extends CharacterBody2D

func _physics_process(_delta: float) -> void:
	velocity = Input.get_vector("ui_left", "ui_right", "ui_up", "ui_down") * 200.0
	move_and_slide()

Main handles starting, skipping, and ending the cutscene.

# main.gd (attach to Main)
extends Node2D

@onready var player: CharacterBody2D = $Player
@onready var player_camera: Camera2D = $Player/PlayerCamera
@onready var cutscene_camera: Camera2D = $CutsceneCamera
@onready var cutscene: AnimationPlayer = $Cutscene
@onready var roar_sound: AudioStreamPlayer = $RoarSound

var seen := false        # once seen, don't play it again
var running := false     # whether the cutscene is in progress

func _ready() -> void:
	player_camera.make_current()
	$Trigger.body_entered.connect(_on_trigger_body_entered)
	cutscene.animation_finished.connect(_on_cutscene_finished)

func _on_trigger_body_entered(body: Node2D) -> void:
	if body == player and not seen:
		_start()

# stop input and switch the camera
func _start() -> void:
	seen = true                        # mark it "seen" as soon as it starts
	running = true
	player.set_physics_process(false)  # keep the player still during the cutscene
	cutscene_camera.make_current()
	cutscene.play("boss_intro")

func _unhandled_input(event: InputEvent) -> void:   # input the UI didn't use arrives here
	if running and event.is_action_pressed("ui_accept"):
		# jump to the last moment. The third true skips processing method and audio tracks
		cutscene.seek(cutscene.current_animation_length, true, true)
		_finish()                      # this seek doesn't emit animation_finished

func _on_cutscene_finished(anim_name: StringName) -> void:
	if anim_name == "boss_intro":
		_finish()

# whether watched to the end or skipped, it comes here
func _finish() -> void:
	if not running:
		return                         # run only once even if called twice
	running = false
	cutscene.stop(true)                # stop with the last values kept (no argument goes back to the start)
	roar_sound.stop()
	player_camera.make_current()       # switch back to the original camera
	player.set_physics_process(true)   # give input back

# called from the method track at 3.0 seconds
func play_roar() -> void:
	if running:
		roar_sound.play()

After attaching the script and saving, create a new animation boss_intro in Cutscene. Its length is 6.0 seconds, with looping off. There are six tracks.

TrackKeys
Door:position0 s (900, 300) → 1 s (900, 150)
CutsceneCamera:position1 s (450, 300) → 3 s (1000, 300) → 5 s (1000, 300) → 6 s (450, 300)
Boss:visible (Discrete)0 s false, 2 s true
Boss:scale3 s (1, 1) → 3.5 s (1.3, 1.3) → 4 s (1, 1)
UI/Subtitle:visible (Discrete)0 s false, 3 s true, 5 s false
Method (target Main)3 s play_roar()

Create the method track by choosing Call Method Track from Add Track. Set Main as the target. Right-click at the 3-second position and choose Insert Key, and you can pick play_roar from the list of functions.

The 6-second boss_intro timeline. The door from 0 to 1 s, the camera moves in from 1 to 3 s and back from 5 to 6 s, the boss appears at 2 s, a roar key at 3 s, and the subtitle from 3 to 5 s

③ Run it and check

Comparing watching to the end with pressing Enter at 0.5 seconds. Walking right, both have the door open and the boss out. The skipped one has no roar or subtitle

Run Main. Check three things.

First, walk right with the arrow keys. The cutscene starts when you touch the invisible range at the entrance (you can see the range with Visible Collision Shapes in the Debug menu). The door opens upward over 1 second, and the camera moves right toward the boss. At 2 seconds the boss appears to the right of the door, and at 3 seconds it roars and the subtitle appears. During this time, the player doesn't move even if you press the arrow keys. If it does, the movement code is outside _physics_process().

Next, at 6 seconds the camera returns to the player and you can walk again right away. The boss is off screen, but walk right and the door is still open and the boss is still out. Go back through the entrance and pass it again, and the cutscene doesn't play.

To finish, restart the game and press Enter while the door is partway open (around 0.5 seconds). It skips the subtitle and the roar, the camera returns to the player, and you can walk right away. Walk right from there, and just as when you watched to the end, the door is fully open and the boss is out. If the door is shut and the boss is missing, the skip is calling stop() with no argument.

Two points.

  • Put the state that remains at the end in property tracks: the door's position and the boss's visibility are on tracks, so the skip only needs to jump to the last moment to reach the same state. You don't have to write a second set of code just for skipping
  • One end function: whether watched to the end or skipped, it goes through _finish(), so you never forget to restore the camera and input. If you move the player with the animation during a cutscene, the multi-track setup in AnimationPlayer Advanced works
Sponsored

Troubleshooting

SymptomCauseFix
During the cutscene, walking stops but jumping and attacking still workThose inputs are received outside _physics_process(), such as in _unhandled_input()Also call set_process_unhandled_input(false) during the cutscene, and set it back to true in the end function
A door with collision opens visually only, and you can't pass throughThe animation moves only the visual Sprite2D, and the collision stays in its original placeMake the door an AnimatableBody2D (a physics body meant to be moved by animation) so the collision moves with it. Also set Cutscene's Callback Mode › Process (the step at which the animation writes values) to Physics
Playing the animation in the editor, nothing happens at the method keyIn the editor's preview, method tracks aren't calledRun the game and start the cutscene to check. If it still doesn't work, check the target node and the function name
After checking the animation in the editor, the scene is saved with the door openThe values you checked remained in the scene. The RESET animation (the original values reapplied when saving) doesn't hold the closed valuesPut the values "door closed, boss and subtitle hidden" in RESET, and leave Cutscene's Reset On Save on

Bonus: Good to Know First

  • Remember that it was "seen": reload the room scene and seen is lost, and the door and boss go back to their initial state. In a game where you go in and out of rooms, keep seen in an Autoload. If it was seen, call _start() in _ready(), followed by the same seek() and _finish() as the skip. It starts from the final state, and the cleanup written in _finish() isn't missed either. How to put it in a save is in Save and Load Basics
  • Don't pause the game and run only the cutscene: pause the whole tree without any settings, and AnimationPlayer and the Enter input stop along with it. Stop only the things you don't want moving during the cutscene. To stop enemies and bullets too, put them in a group, set_physics_process(false) them all together in the start function, and restore them in the end function. How pausing and Process Mode relate is in Process Mode Basics
  • To insert dialogue, make the cutscene wait partway: to insert text that advances line by line, pause() at that time, and play() to continue once the conversation ends. How to build conversations is in Dialogue System Basics

Summary

  • What you leave to the animation and what you switch yourself: the door, boss, and subtitle are the animation's; input and the camera belong to the start and end functions
  • The state that remains at the end goes in property tracks: the last values remain after playback ends. Show the boss with visible, not add_child()
  • Events go in method tracks: only put things that are fine to skip there. The call can arrive a little late, so check running before playing
  • Skip with seek(length, true, true): leave out the third argument and methods from the middle get called; with stop() it goes back to the initial state
  • One end function: call it from both animation_finished and the skip, and turn away the second call with running

In your game, which cutscene will players most want to skip the second time around?

Read nextNext in the learning path
Animation

Smooth Animation with Tween

Read this note
Godot Notes in this section104