2025/10/23

Type
Learning Resource
Format
Study Guide
Version
Godot 4.x
  • Downloadable Demo
  • FAQ/Troubleshooting
Code
Assets
All else
Copyright 2016-2025, GDQuest
Created
2025/10/09
Updated
2025/10/23

Juicing up your game attacks

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.

This video was made with Godot 3. The guide below has been rewritten for Godot 4.

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.

Nathan

Founder and teacher at GDQuest
What changed in Godot 4

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.

Animating the sword attack

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:

  1. Anticipation, pulling back the sword before swinging forward
  2. Smearing, creating a trail that follows the attack
  3. Easings, a way to control the acceleration and deceleration of movement in animation

The sword scene

First, here's what the sword scene looks like:

Screenshot of the Sword2D scene tree

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.

The attack 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.

In this clip I click and drag over the animation editor timeline to highlight the anticipation phase.

NOTE:
Be careful with anticipation! While it adds weight to your attacks, it also introduces a slight delay before the attack lands. If the anticipation phase is too long, players might feel like the controls are unresponsive.

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.

I activate the smear only when the sword is swinging forward.

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.

Nathan

Founder and teacher at GDQuest

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.

Animating the enemy reaction

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:

Screenshot of the Scarecrow2D scene

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:

Screenshot of the enemy reaction animation

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.

Layering particle effects

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:

Screenshot of two example parameters for the FrozenGPUParticles2D

The snowflake texture starts small, gets gradually larger than shrinks back down before disappearing. The DisplayScaleScale min and max parameters define the starting range of the scale, while I use the Scale Curve to affect how the scale changes over the particle's lifetime.

The same idea applies with Color CurvesColor ramp - I use it to vary each particle's color over their lifetime.

With all of these elements together, here's the full sword attack animation:

Screenshot of 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.

Rotating and swaying the sword

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.

Adding a knockback effect to the enemy

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 property on it (in the label's scene). This makes the label move independently of its parent, so it stays in place even when the enemy gets knocked back.

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.

The final touch: freezing time

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.

Nathan

Founder and teacher at GDQuest

Download files

Import the zip file in Godot 4.5 or later to explore the code and see how everything works together.

Updates / Code patches

Become an Indie Gamedev with GDQuest!

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.

Nathan

Founder and teacher at GDQuest
  • Starter Kit
  • Learn Gamedev from Zero
Check out GDSchool

You're welcome in our little community

Get help from peers and pros on GDQuest's Discord server!

20,000 membersJoin Server

Contribute to GDQuest's Free Library

There are multiple ways you can join our effort to create free and open source gamedev resources that are accessible to everyone!