Godot 4.3 Adding Basic Keyboard Input
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
- Go to Project > Project Settings.
- Navigate to the Input Map tab.
- In the Action box, type
"jump"
and press Add.
Step 2: Assign a Key
- Select
"jump"
in the list. - Click Add Event > Key.
- 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
-
Modular Class:
- We define
Player
usingclass_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, whilemain.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.
- We define
-
Exported Variables:
- The variables
jump_force
,gravity
, androtation_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.
- The variables
-
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.
- When the player jumps, an upward velocity (
- This setup prevents “infinite jumping”, since the character must land before jumping again.
- We separate the jump logic from
-
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.
- The function
-
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.
- The
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 |