Now that we’ve successfully set up a 3D scene, imported a custom 3D model, and made it rotate, it’s time to add user input. In this guide, we’ll modify our script so that pressing the spacebar makes the model jump.


Final Code Structure

Project/
├── Models/
│   └── 000_Snowpuff.glb   # Custom 3D Model
├── Scripts/
│   ├── main.gd       # Root scene script
│   └── skybox.gd     # Skybox configuration
├── Textures/
│   └── default_sky.hdr  # HDRI skybox texture
└── project.godot

1. Understanding Input in Godot 4.3

Before writing code, let’s discuss how Godot handles input.

🔹 Using Input.is_action_just_pressed()

Godot processes input using the Input singleton. The function:

Input.is_action_just_pressed("jump")
  • Returns true only on the frame the key is pressed.
  • Does not return true if the key is held.

By default, Godot doesn’t have a “jump” action, so we need to set it up in the Input Map.


2. Setting Up Input Actions in Godot

Step 1: Open the Input Map

  1. Go to Project > Project Settings.
  2. Navigate to the Input Map tab.
  3. In the Action box, type "jump" and press Add.

Step 2: Assign a Key

  1. Select "jump" in the list.
  2. Click Add Event > Key.
  3. Press Spacebar and confirm.

Now, Godot recognizes "jump" when Space is pressed.


3. Create a Player Class (player.gd)

Instead of directly controlling the model inside main.gd, we will encapsulate movement logic inside a dedicated Player class.

🔹 File Structure After Modularization

Project/
├── Models/
│   └── 000_Snowpuff.glb   # Custom 3D Model
├── Scripts/
│   ├── main.gd       # Root scene script
│   ├── player.gd     # Player class (handles input & movement)
│   └── skybox.gd     # Skybox configuration
├── Textures/
│   └── default_sky.hdr
└── project.godot

📝 player.gd (Player Class)

extends Node3D

class_name Player

var jump_force: float = 5.0
var gravity: float = -9.8
var rotation_speed: float = 0.5
var velocity: Vector3 = Vector3.ZERO
var is_jumping: bool = false
var snowpuff: Node3D

func _init():
    # Add a rotating snowpuff
    var snowpuff_model = load("res://Models/000_Snowpuff.glb")

    if snowpuff_model:
        snowpuff = snowpuff_model.instantiate()
        snowpuff.transform.origin = Vector3(0,0,0)
        add_child(snowpuff)

func _process(delta):
    # Continuously rotate the model
    rotate_y(delta * rotation_speed)

    # Apply gravity if jumping
    if is_jumping:
        velocity.y += gravity * delta
        transform.origin += velocity * delta

        # Stop at the ground level
        if transform.origin.y <= 0:
            transform.origin.y = 0
            velocity.y = 0
            is_jumping = false

    # Handle jump input
    if Input.is_action_just_released("Jump") and not is_jumping:
        is_jumping = true
        velocity.y = jump_force

🔹 Explanation of player.gd

  1. Modular Class:

    • We define Player using class_name Player, which turns it into a reusable class. This means that instead of just being a script attached to a single object, we can now instantiate it multiple times, just like a built-in Godot class.
    • This modular approach makes it easier to organize our project, since Player.gd handles only player-specific logic, while main.gd is responsible for the scene setup.
    • Because of this, Player can now be created and used in multiple scenes without needing to rewrite movement logic every time.
  2. Exported Variables:

    • The variables jump_force, gravity, and rotation_speed are defined using the @export keyword.
    • What does @export do?
      • It makes these variables adjustable from the Godot Editor, allowing us to tweak values like jump height or gravity strength without modifying code.
      • This is useful for balancing gameplay—if the jump is too weak or too strong, we can simply adjust jump_force in the Inspector without reopening the script.
    • How did we determine these values?
      • jump_force = 5.0 → A reasonable jump height based on typical platformer physics. If set too low, the jump would barely be visible.
      • gravity = -9.8 → Mimics real-world gravity (Earth’s gravity is ~9.8 m/s²). This makes falling feel natural and consistent.
      • rotation_speed = 0.5 → A slow, smooth rotation for visual appeal. If too fast, it could feel jarring.
  3. Jump Logic & Gravity:

    • We separate the jump logic from main.gd to keep things organized and self-contained.
    • How does gravity work in our script?
      • When the player jumps, an upward velocity (jump_force) is applied.
      • Each frame, the velocity is adjusted downward by gravity, causing a realistic falling effect.
      • Once the character reaches the ground (y = 0), we reset the velocity and allow another jump.
    • This setup prevents “infinite jumping”, since the character must land before jumping again.
  4. Rotation:

    • The function rotate_y(delta * rotation_speed) makes the player spin continuously along the Y-axis.
    • Why is delta used?
      • delta represents the time passed since the last frame.
      • Without delta, the rotation would be dependent on frame rate, meaning faster computers would spin the player more quickly.
      • Multiplying by delta ensures smooth and consistent rotation across all devices.
  5. Understanding _process():

    • The _process(delta) function is a built-in Godot function that runs every frame.
    • This makes it ideal for continuous actions, like checking for input (is_action_just_pressed()) or applying gravity.
    • Why not use _ready() instead?
      • _ready() only runs once when the object is created.
      • _process() runs constantly, making it necessary for things like movement and physics updates.

