CS 470 Game Development

Week 2, Lecture 4

Micro-Game 1: Wanderer

Building Your First Interactive Character

Today's Goal

Build a character that moves with arrow keys

Along the way, learn the fundamental building blocks of every Godot game

Agenda (~50 minutes)

  1. Nodes, Scenes, and the Scene Tree (7 min)
  2. Coordinate System & Vectors (6 min)
  3. GDScript Basics (5 min)
  4. The Game Loop & Scripts (6 min)
  5. Understanding Delta (5 min)
  6. Input Handling (5 min)
  7. Building Wanderer (Live Demo) (12 min)
  8. Screen Boundaries & Wrap-up (4 min)

1. Nodes, Scenes, and the Scene Tree

The foundation of everything in Godot

Everything is a Node

Godot's architecture: everything is a Node

A player, a background, a sound effect, a timer — all nodes

Each node type has a specific job:

Common Node Types

Node Type What It Does
Node Base type. Can hold logic and children.
Node2D Adds 2D position, rotation, scale.
Sprite2D Displays an image (texture) on screen.
Area2D Detects overlaps with other objects.
AudioStreamPlayer Plays sound.

There are hundreds of node types. You'll learn them gradually.

Scenes = Saved Node Trees

A scene is a tree of nodes saved as a .tscn file

A scene can be:

  • A player character
  • A bullet
  • An entire level
  • A full game

Key insight: Scenes are reusable — drag them into other scenes!

Wanderer Scene Structure

Our entire game is one scene:

Game (Node2D)            ← root of the scene
  └── Player (Node2D)    ← has a position we can move
        └── Sprite2D     ← displays the icon image

Why nest Sprite2D under Player?

Because Player is a container. Later we'll add collision shapes, health bars, particles under it.

The Scene Tree (Runtime)

When you press Play, Godot builds a scene tree

A live hierarchy of every node currently in the game

The engine walks this tree every frame, calling functions on each node

This is the heartbeat of your game

2. The Coordinate System

May surprise you if you're coming from math class!

Godot 2D Coordinates

(0, 0) ——————————→ +X (right)
  |
  |
  |
  ↓
 +Y (down)
  • Origin (0, 0) is the top-left corner
  • +X goes right
  • +Y goes down (not up!)

Why Down is Positive?

Historical reason: screens draw pixels top-to-bottom, left-to-right

This is standard in almost all 2D game engines

Example: 1152 × 648 Window

Position Where on Screen
(0, 0) Top-left corner
(1152, 0) Top-right corner
(0, 648) Bottom-left corner
(576, 324) Dead center

Implication for Movement

To move up on screen → subtract from Y

To move downadd to Y

This trips up everyone at first. You'll get used to it!

Quick Vector Refresher

A Vector2 is a pair of numbers (x, y)

Can represent: position, direction, velocity, or anything with two components

Key Vector Operations

Addition:        (2, 3) + (1, -1) = (3, 2)       → combining movement
Scalar multiply: (1, 0) * 5      = (5, 0)        → scaling (speed)
Length:          |(3, 4)|         = 5              → distance from origin
Normalized:      (3, 4).normalized() = (0.6, 0.8) → same direction, length 1

Why Normalize?

Suppose the player presses right + down simultaneously:

direction = (1, 0) + (0, 1) = (1, 1)
length of (1, 1) = √2 ≈ 1.414

The player moves ~41% faster diagonally! That's unfair.

(1, 1).normalized() = (0.707, 0.707)
length = 1.0 ✓

Now all directions produce the same speed!

3. GDScript — Quick Comparison

If you know Python, you're 80% there

Similarities with Python

  • Indentation-based (no curly braces)
  • Dynamic-feeling syntax
  • for, if, while work the same
  • print() works the same

Key Differences

Python GDScript Notes
class MyClass: extends Node2D Every script extends a node type
self.x x or position No self keyword
def __init__(self): func _ready(): Called when node enters scene
def update(self, dt): func _process(delta): Called every frame
x: int = 5 var x: int = 5 Variables declared with var
def foo(self): func foo(): Functions declared with func

Where's self?

In Python, every method has explicit self

In GDScript, the node is the implicit context

# GDScript — these are equivalent:
position.x += 5
self.position.x += 5    # works, but nobody writes this

When you write position, it means "this node's position"

4. Scripts and How They Run

Understanding the heartbeat of your game

Attaching a Script

When you attach a script to a node, you give it custom behavior

extends Node2D    # "I am a Node2D with extra behavior"
  • One script per node
  • Script has access to all node's properties (position, rotation, etc.)

The Game Loop

