13 Essential Godot Features Every 2D Game Developer Should Know (With Recommended Course)

Jun 20, 2025Nov 6, 2025 Updated📖 41 min read | 24,352 charsGame DevelopmentGodotLearning

In my previous article, I covered the basics of learning Godot. This time, as a follow-up, I took on building a more practical 2D action-adventure game. I completed the Udemy course "Godot4: Build a 2D Action-Adventure Game" and learned many practical features that weren't covered in the fundamentals.

The course systematically covers everything from basic player controls to NPC dialogue, puzzle elements, and combat systems — all the essentials for 2D game development. Rather than repeating the course content, I'll use this article to review the key Godot features and concepts I learned along the way.

Course Overview: Learning Through 2D Action-Adventure Game Development

The course "Godot4: Build a 2D Action-Adventure Game" is an excellent English-language course that teaches practical Godot features systematically by building a 2D action-adventure game from scratch. Specifically, it covers:

  • 8-directional player movement and animation
  • Environment building with auto-tiles using the Terrains feature
  • Y-Sort for controlling character and object draw order
  • Physics puzzles using RigidBody2D (block pushing)
  • NPC dialogue system with pause control
  • Data persistence using Autoload (e.g., treasure chest open states)
  • Combat system with enemy AI and knockback
  • Particle effects for visual polish

This is a step up in difficulty from the beginner course I covered in my previous article, but it's incredibly valuable for learning how to build basic enemy AI, combat systems, and dialogue systems.

  • How do you handle area transitions?
  • How do you implement pushable objects?
  • How do you persist the state of opened treasure chests?
  • How do you manage NPC conversations?
  • How do you implement a door that opens when multiple switches are pressed?
  • Where do you control the damage flash effect?

These are the kinds of "real-world game development" problems this course addresses, and working through them takes your Godot understanding to the next level.

If you're interested, check out the course on Udemy below.

Udemy frequently runs 90% off sales, so I recommend adding it to your wishlist and buying during a sale. Even if you're not comfortable with English, you can follow along by copying the code on screen and looking up documentation or asking ChatGPT/Gemini for explanations. You can also request a refund if it's too challenging, so don't hesitate to give it a try.

Godot4: Build a 2D Action-Adventure Game (Udemy)

Deep Dive Notes on Key Godot Features

From here, I'll share notes on the Godot features and concepts I found most important throughout the course, organized to deepen my own understanding.

Node Process Mode: Smart Pause Control

In Godot, you can control how each node behaves during a pause by setting its Process Mode. This is an extremely powerful feature.

For example, when talking to an NPC, you might want to freeze enemies and the player in the background while keeping the dialogue window interactive.

  • Pausable (default): When get_tree().paused = true, _process and _physics_process stop. Ideal for game world objects like players and enemies.
  • Always: Ignores pause state and keeps running. Used for NPCs during conversations, UI elements, or background music that should continue during pauses.
  • When Paused: Only runs during pause. Perfect for pause-only UI like a "PAUSED" overlay.

This lets you selectively exempt specific nodes from being affected by the pause.

Example: Pausing the game during dialogue

The scene pauses during conversation (get_tree().paused = true) and resumes when the dialogue ends

# NPC.gd

# Set this NPC node's Process Mode to "Always" in the inspector

func _process(delta):
    # (If the player is nearby and the interact key is pressed)
    if Input.is_action_just_pressed("interact") and can_talk:
        if is_dialog_active():
            # Close dialogue and unpause
            close_dialog()
            get_tree().paused = false
        else:
            # Open dialogue and pause the game
            open_dialog()
            get_tree().paused = true

By keeping the NPC itself running while pausing the rest of the game, you can safely handle dialogue open/close operations.

CharacterBody2D Motion Mode: Top-Down vs Side-Scrolling

CharacterBody2D has an important setting called Motion Mode that determines the character's physics behavior. Setting this correctly based on your game genre is essential.

  • Grounded (default): Gravity is automatically applied, and floor detection functions like is_on_floor() work. Best for platformers and side-scrolling action games with jumping and falling.
  • Floating: No gravity influence and no floor concept. Suited for top-down action games, shooters, and other games where characters move freely on the XY plane.

