Latest Post: Game Feel: 20 Principles Behind the Invisible Polish, Animated (with Godot Examples)

Game Feel: 20 Principles Behind the Invisible Polish, Animated (with Godot Examples)

The 20 principles of game feel, from coyote time to hit stop and i-frames, each shown with a side-by-side WITHOUT vs. WITH animation and practical Godot 4 examples.

17 min read
Black and white split illustration: on the left a flat gray square labeled FLAT, on the right a white square character with eyes, motion trail, sparks and dust labeled JUICY.

Why does one game feel amazing in your hands, while another one with exactly the same mechanics feels flat? Most of the time, the answer is game feel: all the small details that nobody notices until they’re missing.

I make precision platformers, so for me this isn’t theory. A jump pressed two frames too late, a hit without any weight or a camera that hides the next spike can ruin a level that looks perfect on paper.

I don’t remember one exact moment when game feel “clicked” for me. It’s more something we’ve been seeing again and again over the years, and every time we get the chance, we add it. In our last game jam entry, Litterbug, you fly a rocket that has to dodge the trash it drops itself (a bit like Snake). Score popups, combos, particles, shake, flashes: a good part of this article is in there somewhere.

Litterbug, our entry for Mini Jame Gam #60 (theme: Trash, special object: Rocket). Every delivery pops a score, a combo badge and a burst of debris.

Recently I came across a great 12 Principles of Game Feel infographic by Ali Farha and thought: this deserves animations. So I took his 12, added 8 more that I’d personally miss in any platformer, and made a small WITHOUT vs. WITH loop for each one. You’ll also find some Godot 4 snippets along the way, because I can’t help it.

How to read the animations: the left side is always WITHOUT the principle, the right side is WITH it. They’re short, muted loops that pause when they’re off-screen. Every clip has a pause button, and if your system is set to reduce motion, they won’t play until you press it (yes, I’m the accessibility guy, I had to).

What Is Game Feel?

Steve Swink, who literally wrote the book on it (Game Feel, 2008), describes it as:

Real-time control of virtual objects in a simulated space, with interactions emphasized by polish.

In practice, game feel lives in the tiny gap between your input and the game’s answer. None of the principles below change what your game does. They change how it feels while it does it. That’s also why they’re so easy to forget: a game without them still works.

I grouped the 20 principles into four families, because I think it’s easier to remember them that way:

  1. Control: does the game do what the player meant?
  2. Impact: do actions feel like they have force?
  3. Motion: do things move like they have weight and life?
  4. Feedback and fairness: does the game answer back, clearly and fairly?

1. Control: How the Game Feels in Your Hands

This is the family players notice the least and blame the most. Nobody writes a Steam review saying “the input buffer is too short”. They write “the controls are broken”.

Input Responsiveness

The time between pressing a button and seeing something happen. If there’s a couple hundred milliseconds of lag (a slow animation, too much smoothing, a long wind-up), the character feels heavy and disconnected, even if everything else is perfect.

Both squares get the same input. The left one reacts after 250 ms and eases into motion; the right one moves on the next frame.

In Godot, read the input in _physics_process and apply the velocity right away. If your design needs a wind-up animation, start moving during the animation, not after it.

Coyote Time and Input Buffering

I’m putting these two together because they solve the same problem from both sides: the player was almost on time.

Coyote time is a small grace period after you run off a ledge (usually around 0.1 seconds) where jumping still works. The name comes from Wile E. Coyote, who famously only falls once he looks down.

Both players press jump 80 ms after the edge. Without coyote time the jump is ignored and the square falls; with 100 ms of coyote time it clears the gap.

Input buffering is the opposite: you press jump a bit before landing, and instead of throwing that press away, the game remembers it for a few frames and jumps the moment you touch the ground.

Jump is pressed 80 ms before touching the ground. Without a buffer it's ignored; with a 120 ms buffer the square jumps as soon as it lands.

Both are just two timers, so here they are in one snippet:

const COYOTE_TIME := 0.1
const JUMP_BUFFER := 0.12

var coyote_timer := 0.0
var buffer_timer := 0.0

