Step 9: Screen Transitions

Add fade-in and fade-out transitions between game states.

What you will learn

  • Creating transitions with StartTransition
  • The FadeToBlack and FadeFromBlack transition styles
  • Advancing and drawing transitions each frame
  • Checking Done() to clean up finished transitions

What changed from Step 8

Both states now have a fade *gosprite64.Transition field. The title screen fades in when it appears and fades out when the player presses Start. The gameplay state fades in when it starts. The changes are small but the visual difference is significant.

TitleState changes

type TitleState struct {
	sm    *gosprite64.StateMachine
	blink int
	show  bool
	fade  *gosprite64.Transition
}

func (s *TitleState) Enter() {
	s.show = true
	s.fade = gosprite64.StartTransition(gosprite64.FadeFromBlack, 30)
}

On Enter, a 30-frame fade-from-black plays. The screen starts fully black and gradually reveals the title over half a second.

When the player presses Start, a fade-to-black begins before switching states:

if gosprite64.IsButtonJustPressed(gosprite64.ButtonA) || gosprite64.IsButtonJustPressed(gosprite64.ButtonStart) {
	s.fade = gosprite64.StartTransition(gosprite64.FadeToBlack, 20)
	s.sm.Switch(&PlayState{sm: s.sm})
}

PlayState changes

func (s *PlayState) Enter() {
	// ... asset loading (unchanged) ...
	s.fade = gosprite64.StartTransition(gosprite64.FadeFromBlack, 30)
}

The gameplay state fades in from black when it starts. This creates a smooth visual bridge: title fades to black, gameplay fades from black.

The transition loop

Both states share the same pattern in Update and Draw:

func (s *PlayState) Update() {
	if s.fade != nil {
		s.fade.Advance()
		if s.fade.Done() {
			s.fade.Stop()
			s.fade = nil
		}
	}
	// ... rest of update ...
}

func (s *PlayState) Draw() {
	// ... draw everything ...
	if s.fade != nil {
		s.fade.Draw()
	}
}

Advance() ticks the transition forward by one frame. Done() returns true when all frames have played. Stop() deactivates it and Draw() renders a semi-transparent black overlay whose opacity changes each frame.

The transition must be drawn last so it appears on top of everything else.

The complete code

The full listing is the same as Step 8 with the fade field added to both states. Here are the key differences:

TitleState - Added fade field. Enter starts FadeFromBlack. Update advances and cleans up the fade. Draw renders the fade overlay last. The button press starts FadeToBlack.

PlayState - Added fade field. Enter starts FadeFromBlack. Update advances and cleans up the fade. Draw renders the fade overlay last.

Transition API reference

Function/MethodWhat it does
StartTransition(style, frames)Create and start a new transition
Advance()Tick forward one frame
Done()True when all frames have played
Stop()Deactivate the transition
Draw()Render the overlay (call last in Draw)
StyleEffect
FadeToBlackScreen gradually goes black (alpha 0 to 255)
FadeFromBlackScreen gradually reveals (alpha 255 to 0)

The duration is in frames. At 60 FPS, 30 frames is half a second and 60 frames is one full second.

Build and run

go generate ./examples/platformer
GOENV=n64.env go1.24.5-embedded build -o examples/platformer/game.elf ./examples/platformer

The title screen now fades in from black when the game starts. Press Start and the screen fades to black, then the gameplay fades in. The transitions are short (20-30 frames) to feel snappy rather than sluggish.