Example: Top-down movement

# Player.gd (Set Motion Mode to "Floating")
extends CharacterBody2D

@export var speed: float = 200.0

func _physics_process(delta):
    var direction = Input.get_vector("move_left", "move_right", "move_up", "move_down")
    velocity = direction * speed
    move_and_slide()

If Motion Mode isn't set correctly, you'll get unintended gravity or broken floor detection, so always verify this at the start of a project.

InputMap: Efficient Key Binding Management

Godot InputMap

Godot's Input Map (Project Settings > Input Map) lets you assign "action names" to keyboard keys and gamepad buttons. This means your code uses abstract action names (like "move_left") rather than specific key names (like "A key").

Benefits:

  • Multiple keys can map to the same action (e.g., both "A" and "Left Arrow" for "move_left")
  • Makes key rebinding features much easier to implement
  • Improves code readability

Example: Getting input using InputMap

func _process(delta):
    # One-shot input (the moment a button is pressed)
    if Input.is_action_just_pressed("interact"):
        open_chest()

    # Continuous input (while a button is held)
    if Input.is_action_pressed("dash"):
        speed = DASH_SPEED
    else:
        speed = NORMAL_SPEED

    # 2-axis analog input (extremely useful)
    var direction = Input.get_vector("move_left", "move_right", "move_up", "move_down")
    velocity = direction * speed
    move_and_slide()

Input.get_vector() is particularly useful — it returns a normalized Vector2 from four actions, making top-down movement code incredibly concise.

Basic Input Handling: just_pressed, pressed, and released

After defining actions in InputMap, you need to properly handle that input in your game. Godot provides multiple methods depending on the input state, and using the right one for each situation is important.

Input Class Reference (Godot Documentation)

Input State Types

  • is_action_just_pressed(): Returns true only at the moment a button is pressed
  • is_action_pressed(): Returns true while a button is being held
  • is_action_just_released(): Returns true only at the moment a button is released

Practical Usage Examples

func _process(delta):
    # One-shot actions (jump, attack, menu toggle, etc.)
    if Input.is_action_just_pressed("jump"):
        if is_on_floor():
            velocity.y = JUMP_VELOCITY

    if Input.is_action_just_pressed("attack"):
        perform_attack()

    if Input.is_action_just_pressed("pause"):
        toggle_pause_menu()

    # Continuous actions (movement, dash, charge, etc.)
    if Input.is_action_pressed("dash"):
        current_speed = dash_speed
    else:
        current_speed = normal_speed

    # Charge mechanic
    if Input.is_action_pressed("charge"):
        charge_power += charge_rate * delta
        charge_power = min(charge_power, max_charge)

    # Action on button release (firing a charged shot, etc.)
    if Input.is_action_just_released("charge"):
        fire_charged_shot(charge_power)
        charge_power = 0.0

Common Mistakes and Solutions

Example: Unintended continuous firing

# Bad: Using is_action_pressed() fires bullets every frame
func _process(delta):
    if Input.is_action_pressed("shoot"):  # Wrong
        fire_bullet()  # Runs every frame

# Good: Using is_action_just_pressed() fires once per press
func _process(delta):
    if Input.is_action_just_pressed("shoot"):  # Correct
        fire_bullet()  # Runs only on the frame the button is pressed

Advanced Input: get_action_strength() and Analog Input

For gamepad analog sticks and triggers that return values from 0.0 to 1.0, use get_action_strength().

func _process(delta):
    # Get analog input (0.0 to 1.0)
    var move_strength = Input.get_action_strength("move_forward")
    var brake_strength = Input.get_action_strength("brake")

    # Car acceleration example
    if move_strength > 0.0:
        velocity += forward_direction * acceleration * move_strength * delta

    # Braking example
    if brake_strength > 0.0:
        velocity = velocity.move_toward(Vector2.ZERO, brake_force * brake_strength * delta)

Input Function Selection Guide

Use CaseFunctionExamples
One-shot actionsis_action_just_pressed()Jump, attack, menu toggle, item use
Continuous actionsis_action_pressed()Movement, dash, charge, aim
On-release actionsis_action_just_released()Charged attack release, long-press end
Analog inputget_action_strength()Throttle, aim precision