func _physics_process(delta: float) -> void:
	if is_on_floor():
		coyote_timer = COYOTE_TIME
	else:
		coyote_timer -= delta
		velocity.y += gravity * delta

	if Input.is_action_just_pressed("jump"):
		buffer_timer = JUMP_BUFFER
	else:
		buffer_timer -= delta

	if buffer_timer > 0.0 and coyote_timer > 0.0:
		velocity.y = JUMP_VELOCITY
		buffer_timer = 0.0
		coyote_timer = 0.0 # so you can't jump twice inside the window

	move_and_slide()

Dimenshaper uses both. Before the demo was out on Steam (and on the Steam Deck), I tested them with a lot of people, and I stopped tweaking the values when most of them told me it felt good. In the end, that’s what matters: not whether it’s 0.1 or 0.12 seconds, but whether it feels good to the people playing.

If you want to go deeper, Maddy Thorson wrote a short article about these “forgiveness” tricks in Celeste: Celeste & Forgiveness. Worth the five minutes.

Variable Jump Height

Tap for a small hop, hold for a full jump. Sounds like a detail, but now the player isn’t just triggering a jump, they’re shaping the arc. In tight levels that’s the difference between precise control and hitting your head on every ceiling.

A tap and a long press. Without variable height both give the same full jump; with it, the tap turns into a small hop.

The classic way to do it is a jump cut: when the player releases the button while still going up, you cut the upward velocity.

const JUMP_CUT := 0.4

if Input.is_action_just_released("jump") and velocity.y < 0.0:
	velocity.y *= JUMP_CUT

Acceleration and Easing

How fast your character reaches top speed, and how fast it stops. Slow ramps feel like running on ice. Snappy ones feel tight. Same top speed, completely different game.

The line at the bottom is the velocity. On the left it ramps up slowly and keeps sliding; on the right it reaches full speed fast and stops cleanly.

move_toward is your best friend here:

const MAX_SPEED := 220.0
const ACCEL := 1800.0
const DECEL := 2400.0

var direction := Input.get_axis("move_left", "move_right")
if direction != 0.0:
	velocity.x = move_toward(velocity.x, direction * MAX_SPEED, ACCEL * delta)
else:
	velocity.x = move_toward(velocity.x, 0.0, DECEL * delta)

Once you have separate values for accelerating, braking, turning around and moving in the air, you can tune the feel of a character for hours (ask me how I know: Dimenshaper has its own acceleration curves too).

🎮 I made my first professional game with Godot: Dimenshaper, a precision platformer where switching dimensions reshapes every room. Try the free demo on Steam

2. Impact: Making Hits Land

This family is about force. When something hits, falls or explodes, you should feel it in your hands, even if your controller doesn’t vibrate.

Hit Stop

A tiny freeze at the moment of impact, usually between 3 and 6 frames. Attacker and target stop, the target flashes, and then the knockback kicks in. You’ll never consciously see it, but it’s a big part of why hits in good action games feel heavy.

The frame strip shows the freeze. On the right, the game holds the impact for a few frames before the target flies away.
func hit_stop(duration := 0.08) -> void:
	Engine.time_scale = 0.0
	# process_always, not in physics, ignore_time_scale = true
	await get_tree().create_timer(duration, true, false, true).timeout
	Engine.time_scale = 1.0

Freezing the whole game is the lazy version, and for a small project it’s totally fine. In bigger games you usually pause only the two characters involved, so the UI and the music keep going.

Camera Shake

A short shake when something heavy lands. The important part is that it fades out quickly. A shake that’s too long or too strong stops being feedback and becomes noise (and motion sickness, more on that later).

The same block falls in both scenes. On the right the view shakes for half a second and dust puffs out from the impact.

If you want to see how far a bit of shake can take a game, watch Jan Willem Nijman’s classic talk The Art of Screenshake. It’s from 2013 and still holds up.

For the implementation, I like the trauma model from Squirrel Eiserloh’s GDC talk on camera math: events add “trauma”, trauma decays over time, and the shake grows with trauma squared. Small hits stay subtle, big ones feel big.

extends Camera2D

@export var max_offset := 12.0
@export var decay := 2.5
var trauma := 0.0

func add_trauma(amount: float) -> void:
	trauma = minf(trauma + amount, 1.0)