Every game engine runs an infinite loop:

while game_is_running:
    process_input()
    update_all_objects()     ← your _process() runs here
    render_frame()
    wait_for_vsync()         ← targets ~60 FPS

Each iteration = one frame (~16.6 milliseconds at 60 FPS)

Walking the Scene Tree

During "update all objects", Godot walks the entire scene tree

Frame 1:  Game._process() → Player._process() → (Sprite2D has no script)
Frame 2:  Game._process() → Player._process() → ...
Frame 3:  Game._process() → Player._process() → ...
...60 times per second

If _process exists on a node, it runs. Every. Single. Frame.

The Special Functions

Function When It Runs Use It For
_ready() Once when node enters scene Initialization, set starting values
_process(delta) Every frame (~60 times/sec) Game logic, movement, animation
_input(event) Every input event Responding to discrete events

There are others (_physics_process, _enter_tree) — we'll meet them later

What is position?

Every Node2D has a built-in property: position

A Vector2 representing where the node sits relative to its parent

position.x += 5    # move 5 pixels to the right

Since our Player's parent is Game (at origin), Player.position is effectively the screen position

5. Understanding delta

The secret to smooth, consistent gameplay

The Problem

func _process(delta):
    position.x += 5     # move 5 pixels per frame

At 60 FPS → 300 pixels/sec

At 30 FPS → 150 pixels/sec

Game speed depends on framerate. That's bad!

The Solution: delta

delta = time in seconds since the last frame

At 60 FPS, delta ≈ 0.0166

At 30 FPS, delta ≈ 0.0333

Using Delta Correctly

func _process(delta):
    position.x += 300 * delta    # 300 pixels per SECOND

At 60 FPS: 300 × 0.0166 = 5.0 pixels/frame → 300 px/sec ✓

At 30 FPS: 300 × 0.0333 = 10.0 pixels/frame → 300 px/sec ✓

Rule: Always multiply movement by delta

Delta Timer Example

Print a message after 3 seconds:

extends Node2D

var timer = 0.0
var message_printed = false

func _process(delta):
    timer += delta
    if timer >= 3.0 and not message_printed:
        print("3 seconds have passed!")
        message_printed = true

Key takeaway: delta turns discrete frames into smooth, continuous time

6. How Input Works

Without blocking!

The Wrong Mental Model

Console program approach:

key = input("Press a key: ")   # BLOCKS — program freezes

Games can never do this!

If the game froze waiting for input, nothing would move, animate, or render

The Right Mental Model: Polling

Godot checks the state of all keys every frame

if Input.is_action_pressed("ui_right"):
    # Right arrow is currently held down — this frame

This is called polling

It doesn't wait. It doesn't block. It just checks: "Is this key down right now?"

Pressed vs Just Pressed

Function Returns true when...
is_action_pressed() Key is held down (true every frame)
is_action_just_pressed() Key was just pressed (true for one frame)

For movement, we want is_action_pressed — keep moving while held

What are "Actions"?

Godot maps physical keys to named actions

Action Name Default Key
"ui_right"→ (Right Arrow)
"ui_left"← (Left Arrow)
"ui_up"↑ (Up Arrow)
"ui_down"↓ (Down Arrow)
"ui_accept"Enter / Space

You can define custom actions in Project → Project Settings → Input Map

7. Building Wanderer

Step-by-step live demo

Step 1: Create the Project

  1. Open Godot 4 → New Project
  2. Name: Wanderer
  3. Pick a folder
  4. Click Create & Edit

Step 2: Build the Scene Tree

  1. Scene panel → + Other Node → search Node2D → Create
  2. Rename it to Game
  3. Select Game+ button → Node2D → rename to Player
  4. Select Player → Add Child Node → Sprite2D
  5. Select Sprite2D → Inspector → Texture → drag icon.svg
  6. Select Player → Inspector → Position(576, 324)
  7. Ctrl+S → save as game.tscn

Your Scene Tree Should Look Like:

Game (Node2D)
  └── Player (Node2D)
        └── Sprite2D

You should see the Godot icon centered in the viewport

Step 3: Attach a Script

  1. Select the Player node
  2. Click scroll icon (or right-click → Attach Script)
  3. Keep all defaults → click Create

The Movement Script

extends Node2D

var speed = 300.0

func _process(delta):
    var direction = Vector2.ZERO    # (0, 0) — no movement yet

    if Input.is_action_pressed("ui_right"):
        direction.x += 1
    if Input.is_action_pressed("ui_left"):
        direction.x -= 1
    if Input.is_action_pressed("ui_down"):
        direction.y += 1            # remember: +Y is DOWN
    if Input.is_action_pressed("ui_up"):
        direction.y -= 1            # -Y is UP

    if direction.length() > 0:
        direction = direction.normalized()  # fix diagonal speed

    position += direction * speed * delta