Understanding these distinctions lets you create controls that feel natural and responsive to players.

Movement Functions: move_and_slide vs move_toward

Knockback uses move_toward

Godot provides many convenient movement functions, but understanding when to use move_and_slide() vs move_toward() is particularly important.

  • move_and_slide(): The primary function for CharacterBody2D. Moves the object based on its current velocity, automatically handles wall and floor collisions, and slides appropriately. This is the go-to for basic physics movement.
  • move_toward(target_velocity, delta): Gradually changes the current velocity toward a target velocity by a specified amount (delta). Useful for smooth acceleration/deceleration without sudden speed changes.

Why was move_toward needed to make movement and knockback work together?

When implementing knockback in the course, I needed movement and knockback to coexist. Initially, I set velocity directly, but the knockback effect would vanish instantly.

The role of move_toward's delta parameter

The second argument delta in move_toward(target_velocity, delta) specifies "how far to move from the current velocity toward the target velocity this frame." Typically used as acceleration * delta.

# Problematic code (direct assignment)
func move_player():
    var move_vector = Input.get_vector("move_left", "move_right", "move_up", "move_down")
    # Direct assignment: instantly reaches target speed when there's input
    velocity = move_vector * move_speed
    # After knockback, any input instantly cancels the knockback effect

# Improved code: using move_toward
@export var acceleration: float = 500.0  # 500 pixels/sec per second of acceleration

func move_player():
    var move_vector = Input.get_vector("move_left", "move_right", "move_up", "move_down")
    var target_velocity = move_vector * move_speed
    # Gradual change: smoothly transitions from current to target velocity
    velocity = velocity.move_toward(target_velocity, acceleration * delta)

# Knockback handling
func apply_knockback(direction: Vector2, strength: float):
    # Apply force directly to velocity
    velocity += direction * strength

(See the course for detailed explanation)

Groups: Flexible Object Identification

Groups are like tags that can be assigned to any node. Very similar to Unity's Tag system, they let you flexibly identify object types. While Unity limits each GameObject to a single Tag, Godot groups allow multiple assignments for more flexible categorization.

For example, you can easily write logic like "is the object hit by the player's attack in the 'enemy' group?" This is more versatile than type checking like is Player and is used frequently in Godot development.

How to use:

  1. Select the node you want to tag (e.g., Slime), go to the Inspector's "Node" tab > "Groups," and add a group name like "enemies."
  2. Use the is_in_group() method in code to check group membership.

Example: Checking objects in the player's attack range

# Function connected to the player's attack Area2D
func _on_sword_area_body_entered(body: Node2D):
    # If the contacted object is in the "enemies" group, deal damage
    if body.is_in_group("enemies"):
        body.take_damage(attack_power)

    # If it's in the "pushable" group, it's a pushable object
    elif body.is_in_group("pushable"):
        # ...push logic
        pass

Collision Layers/Masks: Organizing Collision Detection

To organize collision detection, Godot provides Layers and Masks. Setting these correctly improves performance and prevents unintended collisions.

  • Collision Layer: Which layer this object exists on.
  • Collision Mask: Which layers this object should check for collisions.

Layer assignment example:

  • Layer 1: Player — the player character
  • Layer 2: Enemies — enemy characters
  • Layer 3: Weapons — player weapons (attack hitboxes)

Mask settings by scenario:

Player (Layer 1): Wants to take damage from enemies, so set Mask to "Layer 2: Enemies."

Enemies (Layer 2): Want to damage the player but pass through each other, so set Mask to "Layer 1: Player" only.

Player Weapon (Layer 3): Should only hit enemies, so set Mask to "Layer 2: Enemies" only. Won't collide with the player.

# Code example: Using collision detection
func _on_weapon_area_body_entered(body):
    # Since the weapon's Area2D Mask only includes "Layer 2: Enemies,"
    # only enemies will trigger this function
    if body.is_in_group("enemies"):
        body.take_damage(attack_power)

Naming each layer in "Project Settings" > "Layer Names" makes inspector configuration much more readable.

Terrains: RPG Maker-Level Auto-Tiling

Godot's Terrains feature makes creating auto-tiles remarkably easy. If you've used Rule Tiles in Unity, you'll be amazed by how intuitive and simple the setup is.