func _process(delta: float) -> void:
	trauma = maxf(trauma - decay * delta, 0.0)
	var shake := trauma * trauma
	offset = Vector2(randf_range(-1.0, 1.0), randf_range(-1.0, 1.0)) * max_offset * shake

We used a lot of camera shake in Beyond the Spin, our entry for a jam with the theme “Spin to Win”. It’s a collection of minigames, and several of them shake the camera, like the very first one, where you play Michael Jackson and have to make him spin.

Knockback and Recoil

Every shot pushes the shooter back a bit, and every hit pushes the target away. Without this, bullets just disappear into enemies and you have to trust the damage numbers. With it, you can actually see the force.

Four shots each. On the right the shooter kicks back with every shot and the enemy gets pushed further with every hit.
func take_hit(from_position: Vector2, force := 260.0) -> void:
	var direction := (global_position - from_position).normalized()
	velocity += direction * force

Screen Flash

A quick white flash when you take damage. Combine it with a health bar that drops and then drains slowly (the gray part in the clip), and the player knows immediately what happened and how bad it was.

A projectile hits the player. On the right the screen flashes, the player is knocked back and the lost health drains away slowly.

In Godot: a white ColorRect in a CanvasLayer, with its alpha tweened from 0.8 to 0 in about 0.12 seconds. Be careful with this one though. I’ll explain why in the accessibility section.

Slow Motion

Drop time to a fraction for half a second on a big moment, like the final hit on a boss. It’s basically a long, cinematic hit stop. The trick is to use it rarely, otherwise it stops being special very fast.

The same explosion. On the right, time drops to 0.18x for a moment and then eases back to normal speed.
func slow_motion(scale := 0.2, hold := 0.6) -> void:
	Engine.time_scale = scale
	await get_tree().create_timer(hold, true, false, true).timeout
	var tween := create_tween().set_ignore_time_scale(true) # Godot 4.3+
	tween.tween_property(Engine, "time_scale", 1.0, 0.3)

3. Motion: Weight and Life

Most of this family comes from animation. Disney’s animators figured it out almost a hundred years ago, and it works just as well for a square made of eight pixels.

Squash and Stretch

Stretch the ball while it falls, squash it when it hits the ground. That’s it. You don’t touch the physics at all, and suddenly the ball has weight and energy instead of looking like a sticker sliding around.

The same bounce. On the right the ball stretches while it moves fast and squashes flat when it touches the ground.
func squash(sprite: Node2D, amount := Vector2(1.35, 0.7)) -> void:
	sprite.scale = amount
	var tween := create_tween().set_trans(Tween.TRANS_BACK).set_ease(Tween.EASE_OUT)
	tween.tween_property(sprite, "scale", Vector2.ONE, 0.18)

Call it when landing (squash) and when jumping (stretch, something like Vector2(0.75, 1.3)). Small tip: put the sprite’s origin at its feet, so it squashes into the floor instead of floating.

Weight and Physics

A heavy stone block shouldn’t fall and bounce like a rubber ball. When objects behave the way they look, players can read the world and predict what’s going to happen.

On the left the heavy block and the light ball fall exactly the same way. On the right the block drops fast and lands with a thud, while the ball falls more gently.

In Godot, give heavier objects stronger gravity and less bounce. With RigidBody2D that’s mass, gravity_scale and a PhysicsMaterial with low bounce. With CharacterBody2D, just expose gravity and bounciness per object type.

Anticipation

The wind-up before the action. An enemy that pulls back before charging is telling you what it’s about to do. Without it, you just get hit and it feels cheap. With it, you get a fair chance to react, and funnily enough, the attack even feels stronger because you saw it coming.

On the left the enemy charges without warning and hits the player. On the right it pulls back, shows an exclamation mark, and the player has time to jump over it.

In Godot, I’d add a WINDUP state to the enemy’s state machine, with a clear pose, a sound and 0.3 to 0.6 seconds of duration. If you want harder enemies, make the wind-up shorter, not the attack faster.

Trails and Motion Blur

Fast objects can look like they’re teleporting between frames. A trail behind them shows speed and direction at a glance.

The same fast figure-eight movement. On the right a fading trail shows where the ball came from.

A Line2D that remembers the last positions is enough:

extends Line2D
# Add as a child of the moving node and set top_level = true

@export var max_points := 14

