Screen Transitions
A fancy wipe effect that you can use to transition between screens in Godot.
Intro
This takes the work of DDRKirby and wraps it up in the script needed to do something with it in Godot 4.
Together, that script and shader make an effective transition that wipes in a solid colour, lets you swap out the old screen underneath it, and then wipes out that colour to reveal the new screen.
Transition Scene
You’ll first need to create a new scene of two nodes - a root node of a CanvasLayer
and a child node of a ColorRect
.
Then set the anchors of the ColorRect
to be a full rectangle, and its mouse filter to ignore, and attach the following script to the root:
extends CanvasLayer
@export var delay := 1.5
@export var start_color := Color(0.5, 0, 1)
@export var end_color := Color(0, 0, 0)
@export_range(5,50,0.1) var start_size := 30.0
@export_range(5,50,0.1) var end_size := 10.0
@export var demo := true
@onready var background = $ColorRect
signal okay_to_swap
signal finished
func _ready() -> void:
# get the background ready for the first time we call start()
background.color = end_color
background.material.set_shader_parameter("progress", 0.0)
background.material.set_shader_parameter("diamondPixelSize", end_size)
if demo:
demonstrate()
func demonstrate() -> void:
# code just to set up the demo
var screen1 = ColorRect.new()
screen1.color = Color.GREEN
screen1.set_anchors_preset(Control.PRESET_FULL_RECT, true)
var screen2 = ColorRect.new()
screen2.color = Color.BLUE
screen2.set_anchors_preset(Control.PRESET_FULL_RECT, true)
add_child(screen1)
move_child(screen1, 0)
print("transition demo: before screen change")
var timer = get_tree().create_timer(2.0)
await timer.timeout
# now do the swap from screen1 to screen2
print("transition demo: starting screen change")
start()
# listen out for the signal that says we're covering the screen
await okay_to_swap
print("transition demo: switching screens")
# we've got the signal, so now delete the old node
screen1.queue_free()
# and add the new one
add_child(screen2)
move_child(screen2, 0)
# and a final signal in case there's anything else you want to do
await finished
print("transition demo: after screen change")
func start():
# disable input while we're swapping the screen
get_tree().get_root().set_disable_input(true)
# start the wipe in that hides our swap
await wipe(start_color, start_size, 1.0)
# announce it's okay to change the screen now
emit_signal("okay_to_swap")
# and now start the wipe out that reveals the new screen
await wipe(end_color, end_size, 0.0)
# re-enable input now it's over
get_tree().get_root().set_disable_input(false)
# announce everything's done and back to normal
emit_signal("finished")
func wipe(to_color : Color, to_size : float, to_fade : float, direction : int = -1, easing : Tween.EaseType = Tween.EASE_IN, transition : Tween.TransitionType = Tween.TRANS_CUBIC):
if demo: print("transition demo: starting wipe %s" % [to_fade])
# see the shader for what the direction param does
background.material.set_shader_parameter("wipeDirection", randi_range(0,8) if direction == -1 else direction)
var tween = get_tree().create_tween().set_parallel(true)
tween.tween_property(background, "color", to_color, delay).set_trans(transition).set_ease(easing)
tween.tween_property(background.material, "shader_parameter/progress", to_fade, delay).set_trans(transition).set_ease(easing)
tween.tween_property(background.material, "shader_parameter/diamondPixelSize", to_size, delay).set_trans(transition).set_ease(easing)
tween.play()
await tween.finished
if demo: print("transition demo: finished wipe %s" % [to_fade])
Instantiate this completed scene elsewhere in your scene tree - just make sure the CanvasLayer
layer will show on top of whatever nodes you’re swapping around underneath it.
Material Shader
Here’s the (Godot 4) version of DDRKirby’s shader that combines their suggested different styles of screen wipe into one script.
This is attached to the shader
property of a ShaderMaterial
material of the ColorRect
background.
shader_type canvas_item;
// how far along in the animation are we?
uniform float progress : hint_range(0.0, 1.0) = 0.0;
// how big our screen sub division is
uniform float diamondPixelSize : hint_range(5.0, 50.0) = 10.0;
// wipe direction
uniform int wipeDirection : hint_range(0, 8) = 1;
void fragment() {
float xFraction = fract(FRAGCOORD.x / diamondPixelSize);
float yFraction = fract(FRAGCOORD.y / diamondPixelSize);
float xDistance = abs(xFraction - 0.5);
float yDistance = abs(yFraction - 0.5);
bool test = false;
switch (wipeDirection) {
case 0: // fill stationary
test = (xDistance + yDistance > progress * 2.0);
break;
case 1: // fill TL to BR
test = (xDistance + yDistance + UV.x + UV.y > progress * 4.0);
break;
case 2: // fill BR to TL
test = (xDistance + yDistance + UV.x + UV.y < (1.0 - progress) * 4.0);
break;
case 3: // fill BL to TR
test = ((1.0 - xDistance) + (1.0 - yDistance) + (UV.x - UV.y) > progress * 3.0);
break;
case 4: // fill TR to BL
test = ((1.0 - xDistance) + (1.0 - yDistance) + (UV.x - UV.y) < (1.0 - progress) * 3.0);
break;
case 5: // fill left to right
test = (xDistance + yDistance + UV.x > progress * 2.0);
break;
case 6: // fill right to left
test = (xDistance + yDistance + UV.x < (1.0 - progress) * 2.0);
break;
case 7: // fill top to bottom
test = (xDistance + yDistance + UV.y > progress * 2.0);
break;
case 8: // fill bottom to top
test = (xDistance + yDistance + UV.y < (1.0 - progress) * 2.0);
break;
}
if (test) {
discard;
}
}
Finally, once you’re using it in a real project, call start()
and listen out for the signals from whatever main parent scene you’ve instantiated this from.
(And strip out the demo bits of code…)