The basic process is to open the "Terrains" tab in a TileSet resource and visually paint which edge of each tile connects to which Terrain type (e.g., grass, dirt). Then use the brush tool in the TileMap editor, and it automatically determines boundaries and places the correct tiles.

Useful features:

  • Random brush: Dice icon randomly selects from multiple tiles
  • Probability control: Adjust the frequency of specific tiles
  • Batch physics: "F" key applies collision detection to all tiles at once

Marker2D: The Ideal Manager Node

When you need a node with no physical presence but want to attach a script for management purposes (e.g., GameManager, PuzzleManager), Unity developers would use an empty GameObject. In Godot, Marker2D fills this role.

Marker2D is the lightest 2D node, holding only position (Transform) data. With zero rendering or physics overhead, it's perfect for housing "manager" scripts that control scene-wide events and data.

Editable Children and Scene Inheritance: Efficient NPC Creation

Godot offers two main approaches for creating variations from a base scene.

  • Editable Children: Right-click an instance placed in a scene and select "Editable Children" to directly edit its contents (sprites, collision shapes, etc.). Changes are saved only within that scene. Use case: Mass-placing mob NPCs that share functionality but differ in appearance or dialogue.

  • Scene Inheritance: Create a new scene (e.g., Shopkeeper.tscn) that inherits from a base scene (e.g., BaseNPC.tscn). The inherited scene keeps the parent's functionality while allowing new nodes and scripts for unique features. Use case: Creating a merchant NPC that has the basic "talk" function plus a unique "buy/sell items" feature.

Editable Children nodes appear in yellow

Selection guide:

FeatureEditable ChildrenScene Inheritance
Best forVillager A, Villager B (different looks/dialogue)Merchant, Blacksmith (unique functionality)
ReusabilityLow (local to that scene)High (inherited scene can be placed anywhere)
ManagementSimple (base scene only)Systematic (files organized by feature)

Autoload: Cross-Scene Data Management

Autoload is like combining Unity's DontDestroyOnLoad with the singleton pattern. When you register a script or scene as Autoload in project settings, it's automatically loaded at game startup and accessible as a global variable from any scene.

How to use:

  1. Create a global management script (e.g., GameManager.gd).
  2. Go to "Project Settings" > "AutoLoad" tab, register the script, and give it a global name (e.g., GameManager).

Example: Tracking opened treasure chests

# GameManager.gd (registered as AutoLoad)
extends Node

var opened_chests: Array[String] = []
var player_hp: int = 3
var player_spawn_position: Vector2

# ...other global data
# TreasureChest.gd
extends StaticBody2D

@export var chest_id: String # Set a unique ID in the inspector ("forest_chest_01", etc.)

func _ready():
    # If this chest's ID is already recorded, show it as opened
    if GameManager.opened_chests.has(chest_id):
        play_open_animation(false) # Just play the animation

func open_chest():
    # ...opening logic...
    GameManager.opened_chests.append(chest_id) # Record the ID
    play_open_animation(true)

This makes it easy to manage data that needs to persist across scenes: player health, score, inventory, quest progress, and more.

Visual Effects: White Flash with modulate

Knockback uses move_toward

The "white flash" effect when a character takes damage can be easily implemented using the modulate property on CanvasItem (which Sprite2D, CharacterBody2D, etc. inherit from).

modulate is a color value that multiplies against the node and its descendants. The default is white (1, 1, 1), and changing it lets you easily tint the entire node.

modulate Properties

  • Inheritance: Automatically inherited from parent to child nodes
  • Multiplication: Applies as a color multiply on the original colors
  • Range: 1.0 is the base; above 1.0 brightens, below darkens

Example: Damage flash

# Player.gd
func take_damage(amount):
    # ...damage calculation...

    # Trigger flash effect
    flash_effect()

func flash_effect():
    # Flash white (multiplying by values > 1 increases brightness)
    modulate = Color(2, 2, 2)

    # Wait 0.1 seconds (await enables concise async code)
    await get_tree().create_timer(0.1).timeout

    # Restore original color
    modulate = Color(1, 1, 1)