func _process(_delta: float) -> void:
	add_point(get_parent().global_position)
	if get_point_count() > max_points:
		remove_point(0)

Add a width_curve from 0 to 1 and a transparent-to-white gradient, and you get a nice tapering trail.

Appeal and Character

Give a plain square two eyes, let it blink, breathe and look around, and people start caring about it. I find this one fascinating, because it costs almost nothing.

On the left, a static white square. On the right, the same square breathes, blinks, follows the dot with its eyes and hops.

The protagonist of Hexactly, our cozy hex puzzle game, is a small hexagon that blinks and that you can dress up with different skins. Even that little bit gives it some personality, but to be honest there’s still a lot of potential here: it could react when you merge tiles, look at the tile you’re about to touch, or get a bit nervous when the board is almost full.

Hexactly customize screen with five skins for the hexagon protagonist: Classic with round glasses (equipped), Simple, Cool with sunglasses, King with a crown and Gold.
The Hexactly protagonist and some of its skins.

In Godot: a breathing idle (a tiny looping scale tween), a random blink timer and eyes that use look_at() or a clamped offset towards whatever they should look at.


4. Feedback and Fairness: The Game Answers Back

The last family is about communication. Every action should get a clear answer, and when the game punishes you, it should feel fair.

Juice

Juice means getting as much output as possible out of every input. Picking up a coin could just make it disappear. Or the coin can pop, burst into sparks, make your character hop and bump the score counter.

On the left the coin simply disappears. On the right it bursts into sparks, a +1 floats up, the player hops and the score pops.

For me, juice isn’t one technique but more of a habit: for every event in your game, ask yourself “how could this answer the player a bit more?”. If you haven’t seen it yet, Juice It or Lose It by Martin Jonasson and Petri Purho is still the best demo of this idea.

Particle Effects

A few sparks, a puff of dust, some debris. Particles confirm that something happened and make the world react to you. Small, fast and short-lived usually works best.

A bullet hits a wall. On the left it vanishes; on the right sparks fly, dust puffs out and small debris falls to the ground.
const SPARKS := preload("res://fx/sparks.tscn")

func burst(at: Vector2) -> void:
	var particles: GPUParticles2D = SPARKS.instantiate()
	get_tree().current_scene.add_child(particles)
	particles.global_position = at
	particles.one_shot = true
	particles.emitting = true
	particles.finished.connect(particles.queue_free)

Audio Feedback

Try muting a game you love and play for five minutes. It instantly feels weaker. Sound carries a huge part of the impact. Layer a low thump, a crunchy mid and a short tail, and vary the pitch a little so it doesn’t get repetitive.

The clips here are muted (sorry), so watch the speaker icon and the waveform: on the right, every bounce makes a sound. You can hear it in the video above.
func play_varied(player: AudioStreamPlayer2D) -> void:
	player.pitch_scale = randf_range(0.92, 1.08)
	player.play()

Godot can also do this for you with an AudioStreamRandomizer, which randomizes pitch, volume and even which sample plays.

Camera Lookahead

A good camera doesn’t just follow the player, it looks ahead. If you’re running right, show more of what’s on the right. Add some smoothing so it doesn’t feel stiff, and players react to the level instead of to surprises.

The dashed line marks the center of the screen. On the left the camera is locked to the player; on the right it moves ahead in the direction of movement and eases when the player turns.
extends Camera2D

@export var target: CharacterBody2D
@export var lookahead := 90.0
@export var smoothing := 4.0

func _process(delta: float) -> void:
	var lead := Vector2(signf(target.velocity.x) * lookahead, 0.0)
	offset = offset.lerp(lead, 1.0 - exp(-smoothing * delta))

If you’re also using the shake from before, add both offsets together instead of overwriting one with the other. And if you need cameras for cutscenes too, I wrote about a Godot 4 Camera Switcher a while ago.

Invincibility Frames

After you get hit, you blink for a moment and nothing can hurt you. Without i-frames, three projectiles in a row can kill you before you even understand what happened. With them, every hit is readable, and dying feels like your fault (which is exactly what you want).

Three projectiles in quick succession. On the left all three hit; on the right the player blinks after the first hit and the other two pass through.
@export var iframes_duration := 0.9
var invincible := false