Step 4: Run It!

  1. Press F5 (or Play ▶ button)
  2. Godot asks for main scene → select game.tscn
  3. Use arrow keys — the icon moves!

🎉 You just made your first interactive game object!

Code Walkthrough

What happens every frame (~60 times/sec):

  1. direction starts as (0, 0)
  2. Each if check adds to the direction vector
  3. Pressing Right → (1, 0), Right + Up → (1, -1)
  4. If any key pressed, normalize to length 1
  5. direction * speed * delta = movement vector
  6. position += moves the Player node

Pattern: Build direction → normalize → apply speed & delta

8. Screen Boundaries

Keeping the player on screen

The Problem

Hold Right → character disappears off the edge!

We need to clamp (restrict) the position to stay within the screen

Getting Screen Size

var screen_size = get_viewport_rect().size
# Returns a Vector2, e.g. (1152, 648)

get_viewport_rect() returns the rectangle of the current viewport

.size gives width and height as a Vector2

The clamp() Function

clamp(value, min, max) forces a number to stay within a range:

clamp(500, 0, 1152)  → 500   (already in range)
clamp(-20, 0, 1152)  → 0     (was below min)
clamp(1200, 0, 1152) → 1152  (was above max)

Adding Clamping

Add these lines at the end of _process:

    var screen_size = get_viewport_rect().size
    position.x = clamp(position.x, 0, screen_size.x)
    position.y = clamp(position.y, 0, screen_size.y)

Run it again — the icon now stops at the edges!

Complete Final Script

extends Node2D

var speed = 300.0

func _process(delta):
    var direction = Vector2.ZERO

    if Input.is_action_pressed("ui_right"):
        direction.x += 1
    if Input.is_action_pressed("ui_left"):
        direction.x -= 1
    if Input.is_action_pressed("ui_down"):
        direction.y += 1
    if Input.is_action_pressed("ui_up"):
        direction.y -= 1

    if direction.length() > 0:
        direction = direction.normalized()

    position += direction * speed * delta

    # Keep player inside the screen
    var screen_size = get_viewport_rect().size
    position.x = clamp(position.x, 0, screen_size.x)
    position.y = clamp(position.y, 0, screen_size.y)

Summary: What You Learned

Core Concepts (1/2)

Nodes Building blocks of everything in Godot
Scenes Saved tree of nodes (`.tscn`). Reusable.
Scene Tree Live hierarchy at runtime. Engine walks it every frame.
Coordinate System Origin = top-left. +X = right. +Y = down.
extends GDScript's version of class inheritance
No self Access node properties directly (position, rotation)

Core Concepts (2/2)

_ready() Runs once when node enters the tree
_process(delta) Runs every frame. The heartbeat of game logic.
delta Seconds since last frame. Always multiply by it!
position Vector2 — where this node is relative to parent
Input polling is_action_pressed() — non-blocking, checked every frame
normalized() Scales vector to length 1. Prevents faster diagonals.
clamp() Restricts value to min/max range

Exercises

Try these on your own!

Beginner Exercises

  1. Speed Experiment: Change speed to 600, then 100. What feels "right"?
  2. Delta Timer: Accumulate delta each frame. Print "Hello!" every 2 seconds (reset timer after each print).
  3. Boundary Padding: The icon's center stops at the edge, but half goes off-screen. Account for icon size (128×128, radius=64). Hint: clamp between 64 and screen_size - 64.

Intermediate Exercises

  1. Wrap Instead of Clamp: Make the player wrap around — exit right, appear on left. Use if statements instead of clamp.
  2. Sprint Key: Double speed when holding Shift. Hint: check Input.is_key_pressed(KEY_SHIFT).
  3. Mouse Follower: Move icon toward mouse cursor at constant speed. Hint: get_viewport().get_mouse_position(), calculate direction, normalize.

Advanced Exercise

  1. Smooth Rotation: Make the icon rotate to face movement direction. Hints:
    • Calculate target angle: direction.angle()
    • Smoothly interpolate: rotation = lerp_angle(rotation, target_angle, 5.0 * delta)
    • Only rotate when moving: if direction.length() > 0

Next Lecture

We'll add:

  • Collision detection (Area2D)
  • Collectible items
  • Scoring system
  • Win/lose conditions

Building on today's foundation to make a real game!

Questions?

Try building Wanderer on your own

Experiment with the exercises

Office hours: [Your time here]