6-space-game/3-moving-elements-around/README.md
journey
title Your Game Animation Journey
section Movement Basics
Understand motion principles: 3: Student
Learn coordinate updates: 4: Student
Implement basic movement: 4: Student
section Player Controls
Handle keyboard events: 4: Student
Prevent default behaviors: 5: Student
Create responsive controls: 5: Student
section Game Systems
Build game loop: 5: Student
Manage object lifecycle: 5: Student
Implement pub/sub pattern: 5: Student
Think about your favorite games ā what makes them captivating isn't just pretty graphics, it's the way everything moves and responds to your actions. Right now, your space game is like a beautiful painting, but we're about to add movement that brings it to life.
When NASA's engineers programmed the guidance computer for the Apollo missions, they faced a similar challenge: how do you make a spacecraft respond to pilot input while automatically maintaining course corrections? The principles we'll learn today echo those same concepts ā managing player-controlled movement alongside automatic system behaviors.
In this lesson, you'll learn how to make spaceships glide across the screen, respond to player commands, and create smooth movement patterns. We'll break everything down into manageable concepts that build on each other naturally.
By the end, you'll have players flying their hero ship around the screen while enemy vessels patrol overhead. More importantly, you'll understand the core principles that power game movement systems.
mindmap
root((Game Animation))
Movement Types
Player Controlled
Automatic Motion
Physics Based
Scripted Paths
Event Handling
Keyboard Input
Mouse Events
Touch Controls
Default Prevention
Game Loop
Update Logic
Render Frame
Clear Canvas
Frame Rate Control
Object Management
Position Updates
Collision Detection
Lifecycle Management
State Tracking
Communication
Pub/Sub Pattern
Event Emitters
Message Passing
Loose Coupling
Games come alive when things start moving around, and there are fundamentally two ways this happens:
Making objects move on a computer screen is simpler than you might think. Remember those x and y coordinates from math class? That's exactly what we're working with here. When Galileo tracked Jupiter's moons in 1610, he was essentially doing the same thing ā plotting positions over time to understand motion patterns.
Moving things on screen is like creating a flipbook animation ā you need to follow these three simple steps:
flowchart LR
A["Frame N"] --> B["Update Positions"]
B --> C["Clear Canvas"]
C --> D["Draw Objects"]
D --> E["Frame N+1"]
E --> F{Continue?}
F -->|Yes| B
F -->|No| G["Game Over"]
subgraph "Animation Cycle"
H["1. Calculate new positions"]
I["2. Erase previous frame"]
J["3. Render new frame"]
end
style B fill:#e1f5fe
style C fill:#ffebee
style D fill:#e8f5e8
Do this fast enough, and boom! You've got smooth movement that feels natural to players.
Here's what it can look like in code:
// Set the hero's location
hero.x += 5;
// Clear the rectangle that hosts the hero
ctx.clearRect(0, 0, canvas.width, canvas.height);
// Redraw the game background and hero
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = "black";
ctx.drawImage(heroImg, hero.x, hero.y);
Here's what this code does:
ā Can you think of a reason why redrawing your hero many frames per second might accrue performance costs? Read about alternatives to this pattern.
This is where we connect player input to game action. When someone hits the spacebar to fire a laser or taps an arrow key to dodge an asteroid, your game needs to detect and respond to that input.
Keyboard events happen at the window level, meaning your entire browser window is listening for those keypresses. Mouse clicks, on the other hand, can be tied to specific elements (like clicking a button). For our space game, we'll focus on keyboard controls since that's what gives players that classic arcade feel.
This reminds me of how telegraph operators in the 1800s had to translate morse code input into meaningful messages ā we're doing something similar, translating keypresses into game commands.
To handle an event you need to use the window's addEventListener() method and provide it with two input parameters. The first parameter is the name of the event, for example keyup. The second parameter is the function that should be invoked as a result of the event taking place.
Here's an example:
window.addEventListener('keyup', (evt) => {
// evt.key = string representation of the key
if (evt.key === 'ArrowUp') {
// do something
}
});
Breaking down what happens here:
For key events there are two properties on the event you can use to see what key was pressed:
key - this is a string representation of the pressed key, for example 'ArrowUp'keyCode - this is a number representation, for example 37, corresponds to ArrowLeftā Key event manipulation is useful outside of game development. What other uses can you think of for this technique?
sequenceDiagram
participant User
participant Browser
participant EventSystem
participant GameLogic
participant Hero
User->>Browser: Presses ArrowUp key
Browser->>EventSystem: keydown event
EventSystem->>EventSystem: preventDefault()
EventSystem->>GameLogic: emit('KEY_EVENT_UP')
GameLogic->>Hero: hero.y -= 5
Hero->>Hero: Update position
Note over Browser,GameLogic: Event flow prevents browser defaults
Note over GameLogic,Hero: Pub/sub pattern enables clean communication
Some keys have built-in browser behaviors that can interfere with your game. Arrow keys scroll the page and spacebar jumps down ā behaviors you don't want when someone is trying to pilot their spaceship.
We can prevent these default behaviors and let our game handle the input instead. This is similar to how early computer programmers had to override system interrupts to create custom behaviors ā we're just doing it at the browser level. Here's how:
const onKeyDown = function (e) {
console.log(e.keyCode);
switch (e.keyCode) {
case 37:
case 39:
case 38:
case 40: // Arrow keys
case 32:
e.preventDefault();
break; // Space
default:
break; // do not block other keys
}
};
window.addEventListener('keydown', onKeyDown);
Understanding this prevention code:
e.preventDefault() to stop the browser's built-in behaviorEvent Handling Understanding: Before moving to automatic movement, ensure you can:
keydown and keyup eventsQuick Self-Test: What would happen if you didn't prevent default behavior for arrow keys? Answer: The browser would scroll the page, interfering with game movement
Event System Architecture: You now understand:
key strings vs keyCode numbersNow let's talk about objects that move without player input. Think about enemy ships cruising across the screen, bullets flying in straight lines, or clouds drifting in the background. This autonomous movement makes your game world feel alive even when nobody's touching the controls.
We use JavaScript's built-in timers to update positions at regular intervals. This concept is similar to how pendulum clocks work ā a regular mechanism that triggers consistent, timed actions. Here's how simple it can be:
const id = setInterval(() => {
// Move the enemy on the y axis
enemy.y += 10;
}, 100);
Here's what this movement code does:
Here's the concept that ties everything together ā the game loop. If your game were a movie, the game loop would be the film projector, showing frame after frame so fast that everything appears to move smoothly.
Every game has one of these loops running behind the scenes. It's a function that updates all game objects, redraws the screen, and repeats this process continuously. This keeps track of your hero, all the enemies, any lasers flying around ā the entire game state.
This concept reminds me of how early film animators like Walt Disney had to redraw characters frame by frame to create the illusion of movement. We're doing the same thing, just with code instead of pencils.
Here's what a game loop can typically look like, expressed in code:
flowchart TD
A["Start Game Loop"] --> B["Clear Canvas"]
B --> C["Fill Background"]
C --> D["Update Game Objects"]
D --> E["Draw Hero"]
E --> F["Draw Enemies"]
F --> G["Draw UI Elements"]
G --> H["Wait for Next Frame"]
H --> I{Game Running?}
I -->|Yes| B
I -->|No| J["End Game"]
subgraph "Frame Rate Control"
K["60 FPS = 16.67ms"]
L["30 FPS = 33.33ms"]
M["10 FPS = 100ms"]
end
style B fill:#ffebee
style D fill:#e1f5fe
style E fill:#e8f5e8
style F fill:#e8f5e8
const gameLoopId = setInterval(() => {
function gameLoop() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = "black";
ctx.fillRect(0, 0, canvas.width, canvas.height);
drawHero();
drawEnemies();
drawStaticObjects();
}
gameLoop();
}, 200);
Understanding the game loop structure:
Now we'll add movement to the static scene you built previously. We're going to transform it from a screenshot into an interactive experience. We'll work through this step by step to ensure each piece builds on the last.
Grab the code from where we left off in the previous lesson (or start with the code in the Part II- starter folder if you need a fresh start).
Here's what we're building today:
Let's begin implementing these features.
Locate the files that have been created for you in the your-work sub folder. It should contain the following:
-| assets
-| enemyShip.png
-| player.png
-| index.html
-| app.js
-| package.json
You start your project in the your-work folder by typing:
cd your-work
npm start
What this command does:
http://localhost:5000The above will start a HTTP Server on address http://localhost:5000. Open up a browser and input that address, right now it should render the hero and all the enemies; nothing is moving - yet!
Add dedicated objects for hero and enemy and game object, they should have x and y properties. (Remember the portion on Inheritance or composition).
HINT game object should be the one with x and y and the ability to draw itself to a canvas.
Tip: Start by adding a new
GameObjectclass with its constructor delineated as below, and then draw it to the canvas:
class GameObject {
constructor(x, y) {
this.x = x;
this.y = y;
this.dead = false;
this.type = "";
this.width = 0;
this.height = 0;
this.img = undefined;
}
draw(ctx) {
ctx.drawImage(this.img, this.x, this.y, this.width, this.height);
}
}
Understanding this base class:
dead flag to track whether the object should be removeddraw() method that renders the object on the canvasclassDiagram
class GameObject {
+x: number
+y: number
+dead: boolean
+type: string
+width: number
+height: number
+img: Image
+draw(ctx)
}
class Hero {
+speed: number
+type: "Hero"
+width: 98
+height: 75
}
class Enemy {
+type: "Enemy"
+width: 98
+height: 50
+setInterval()
}
GameObject <|-- Hero
GameObject <|-- Enemy
class EventEmitter {
+listeners: object
+on(message, listener)
+emit(message, payload)
}
Now, extend this GameObject to create the Hero and Enemy:
class Hero extends GameObject {
constructor(x, y) {
super(x, y);
this.width = 98;
this.height = 75;
this.type = "Hero";
this.speed = 5;
}
}
class Enemy extends GameObject {
constructor(x, y) {
super(x, y);
this.width = 98;
this.height = 50;
this.type = "Enemy";
const id = setInterval(() => {
if (this.y < canvas.height - this.height) {
this.y += 5;
} else {
console.log('Stopped at', this.y);
clearInterval(id);
}
}, 300);
}
}
Key concepts in these classes:
GameObject using the extends keywordsuper(x, y)setInterval()Add key-event handlers to handle key navigation (move hero up/down left/right)
REMEMBER it's a cartesian system, top-left is 0,0. Also remember to add code to stop default behavior
Tip: Create your
onKeyDownfunction and attach it to the window:
const onKeyDown = function (e) {
console.log(e.keyCode);
// Add the code from the lesson above to stop default behavior
switch (e.keyCode) {
case 37:
case 39:
case 38:
case 40: // Arrow keys
case 32:
e.preventDefault();
break; // Space
default:
break; // do not block other keys
}
};
window.addEventListener("keydown", onKeyDown);
What this event handler does:
Check your browser console at this point, and watch the keystrokes being logged.
Implement the Pub sub pattern, this will keep your code clean as you follow the remaining parts.
The Publish-Subscribe pattern helps organize your code by separating event detection from event handling. This makes your code more modular and easier to maintain.
To do this last part, you can:
Add an event listener on the window:
window.addEventListener("keyup", (evt) => {
if (evt.key === "ArrowUp") {
eventEmitter.emit(Messages.KEY_EVENT_UP);
} else if (evt.key === "ArrowDown") {
eventEmitter.emit(Messages.KEY_EVENT_DOWN);
} else if (evt.key === "ArrowLeft") {
eventEmitter.emit(Messages.KEY_EVENT_LEFT);
} else if (evt.key === "ArrowRight") {
eventEmitter.emit(Messages.KEY_EVENT_RIGHT);
}
});
What this event system does:
flowchart TD
A["Keyboard Input"] --> B["Window Event Listener"]
B --> C["Event Emitter"]
C --> D["KEY_EVENT_UP"]
C --> E["KEY_EVENT_DOWN"]
C --> F["KEY_EVENT_LEFT"]
C --> G["KEY_EVENT_RIGHT"]
D --> H["Hero Movement"]
D --> I["Sound System"]
D --> J["Visual Effects"]
E --> H
F --> H
G --> H
style A fill:#e1f5fe
style C fill:#e8f5e8
style H fill:#fff3e0
Create an EventEmitter class to publish and subscribe to messages:
class EventEmitter {
constructor() {
this.listeners = {};
}
on(message, listener) {
if (!this.listeners[message]) {
this.listeners[message] = [];
}
this.listeners[message].push(listener);
}
Add constants and set up the EventEmitter:
const Messages = {
KEY_EVENT_UP: "KEY_EVENT_UP",
KEY_EVENT_DOWN: "KEY_EVENT_DOWN",
KEY_EVENT_LEFT: "KEY_EVENT_LEFT",
KEY_EVENT_RIGHT: "KEY_EVENT_RIGHT",
};
let heroImg,
enemyImg,
laserImg,
canvas, ctx,
gameObjects = [],
hero,
eventEmitter = new EventEmitter();
Understanding the setup:
Initialize the game
function initGame() {
gameObjects = [];
createEnemies();
createHero();
eventEmitter.on(Messages.KEY_EVENT_UP, () => {
hero.y -= 5;
});
eventEmitter.on(Messages.KEY_EVENT_DOWN, () => {
hero.y += 5;
});
eventEmitter.on(Messages.KEY_EVENT_LEFT, () => {
hero.x -= 5;
});
Setup the game loop
Refactor the window.onload function to initialize the game and set up a game loop on a good interval. You'll also add a laser beam:
window.onload = async () => {
canvas = document.getElementById("canvas");
ctx = canvas.getContext("2d");
heroImg = await loadTexture("assets/player.png");
enemyImg = await loadTexture("assets/enemyShip.png");
laserImg = await loadTexture("assets/laserRed.png");
initGame();
const gameLoopId = setInterval(() => {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = "black";
ctx.fillRect(0, 0, canvas.width, canvas.height);
drawGameObjects(ctx);
}, 100);
};
Understanding the game setup:
awaitAdd code to move enemies at a certain interval
Refactor the createEnemies() function to create the enemies and push them into the new gameObjects class:
function createEnemies() {
const MONSTER_TOTAL = 5;
const MONSTER_WIDTH = MONSTER_TOTAL * 98;
const START_X = (canvas.width - MONSTER_WIDTH) / 2;
const STOP_X = START_X + MONSTER_WIDTH;
for (let x = START_X; x < STOP_X; x += 98) {
for (let y = 0; y < 50 * 5; y += 50) {
const enemy = new Enemy(x, y);
enemy.img = enemyImg;
gameObjects.push(enemy);
}
}
}
What the enemy creation does:
and add a createHero() function to do a similar process for the hero.
function createHero() {
hero = new Hero(
canvas.width / 2 - 45,
canvas.height - canvas.height / 4
);
hero.img = heroImg;
gameObjects.push(hero);
}
What the hero creation does:
and finally, add a drawGameObjects() function to start the drawing:
function drawGameObjects(ctx) {
gameObjects.forEach(go => go.draw(ctx));
}
Understanding the drawing function:
draw() method on each objectComplete Game System Understanding: Verify your mastery of the entire architecture:
System Integration: Your game now demonstrates:
Professional Patterns: You've implemented:
Your enemies should start advancing on your hero spaceship! } }
and add a `createHero()` function to do a similar process for the hero.
```javascript
function createHero() {
hero = new Hero(
canvas.width / 2 - 45,
canvas.height - canvas.height / 4
);
hero.img = heroImg;
gameObjects.push(hero);
}
and finally, add a drawGameObjects() function to start the drawing:
function drawGameObjects(ctx) {
gameObjects.forEach(go => go.draw(ctx));
}
Your enemies should start advancing on your hero spaceship!
Here's a challenge that will improve your game's polish: adding boundaries and smooth controls. Currently, your hero can fly off the screen, and the movement might feel choppy.
Your Mission: Make your spaceship feel more realistic by implementing screen boundaries and fluid movement. This is similar to how NASA's flight control systems prevent spacecraft from exceeding safe operational parameters.
Here's what to build: Create a system that keeps your hero spaceship on screen, and make the controls feel smooth. When players hold down an arrow key, the ship should glide continuously rather than moving in discrete steps. Consider adding visual feedback when the ship reaches screen boundaries ā perhaps a subtle effect to indicate the edge of the play area.
Learn more about agent mode here.
Code organization becomes increasingly important as projects grow. You might have noticed your file getting crowded with functions, variables, and classes all mixed together. This reminds me of how the engineers organizing the Apollo mission code had to create clear, maintainable systems that multiple teams could work on simultaneously.
Your mission: Think like a software architect. How would you organize your code so that six months from now, you (or a teammate) could understand what's happening? Even if everything stays in one file for now, you can create better organization:
Reflection questions:
We've been building everything from scratch, which is fantastic for learning, but here's a little secret ā there are some amazing JavaScript frameworks out there that can handle a lot of the heavy lifting for you. Once you feel comfortable with the fundamentals we've covered, it's worth exploring what's available.
Think of frameworks like having a well-stocked toolbox instead of making every tool by hand. They can solve many of those code organization challenges we talked about, plus offer features that would take weeks to build yourself.
Things worth exploring:
timeline
title Game Animation & Interaction Learning Progression
section Movement Fundamentals (20 minutes)
Animation Principles: Frame-based animation
: Position updates
: Coordinate systems
: Smooth movement
section Event Systems (25 minutes)
User Input: Keyboard event handling
: Default behavior prevention
: Event object properties
: Window-level listening
section Game Architecture (30 minutes)
Object Design: Inheritance patterns
: Base class creation
: Specialized behaviors
: Polymorphic interfaces
section Communication Patterns (35 minutes)
Pub/Sub Implementation: Event emitters
: Message constants
: Loose coupling
: System integration
section Game Loop Mastery (40 minutes)
Real-time Systems: Frame rate control
: Update/render cycle
: State management
: Performance optimization
section Advanced Techniques (45 minutes)
Professional Features: Collision detection
: Physics simulation
: State machines
: Component systems
section Game Engine Concepts (1 week)
Framework Understanding: Entity-component systems
: Scene graphs
: Asset pipelines
: Performance profiling
section Production Skills (1 month)
Professional Development: Code organization
: Team collaboration
: Testing strategies
: Deployment optimization
After completing this lesson, you now have mastered:
addEventListener('keydown', console.log) to see keyboard eventssetInterval to create continuous movementevent.preventDefault()Real-World Applications: Your game animation skills apply directly to:
Professional Skills Gained: You can now:
Game Development Concepts Mastered:
Next Level: You're ready to add collision detection, scoring systems, sound effects, or explore modern game frameworks like Phaser or Three.js!
š Achievement Unlocked: You've built a complete interactive game system with professional architecture patterns!