func take_damage(amount: int) -> void:
	if invincible:
		return
	health -= amount
	invincible = true
	var blink := create_tween().set_loops(int(iframes_duration / 0.12))
	blink.tween_property($Sprite2D, "modulate:a", 0.2, 0.06)
	blink.tween_property($Sprite2D, "modulate:a", 1.0, 0.06)
	await get_tree().create_timer(iframes_duration).timeout
	invincible = false

Game Feel and Accessibility: Juice With an Off Switch

I work a lot with web accessibility, so I couldn’t write about screen shake and flashes without this part. The same effects that make a game feel great can make it unplayable for some people.

  • Screen shake can cause motion sickness. Add a setting to reduce or turn it off. A multiplier on max_offset is literally one line.
  • Flashes can be dangerous for photosensitive players. Avoid more than three flashes per second, keep full-screen flashes short and not too intense, and let players disable them. The web has the same rule in WCAG (2.3.1 Three Flashes or Below Threshold), by the way.
  • Slow motion and hit stop are usually fine, but they change timing, so keep them consistent for players who play by rhythm.
  • Don’t rely on only one channel. Pair important sounds with something visual for players who are deaf or play muted, and the other way around.

In my own games I try to make all of these effects completely optional, without losing the rest of the experience: screen shake, chromatic aberration and flashes should all be possible to turn off. In Litterbug, for example, you can reduce the shake, the flashes and the flickering lines in the options.

Crashing into your own trash in Litterbug: red flash, chromatic aberration and shake. All of them can be reduced in the options.

Good game feel isn’t about maximum intensity. It’s about the right amount for each player.


Stack Them: A Quick Checklist

None of these is magic on its own. It gets interesting when you stack them: a jump with coyote time, buffering, variable height, squash and stretch, a dust puff and a soft landing sound feels like a different game than a plain jump, even though the mechanic is exactly the same.

PrincipleFamilyEffortTypical starting values
Input ResponsivenessControlLowReact on the next frame
Coyote TimeControlLow0.08–0.15 s
Input BufferingControlLow0.1–0.15 s
Variable Jump HeightControlLowCut upward velocity to 30–50%
Acceleration and EasingControlLowFull speed in 0.1–0.2 s
Hit StopImpactLow3–6 frames (0.05–0.1 s)
Camera ShakeImpactLow0.2–0.5 s, decaying
Knockback and RecoilImpactLowScale with damage
Screen FlashImpactLow0.1 s, never more than 3 per second
Slow MotionImpactMedium0.1–0.3x for 0.3–0.8 s, rarely
Squash and StretchMotionLow20–40% deformation, keep the area
Weight and PhysicsMotionMediumConsistent per object type
AnticipationMotionMedium0.3–0.6 s wind-up
Trails and Motion BlurMotionLow8–16 trail points
Appeal and CharacterMotionMediumIdle, blink, look-at
JuiceFeedbackMediumEvery event gets a response
Particle EffectsFeedbackLowShort-lived, few particles
Audio FeedbackFeedbackMedium±5–10% pitch variation
Camera LookaheadFeedbackLowLead by 10–20% of the screen
Invincibility FramesFeedbackLow0.5–1.5 s, with blinking

If you only have one afternoon, start with the Control family. It’s cheap, nobody sees it and everybody feels it.

Conclusion

Game feel isn’t something you add the week before release. It’s a lot of small decisions about timing, motion, camera and sound, and most of them are a few lines of code plus an evening of tweaking values.

My favorite principle, even though it’s probably the most basic one, is juice. I want everything in my games to feel a bit wobbly and alive.

My suggestion: pick one principle, add it to whatever you’re working on right now, and compare with and without, just like in the clips. And if you want to think about how all these details connect into the bigger picture, my article on systems thinking in game design is a good next read.


FAQ about Game Feel

Game feel is how it feels to control a game, moment to moment: how responsive, heavy and fair every action is. It comes from small details like input timing, animation, camera, particles and sound, not from the core mechanics themselves.

References


Share article

Comments

Powered by Giscus

Related Posts

Stay in the loop!

Get to know some good resources. Once per month.

Frontend & Game Development, tools that make my life easier, newest blog posts and resources, codepens or some snippets. All for free!

No spam, just cool stuff. Promised. Unsubscribe anytime.