await get_tree().create_timer(0.1).timeout is a handy one-liner for temporary waits without adding a Timer node. Changing the modulate on CharacterBody2D automatically affects its child AnimatedSprite2D too, so you don't need to operate on sprites individually.

AnimatedSprite2D vs AnimationPlayer: Choosing the Right Animation System

Godot has two main 2D animation systems, and using the right one for each situation is important.

AnimatedSprite2D

  • Purpose: Sprite frame animation
  • Feature: Create animations directly from sprite sheets
  • Use for: Character walking, attacking, idle animations
# Basic AnimatedSprite2D usage
if velocity.x > 0:
    $AnimatedSprite2D.play("move_right")
elif velocity.x < 0:
    $AnimatedSprite2D.play("move_left")
else:
    $AnimatedSprite2D.stop()

AnimationPlayer

  • Purpose: Complex animation control
  • Feature: Simultaneously controls position, rotation, scale, and properties
  • Use for: Sword swings, UI effects, camera work
# AnimationPlayer usage example (sword swing)
func attack():
    var player_animation: String = $AnimatedSprite2D.animation
    if player_animation == "move_right":
        $AnimatedSprite2D.play("attack_right")
        $AnimationPlayer.play("attack_right")  # Controls sword position and angle

Comparison with Unity's Animator

Unity Animator ≈ Godot AnimationPlayer

  • In common: State transitions, blending, simultaneous multi-property control
  • Difference: Godot's AnimationPlayer is more direct and simpler to set up

Unity's Animator is state-machine based, while Godot's AnimationPlayer lets you control animations more directly. For complex state transitions, use AnimationTree (Godot's advanced system).

Selection Guide

Animation TypeRecommended System
Sprite frame switching onlyAnimatedSprite2D
Position/rotation/scale changesAnimationPlayer
Synchronizing multiple objectsAnimationPlayer
Complex state transitionsAnimationTree

In practice, you'll often combine AnimatedSprite2D for character base animations with AnimationPlayer for weapon and effect animations.

Conclusion: Godot's True Strengths Revealed Through Practical Development

Through this practical course, I once again appreciated the elegance of Godot's design philosophy and its power for 2D game development. Many of Godot's advantages in actual development workflows only become apparent beyond basic learning.

Most Impressive Aspects

  • Consistent design philosophy: Unified concepts across Process Mode, Motion Mode, Collision Layers, etc.
  • Rich built-in features: move_and_slide(), Terrains, Y-Sorting, and more come standard
  • Intuitive workflow: Editable Children, Autoload, await — all designed for development efficiency
  • Balance of lightweight and extensible: Marker2D, Signal system — implement only what you need

The combination of node-based design and the signal system is particularly powerful, enabling loosely coupled inter-object communication while keeping development intuitive. The standard library of features like Terrains (auto-tiling), Y-Sort, and various built-in functions hits that sweet spot of "this is exactly what I needed."

Godot from a Unity Developer's Perspective

For Unity developers, Godot feels like an engine that's "easy to learn and comfortable to use." Rather than a complete switch, it's a strong candidate for project-by-project tool selection.

Areas where Godot particularly excels:

  • 2D game development (highly refined features)
  • Indie game development (lightweight engine, fast development cycles)
  • Prototyping (intuitive workflow)
  • Educational purposes (concepts are easy to grasp)

Compared to Unity, Godot can't yet match the scale of asset stores or available resources, but for 2D game development — especially pixel art games — I'm confident Godot is an extremely powerful choice. Combined with the engine's lightweight nature, it has the potential to support stress-free, rapid development cycles from prototyping to release.

Future Learning Plans

Having completed both the fundamentals and practical courses, I now have a solid understanding of Godot's basic development workflow. Going forward, I plan to explore:

  • 3D game development: Exploring Godot's 3D capabilities
  • Advanced systems: AnimationTree, VisualScript, GDExtension, etc.
  • Performance optimization: Optimization techniques for large-scale projects
  • Asset management: Efficient project structure and workflows

I'll continue studying Godot and exploring its 3D features and more advanced systems. I hope these learning notes are helpful for anyone starting game development with Godot, especially those considering a transition from Unity.

Godot4: Build a 2D Action-Adventure Game (Udemy)

Share this article