You land a hit but the enemy barely seems to feel it. Action games live or die on that sense of impact. The moment the player strikes an enemy, or takes a hit themselves, the character flashes white for an instant. That alone makes hits feel dramatically more satisfying. The effect is called a white flash .
It is also very easy to implement, with no shader knowledge required. Combine the modulate property with Tween and it takes a few lines of GDScript.
What You'll Learn
- How
modulatechanges color through multiplication- Why the sprite blows out to white when RGB values go above 1, and why black stays black
- The basic "flash white, then ease back" implementation with
Tween- When to reach for shaders instead, and how to wire this into a hit reaction
Tested with: Godot 4.7
How the modulate Property Works
modulate is a color adjustment property that every 2D node inheriting from CanvasItem (Sprite2D, Label, ColorRect, and so on) has built in. Setting a Color there changes the color of that node and all of its children through multiplication .
modulate = Color(1, 1, 1)(white): unchanged from the original color (default)modulate = Color(1, 0, 0)(red): only the R component survives, so it turns redmodulate = Color(0.5, 0.5, 0.5)(gray): everything gets darkermodulate = Color(1, 1, 1, 0.5): the fourth value (alpha) makes it semi-transparent
Because it is "original color multiplied by your color," multiplying by 1 leaves it as-is and multiplying by 0.5 darkens it.

Why Does It Glow White? Brightness Above 1
The interesting part of modulate is that RGB values can go above 1 . Color is multiplied, so a pixel with a brightness of 0.4 multiplied by 3 becomes 1.2. The screen can only show brightness up to 1, so anything above 1 tops out at white and the sprite looks blown out. That blown-out look is exactly what a white flash is.
As shown at the right end of the diagram above, a large value like modulate = Color(10, 10, 10) blows most colors out to almost pure white. Because it's multiplication, though, a pure black pixel (0) stays black no matter how much you multiply it. On art with black outlines, only the lines remain, so if you want the whole body pure white, repaint it white with a shader. Spiking to a large value for an instant and immediately returning is the core of the flash effect.
The Basic Implementation: Flash and Restore with Tween
A white flash is a short animation: turn white instantly, then ease smoothly back to the original color. That kind of change over time is exactly what Tween is for.

Here is a function that flashes a damaged character (a node with a Sprite2D).
# Script on something like a CharacterBody2D
# Keep the Tween in a variable so we can handle rapid consecutive damage
var flash_tween: Tween
func apply_damage_effect() -> void:
# ... reduce HP, start invincibility frames, and so on ...
_start_white_flash()
func _start_white_flash() -> void:
# If a flash is already running, stop it before starting (handles rapid hits)
if flash_tween and flash_tween.is_valid():
flash_tween.kill()
# 1. Turn white instantly (multiply by a large value to blow it out to white)
$Sprite2D.modulate = Color(10, 10, 10)
# 2. Ease smoothly back to the original Color(1,1,1) over 0.3 seconds
flash_tween = create_tween().set_trans(Tween.TRANS_QUINT).set_ease(Tween.EASE_OUT)
flash_tween.tween_property($Sprite2D, "modulate", Color(1, 1, 1), 0.3)
The trick to structuring this is making the flash itself a direct assignment, and using Tween only for the return . TRANS_QUINT + EASE_OUT starts sharply and settles gently, which suits the afterglow of a flash. Even under rapid damage, calling flash_tween.kill() to clear the previous flash before starting keeps it from breaking.
Warning: If
Color(10, 10, 10)doesn't look white, suspect how dark the texture is. Near-black pixels stay near black no matter how much you multiply them. For a soft glowing bloom, enableHDR 2Din Project Settings, set the Background Mode of theWorldEnvironment's Environment toCanvas, and turn on Glow; everything above 1 then bleeds softly.
Common Mistakes and Best Practices
modulate effects are easy, but a few habits keep them stable. Turning something pure black while trying to darken it is a particularly common beginner mistake.

| Common mistake | Best practice |
|---|---|
Hand-animating with a counter variable inside _process | Use Tween or AnimationPlayer and write it declaratively |
Using Color(0, 0, 0) to darken, making the sprite invisible | For a white glow, push RGB above 1. To darken, keep some brightness like Color(0.1, 0.1, 0.1) |
| Mixing damage logic and effect logic in the same function | Separate hit detection from visual effects with signals, so changing the effect doesn't ripple into game logic |
Firing create_tween() on every effect without cleanup | If effects can overlap in quick succession, kill() the existing Tween and rebuild it |
modulate vs Shaders
You can also build a white flash with a shader. Let's compare the convenience-focused modulate + Tween approach with the expressiveness-focused shader approach.