4. Updating main.gd

Now that we’ve moved the player logic into player.gd, our main.gd will only handle scene setup.

📝 main.gd (Root Scene)

extends Node3D

var snowpuff: Node3D
var player: Player

func _ready():
    # Configure camera
    var camera = Camera3D.new()
    camera.position = Vector3(0, 1, 3)
    camera.look_at(Vector3.ZERO)
    add_child(camera)
    camera.make_current()

    # Add world environment
    var skybox = load("res://Scripts/skybox.gd").new()
    add_child(skybox)

    # Add player
    player = Player.new()
    add_child(player)

Fixing Input Delay: Understanding Input Actions & Jump Logic

At this point, you may have noticed that jumping feels delayed or unresponsive. This is likely because we’re currently detecting input using:

if Input.is_action_just_released("jump") and not is_jumping:

Since is_action_just_released() only triggers when the spacebar is lifted, it introduces a slight delay between when you press the key and when the jump occurs. This might feel sluggish, especially in fast-paced gameplay.


5. Exploring Input Detection Options in Godot

Godot provides multiple ways to detect input, each with its own use case. Let’s go through them:

🔹 Input.is_action_pressed("jump")

  • Triggers every frame as long as the key is held down.

  • Good for holding to charge a jump, flying mechanics, or continuous movement.

  • Example use case:

    if Input.is_action_pressed("jump"):
        velocity.y += jump_force * delta  # Keep rising as long as the key is held
    

🔹 Input.is_action_just_pressed("jump")

  • Triggers only once when the key is initially pressed.

  • More responsive than is_action_just_released() since it activates the moment the key is pressed.

  • Best for jumping mechanics to ensure instant responsiveness.

  • Fixing our delay issue:

    if Input.is_action_just_pressed("jump") and not is_jumping:
        is_jumping = true
        velocity.y = jump_force
    
  • This makes jumping feel immediate, fixing the sluggish input issue.

🔹 Input.is_action_just_released("jump")

  • Triggers only once when the key is released.

  • Good for jump charging mechanics (e.g., the longer you hold, the higher you jump).

  • Example use case:

    if Input.is_action_just_released("jump"):
        velocity.y = charge_amount  # Jump strength depends on hold duration
    

6. Implementing Double Jumping

Now, what if we want the player to double jump? We need to track jumps and allow one extra jump mid-air.

🔹 Adding Double Jump Logic

extends Node3D

class_name Player

var jump_force: float = 5.0
var gravity: float = -9.8
var rotation_speed: float = 0.5
var velocity: Vector3 = Vector3.ZERO
var max_jumps: int = 2
var jump_count: int = 0
var snowpuff: Node3D

func _init():
	# Add a rotating snowpuff
	var snowpuff_model = load("res://Models/000_Snowpuff.glb")
	if snowpuff_model:
		snowpuff = snowpuff_model.instantiate()
		snowpuff.transform.origin = Vector3(0,0,0)
		add_child(snowpuff)

func _process(delta):
	# Rotate the model continuously
	rotate_y(delta * rotation_speed)

	# Apply gravity
	velocity.y += gravity * delta
	transform.origin += velocity * delta

	# Detect when the player hits the ground
	if transform.origin.y <= 0:
		transform.origin.y = 0
		velocity.y = 0
		jump_count = 0  # Reset jumps when landing

	# Handle jump input
	if Input.is_action_just_pressed("Jump") and jump_count < max_jumps:
		velocity.y = jump_force
		jump_count += 1

🔹 How This Works

  • max_jumps = 2 → The player can jump twice before landing.
  • jump_count → Tracks how many jumps have been used.
  • Resets jump_count = 0 on landing → So the player can jump again.

7. What If We Want Infinite Jumps?

If we want infinite jumps (e.g., a flying mechanic), we simply remove the jump limit:

if Input.is_action_pressed("jump"):
    velocity.y = jump_force  # Continuously jump while space is held

This creates a flappy-bird-style flying effect.


8. Choosing the Right Input Method

Input Method Use Case Example
is_action_pressed("jump") Holding the key for continuous movement or flying Holding jump to fly
is_action_just_pressed("jump") Instant jump response when pressing space Most platformer games
is_action_just_released("jump") Charge-based jumps (higher jump when holding longer) Super Mario 64