2025/10/23
- Type
- Learning Resource
- Format
- Study Guide
- Version
- Godot 4.x
- Subject Tags
- Downloadable Demo
- FAQ/Troubleshooting
- Created
- Updated
- 2025/10/09
- 2025/10/23
You know how to code your game mechanics, but how do you make them feel satisfying? That sword swing might work functionally, but does it feel impactful and powerful?
In this guide, I'll show you how to improve the feel of an attack mechanic in a 2D game. We'll use Godot's built-in tools to add polish to sword attacks with layered visual effects, sound design, and animation techniques.
Once you've prototyped your attack mechanics, it's time to juice them up. Even small details can make a big difference in how players experience game mechanics.
Here's an overview of what we'll cover:
This is what the final result looks like:
While I'm using a sword attack as an example in this guide, the principles apply to any type of attack. You can use the same techniques for a sword, a gun, or a magic spell.
Compared to the video above, Godot 4 brought many quality of life improvements but no fundamental changes to any of the techniques shown. Most notably, the particle system inspector has been completely reorganized, and the animation player is more intuitive to use.
There are three changes to GDScript syntax that you should be aware of:
Annotations over keywords: To make a property available in the editor, we now use the @export annotation instead of the export keyword. Also, we now use @onready instead of the onready keyword to initialize a variable when the node is added to the scene tree.
Signal connections: Signals and functions are now first-class citizens. We connect signals using objects, like area_entered.connect(_area_entered), instead of using plain strings like before: connect("area_entered", self, "_on_area_entered"). This gives you better autocompletion, error detection, and lets you connect to any function, including lambdas.
Await: The yield keyword is now await. You also do not use parentheses anymore: await signal_name.
I use three animation techniques to make the sword animation readable although it's really fast (only a couple of frames): anticipation, smearing, and easing:
First, here's what the sword scene looks like:
The pivot node and everything below it are the sword visuals and hit box. The pivot rotates to point toward the mouse cursor, and the sword swings around it when attacking.
The attack animation is stored in the AnimationPlayer node that rotates the Sprite2D node to create the swinging motion. This allows turning the pivot independently of the sword swing animation.
To give the sword some weight, I've added an anticipation animation before the attack: The sword moves back slightly for a frame before swinging forward.
Next, I've added a smear effect that follows the sword during the attack. This creates a sense of motion and makes the attack feel faster than it actually is.
The smear effect here uses a single static sprite. I kept it pretty basic to show you that you don't need to make very complicated art.
Of course, a smear animated by hand by an artist would look better, but this simple approach already adds a lot to the feel of the attack.
To tie it all together, I use easing to create more natural movement. Notice in the clip above how the sword slows down at the end of the swing.
I apply the same animation principles to the enemy reaction. The enemy rotates with easing when hit, making it look like it's actually reacting to being struck. I also add a blink animation that briefly turns the sprite white to give clear visual feedback that the hit connected.
The enemy scene is this training dummy I called Scarecrow2D:
I've added particle effects that trigger on impact and a damage label that gets thrown around when the sword hits.
The animation itself is pretty basic, but effective:
The BodySprite2D rotates with uneven spacing, using easing to give it a more natural feel - like the dummy is actually reacting to being hit rather than just playing back a preset animation.
On top of that, I've added a blink animation that briefly turns the sprite white. This gives clear visual feedback to the player that their hit connected successfully.
I use a custom shader to make the sprite white:
shader_type canvas_item;
uniform bool active = false;
void fragment() {
// Replaces all but alpha to white if active = true
COLOR = texture(TEXTURE, UV);
if (active == true)
COLOR.rgb = vec3(1.0);
}
All I do in the animation is toggle the active property of the shader on and off at appropriate times.
I add snowflake particles that emit from the sword continuously to reinforce the ice theme.
Godot 4's reworked particle system makes it much easier to get natural-looking results. The new ParticleProcessMaterial offers parameters that are more intuitive to understand and tweak.
There's a lot of parameters to experiment with. Here are two examples I used for the snowflake particles:
The snowflake texture starts small, gets gradually larger than shrinks back down before disappearing. The Display
The same idea applies with Color Curves
With all of these elements together, here's the full sword attack animation:
Notice that I also control the CollisionShape2D from the AnimationPlayer as its timing has to match when the hit happens.
The sword rotates to point toward the mouse cursor. I've also added a gentle swaying motion up and down to make the sword feel more alive.
Here's the code that makes the pivot node rotate toward the mouse position and sway up and down:
@onready var _pivot: Marker2D = %Pivot2D
func _physics_process(delta: float) -> void:
var mouse_position := get_global_mouse_position()
_pivot.look_at(mouse_position)
_pivot.position.y = sin(Time.get_ticks_msec() * delta * 0.20) * 10
_pivot.scale.y = sign(mouse_position.x - global_position.x)
The code uses look_at() to make the pivot node rotate toward the mouse position. For the swaying motion, I use a sine wave based on the current time to move the pivot up and down smoothly. Time.get_ticks_msec() returns the time elapsed since the game started, in milliseconds.
The last line in _physics_process() flips the pivot's vertical scale to prevent the sword from appearing upside down when the mouse is on the left side of the character. The sign() function returns 1 or -1 depending on whether the mouse is to the right or left.
Finally, I apply a knockback effect that pushes the enemy away slightly when struck. I also spawn a damage label each time the enemy gets hit. To make it look more dynamic, I add gravity to the label.
Using a CharacterBody2D as the base node for the enemy makes it easy to implement the knockback effect. This is part of the enemy script:
extends CharacterBody2D
var _pushback_force := Vector2.ZERO
var _hit_gpu_particles: GPUParticles2D = %HitGPUParticles2D
func knock_back(source_position: Vector2) -> void:
_hit_gpu_particles.rotation = get_angle_to(source_position) + PI
_pushback_force = -global_position.direction_to(source_position) * 300.0
func _physics_process(delta: float) -> void:
_pushback_force = lerp(_pushback_force, Vector2.ZERO, delta * 10.0)
velocity = _pushback_force
move_and_slide()
In the knock_back() function, I first rotate the hit particles to face away from the sword's position. Then I calculate the _pushback_force by finding the direction from the sword to the enemy and multiplying it by 300 to set the knockback strength. It gives the enemy an impulse away from the hit.
In _physics_process(), I gradually reduce the _pushback_force every frame using lerp(). This creates a smooth deceleration where the enemy gets pushed back strongly at first and then slows down naturally.
I spawn a damage label each time the enemy takes damage. Here's the relevant part of the enemy script:
func take_damage(amount: int) -> void:
_animation_player.play("hit")
var label := preload("damage_label_ui.tscn").instantiate()
label.global_position = _damage_spawning_point.global_position
add_child(label)
label.set_damage(amount)
I set the label's global_position because I enabled the Top Level
The damage label itself is a Control node with physics applied to it. Here's its script:
extends Control
@export var gravity := Vector2(0, 980)
var _velocity := Vector2(randf_range(-200, 200), -300)
@onready var _label: Label = %Label
func _process(delta: float) -> void:
_velocity += gravity * delta
position += _velocity * delta
func set_damage(amount: int) -> void:
_label.text = str(-absi(amount))
The label starts with an upward velocity that has a random horizontal component. Each frame, gravity pulls it down, and I update its position based on the velocity. This is an easy way to give it a nice random arc when it spawns.
I very briefly slow down the entire game when the sword hits the enemy to make the hit feel harder.
This works by changing the Engine.time_scale for a split second. In the previous section, you saw the EventBus.enemy_hit signal being emitted whenever the enemy takes damage. I use this signal in the main scene to trigger a time freeze effect:
extends Node2D
@export var freeze_slow := 0.07
@export var freeze_time := 0.3
func _ready() -> void:
EventBus.enemy_hit.connect(freeze_engine)
func freeze_engine() -> void:
Engine.time_scale = freeze_slow
await get_tree().create_timer(freeze_time * freeze_slow).timeout
Engine.time_scale = 1.0
In this code, I multiply the freeze duration by the slowing factor because when time is slowed, the timer duration also needs to be multiplied to compensate for the slower passage of time.
In this project, I've exaggerated the freeze time a bit to make it noticeable, plus it happens on every hit. But you generally want to apply it just for one to several frames, or even only use this on critical hits or special situations like the player getting hit.
Import the zip file in Godot 4.5 or later to explore the code and see how everything works together.
Don't stop here. Step-by-step tutorials are fun but they only take you so far.
Try one of our proven study programs to become an independent Gamedev truly capable of realizing the games you’ve always wanted to make.
Get help from peers and pros on GDQuest's Discord server!
20,000 membersJoin ServerThere are multiple ways you can join our effort to create free and open source gamedev resources that are accessible to everyone!
Sponsor this library by learning gamedev with us onGDSchool
Learn MoreImprove and build on assets or suggest edits onGithub
Contributeshare this page and talk about GDQUest onRedditYoutubeTwitter…