| Aspect | modulate + Tween | Custom shader |
|---|---|---|
| Ease of use | Excellent. A few lines of GDScript | Fair. Requires learning a shading language |
| Performance | Good. Very lightweight, fine even with many objects | Good to fair. Cheap when simple, but can get expensive |
| Expressiveness | Fair. Only color multiplication and alpha. Limited | Excellent. Outlines, dissolves, glow, all per-pixel |
| Best suited for | Damage flashes, item pickup glows, other simple effects | Elaborate visuals like a boss's special entrance |
For most damage flashes, modulate + Tween is plenty, given how easy and cheap it is. Once you want elaborate effects like glowing outlines or dissolving away, move on to Fragment Shader basics.
Hands-On: Building a Loosely Coupled Hit Reaction
A flash on its own is effective, but in a real game a hit usually triggers several things at once: a white flash, knockback, and a sound effect.
- Action : slash an enemy and it flashes white and recoils
- Shoot 'em up : a ship struck by a bullet flashes and a hit sound plays
- Roguelike : every landed attack flashes the target to sell the impact
The important part is to separate the damage logic (reducing HP) from the visual effect (the flash) . Once separated, you can reuse the same effect across the player, enemies, and destructible crates. Signals provide the bridge.

First, the node that owns HP (Health) does nothing but announce the fact that damage was taken.
# health.gd (the node that manages HP)
class_name Health
extends Node
signal damaged(amount: int)
signal died
@export var max_hp: int = 100
var hp: int
func _ready() -> void:
# The max HP set in the Inspector is already in place by _ready()
hp = max_hp
func take_damage(amount: int) -> void:
hp = max(hp - amount, 0)
damaged.emit(amount) # Just announce the hit. No visuals here
if hp == 0:
died.emit()
The presentation side (attached to the Sprite2D) listens for that signal and flashes.
# hit_feedback.gd (in charge of the visuals)
extends Sprite2D
@export var health: Health
var flash_tween: Tween
func _ready() -> void:
health.damaged.connect(_on_damaged)
func _on_damaged(_amount: int) -> void:
_flash_white()
# Knockback and sound effects can be added here
func _flash_white() -> void:
if flash_tween and flash_tween.is_valid():
flash_tween.kill()
modulate = Color(8, 8, 8)
flash_tween = create_tween().set_ease(Tween.EASE_OUT)
flash_tween.tween_property(self, "modulate", Color(1, 1, 1), 0.25)
There are two points to take away.
- Split logic and presentation with a signal :
Healthonly says "I got hit," andhit_feedback.gdonly listens and flashes. That is why the samehit_feedback.gddrops onto enemies, the player, and destructible crates alike. Swapping the effect never requires touchingHealth. - Always kill the previous Tween before flashing : even when rapid hits call
_flash_white()repeatedly, rebuilding afterkill()keeps it from freezing at some half-lit brightness.
Sound effects and knockback grow the same way, by adding them to _on_damaged(). Having all effect additions land in one place is the biggest payoff of splitting things up.
Bonus: Good to Know for Later
Once you can build a flash, these are worth knowing about. For now, just knowing the options exist is enough.
- Use
self_modulateto spare the children :modulateapplies to every child node. When you want to tint only a UI panel and leave its labels alone,self_modulateapplies to the node itself only. - Use color to convey meaning : a reddish flash for the player getting hit (
Color(4, 1, 1)) and white for hits on enemies (Color(8, 8, 8)) makes the situation readable at a glance. - Want more glow? Use Glow : enabling
HDR 2Din Project Settings and using Glow onWorldEnvironmentmakes sprites withmodulateabove 1 bleed softly, giving the flash a much richer look. - Elaborate effects belong to shaders : glowing outlines and noise-based dissolves are Fragment Shader basics territory. To build them by connecting nodes, you can also start from Introduction to VisualShader.
- When combining with hitstop : a Tween follows the global time speed (
Engine.time_scale) by default, so slowing time makes the white linger. Addset_ignore_time_scale(true)to that Tween (Hitstop Basics).
Summary
modulate: a property that multiplies aCanvasItem's color. Pushing RGB above 1 blows it out to white (black pixels stay black)- Building the flash : assign the white directly for the instant, then use
Tweento ease back toColor(1, 1, 1) - Handling rapid hits : call
flash_tween.kill()to stop the previous flash before starting - Design : split damage logic from presentation with signals so the same effect gets reused
The white flash gives you a big boost in game feel for very little effort. Add one to enemy hits first, then layer on sound effects, knockback, and shake with AnimationPlayer, and your sense of impact will keep growing.