45 Bringing Your World to Life: Basic Sprite Movement
In our previous lessons, we’ve explored the fundamentals of Pyxel, from drawing primitives to loading sprites. Today, we take a major step toward making our games feel alive by learning how to move sprites around the screen. Just as a puppeteer gives life to a puppet by making it move, we’ll give life to our virtual worlds through controlled movement.
45.1 Why Movement Matters
Movement is what transforms static images into dynamic games. It creates:
- Player Agency: Movement lets players interact with and affect the game world
- Challenge: Moving obstacles and enemies create gameplay challenges
- Visual Interest: Even simple movement patterns make a scene more engaging
- Storytelling: Movement can convey character personality and narrative
Let’s start with the essential movement techniques that form the foundation of game development.
45.2 The Basic Movement Model
At its core, sprite movement in games follows a simple pattern:
- Store the sprite’s position in variables
- Update those variables based on inputs or logic
- Draw the sprite at its new position each frame
Let’s implement this pattern with a simple moving square:
import pyxel
class BasicMovement:
def __init__(self):
160, 120, title="Basic Movement")
pyxel.init(
# 1. Store position in variables
self.square_x = 80 # Start at the center of the screen
self.square_y = 60
self.square_size = 8
self.square_vel = 1
self.update, self.draw)
pyxel.run(
def update(self):
if pyxel.btnp(pyxel.KEY_Q):
pyxel.quit()
# 2. Update position variables
# Move right by 1 pixel each frame
self.square_x = self.square_x + self.square_vel
# Wrap around when reaching the right edge
if self.square_x > 160:
self.square_x = 0
# If you wanted it to bounce back instead...
# if self.square_x > 152:
# self.square_vel = -self.square_vel
# if self.square_x < 0:
# self.square_vel = -self.square_vel
def draw(self):
1) # Clear the screen with dark blue
pyxel.cls(
# 3. Draw the sprite at its new position
self.square_x, self.square_y,
pyxel.rect(self.square_size, self.square_size, 11) # Green square
# Display information
5, 5, "Basic Automatic Movement", 7)
pyxel.text(5, 15, "Square moves right and wraps around", 7)
pyxel.text(
BasicMovement()
When you run this code, you’ll see a green square steadily moving from left to right across the screen. When it reaches the right edge, it wraps around to the left side and continues its journey.
45.3 Keyboard-Controlled Movement
Of course, games usually need to respond to player input. Let’s modify our example to move the square using the keyboard arrow keys:
import pyxel
class KeyboardMovement:
def __init__(self):
160, 120, title="Keyboard Movement")
pyxel.init(
# Store position in variables
self.player_x = 80
self.player_y = 60
self.player_size = 8
self.player_speed = 2 # Pixels to move per frame
self.update, self.draw)
pyxel.run(
def update(self):
if pyxel.btnp(pyxel.KEY_Q):
pyxel.quit()
# Update position based on keyboard input
if pyxel.btn(pyxel.KEY_LEFT):
self.player_x = self.player_x - self.player_speed
if pyxel.btn(pyxel.KEY_RIGHT):
self.player_x = self.player_x + self.player_speed
if pyxel.btn(pyxel.KEY_UP):
self.player_y = self.player_y - self.player_speed
if pyxel.btn(pyxel.KEY_DOWN):
self.player_y = self.player_y + self.player_speed
def draw(self):
1) # Clear the screen with dark blue
pyxel.cls(
# Draw the player at its new position
self.player_x, self.player_y,
pyxel.rect(self.player_size, self.player_size, 11)
# Display instructions
5, 5, "Use arrow keys to move", 7)
pyxel.text(5, 15, "Press Q to quit", 7)
pyxel.text(
KeyboardMovement()
This code creates a player-controlled square that responds to the arrow keys. Notice how we:
- Added a
player_speed
variable to control how many pixels the player moves per frame - Use
pyxel.btn()
to check if arrow keys are being pressed - Update the player’s position based on which keys are pressed
45.4 Keeping Sprites Within Bounds
One common issue in games is keeping sprites from moving off-screen. Let’s modify our keyboard movement example to constrain the player to the visible area:
import pyxel
class BoundedMovement:
def __init__(self):
160, 120, title="Bounded Movement")
pyxel.init(
# Store position and size
self.player_x = 80
self.player_y = 60
self.player_size = 16
self.player_speed = 2
self.update, self.draw)
pyxel.run(
def update(self):
if pyxel.btnp(pyxel.KEY_Q):
pyxel.quit()
# Update position based on keyboard input
if pyxel.btn(pyxel.KEY_LEFT):
self.player_x = self.player_x - self.player_speed
if pyxel.btn(pyxel.KEY_RIGHT):
self.player_x = self.player_x + self.player_speed
if pyxel.btn(pyxel.KEY_UP):
self.player_y = self.player_y - self.player_speed
if pyxel.btn(pyxel.KEY_DOWN):
self.player_y = self.player_y + self.player_speed
# Keep the player within the screen bounds
# Left boundary
if self.player_x < 0:
self.player_x = 0
# Right boundary (account for player width)
if self.player_x > 160 - self.player_size:
self.player_x = 160 - self.player_size
# Top boundary
if self.player_y < 0:
self.player_y = 0
# Bottom boundary (account for player height)
if self.player_y > 120 - self.player_size:
self.player_y = 120 - self.player_size
def draw(self):
1) # Clear the screen with dark blue
pyxel.cls(
# Draw the screen boundaries
0, 0, 160, 120, 7)
pyxel.rectb(
# Draw the player
self.player_x, self.player_y,
pyxel.rect(self.player_size, self.player_size, 11)
# Display instructions
5, 5, "Use arrow keys to move", 7)
pyxel.text(5, 15, "Player stays within bounds", 7)
pyxel.text(
BoundedMovement()
This code adds boundary checking after updating the player’s position. We check all four edges of the screen and adjust the player’s position if they try to move beyond them. Notice how we account for the player’s size when checking the right and bottom boundaries.
45.5 Movement with Acceleration and Deceleration
Real-world objects don’t start and stop instantly—they accelerate and decelerate. We can simulate this in our games for more natural-feeling movement:
import pyxel
class SmoothMovement:
def __init__(self):
160, 120, title="Smooth Movement")
pyxel.init(
# Position and size
self.player_x = 80
self.player_y = 60
self.player_size = 8
# Velocity (pixels per frame)
self.velocity_x = 0
self.velocity_y = 0
# Physics constants
self.acceleration = 0.2
self.friction = 0.9 # Acts as deceleration (must be < 1.0)
self.update, self.draw)
pyxel.run(
def update(self):
if pyxel.btnp(pyxel.KEY_Q):
pyxel.quit()
# Apply acceleration based on keyboard input
if pyxel.btn(pyxel.KEY_LEFT):
self.velocity_x = self.velocity_x - self.acceleration
if pyxel.btn(pyxel.KEY_RIGHT):
self.velocity_x = self.velocity_x + self.acceleration
if pyxel.btn(pyxel.KEY_UP):
self.velocity_y = self.velocity_y - self.acceleration
if pyxel.btn(pyxel.KEY_DOWN):
self.velocity_y = self.velocity_y + self.acceleration
# Apply friction to slow down when no keys are pressed
self.velocity_x = self.velocity_x * self.friction
self.velocity_y = self.velocity_y * self.friction
# Update position based on velocity
self.player_x = self.player_x + self.velocity_x
self.player_y = self.player_y + self.velocity_y
# Keep player within bounds
if self.player_x < 0:
self.player_x = 0
self.velocity_x = 0 # Stop horizontal movement
if self.player_x > 160 - self.player_size:
self.player_x = 160 - self.player_size
self.velocity_x = 0
if self.player_y < 0:
self.player_y = 0
self.velocity_y = 0 # Stop vertical movement
if self.player_y > 120 - self.player_size:
self.player_y = 120 - self.player_size
self.velocity_y = 0
def draw(self):
1) # Clear the screen with dark blue
pyxel.cls(
# Draw the player
self.player_x, self.player_y,
pyxel.rect(self.player_size, self.player_size, 11)
# Draw velocity information
5, 5, f"Velocity X: {self.velocity_x:.2f}", 7)
pyxel.text(5, 15, f"Velocity Y: {self.velocity_y:.2f}", 7)
pyxel.text(5, 30, "Use arrow keys to move", 7)
pyxel.text(5, 40, "Notice the smooth acceleration", 7)
pyxel.text(
SmoothMovement()
This code introduces a physics-based movement system with:
- Velocity: We track how fast the player is moving in both X and Y directions
- Acceleration: We increase velocity gradually when keys are pressed
- Friction: We decrease velocity over time to simulate natural slowing down
The result is movement that feels much more natural, with the player gradually speeding up when keys are pressed and slowing down when released.
45.6 Moving Multiple Sprites: Following Patterns
Games often need to move multiple sprites at once, each with its own behavior. Let’s create a simple example with multiple moving objects:
import pyxel
class MultipleSprites:
def __init__(self):
160, 120, title="Multiple Moving Sprites")
pyxel.init(
# Player sprite
self.player_x = 80
self.player_y = 60
self.player_size = 8
self.player_speed = 2
# Enemy sprites (x, y, direction_x, direction_y)
self.enemies = [
20, 20, 1, 0.5], # Right and down
[140, 20, -1, 0.5], # Left and down
[20, 100, 1, -0.5], # Right and up
[140, 100, -1, -0.5] # Left and up
[
]self.enemy_size = 8
self.update, self.draw)
pyxel.run(
def update(self):
if pyxel.btnp(pyxel.KEY_Q):
pyxel.quit()
# Update player position
if pyxel.btn(pyxel.KEY_LEFT):
self.player_x = self.player_x - self.player_speed
if pyxel.btn(pyxel.KEY_RIGHT):
self.player_x = self.player_x + self.player_speed
if pyxel.btn(pyxel.KEY_UP):
self.player_y = self.player_y - self.player_speed
if pyxel.btn(pyxel.KEY_DOWN):
self.player_y = self.player_y + self.player_speed
# Keep player within bounds
self.player_x = max(0, min(self.player_x, 160 - self.player_size))
self.player_y = max(0, min(self.player_y, 120 - self.player_size))
# Update each enemy
for i in range(4): # We have 4 enemies
# Move enemy
self.enemies[i][0] = self.enemies[i][0] + self.enemies[i][2]
self.enemies[i][1] = self.enemies[i][1] + self.enemies[i][3]
# Bounce off walls
if self.enemies[i][0] < 0 or self.enemies[i][0] > 160 - self.enemy_size:
self.enemies[i][2] = -self.enemies[i][2] # Reverse x direction
if self.enemies[i][1] < 0 or self.enemies[i][1] > 120 - self.enemy_size:
self.enemies[i][3] = -self.enemies[i][3] # Reverse y direction
def draw(self):
1) # Clear the screen with dark blue
pyxel.cls(
# Draw the player (green)
self.player_x, self.player_y,
pyxel.rect(self.player_size, self.player_size, 11)
# Draw the enemies (red)
for i in range(4):
self.enemies[i][0], self.enemies[i][1],
pyxel.rect(self.enemy_size, self.enemy_size, 8)
# Display instructions
5, 5, "Use arrow keys to move", 7)
pyxel.text(5, 15, "Avoid the red enemies", 7)
pyxel.text(
MultipleSprites()
This example introduces multiple independently moving objects:
- A player-controlled green square
- Four red enemy squares that move in different directions
- Collision detection with the walls that causes enemies to bounce
Notice how we store each enemy’s data in a list within a list. Each enemy entry contains:
- X position
- Y position
- X direction (positive for right, negative for left)
- Y direction (positive for down, negative for up)
When an enemy hits a wall, we reverse its direction by multiplying it by -1.
45.7 Using blt()
Instead of Shapes
So far, we’ve been using rectangles for our sprites, but in a real game, you’ll typically use actual sprite images. Let’s modify our code to use the blt()
function for drawing sprites:
import pyxel
class SpriteMovement:
def __init__(self):
160, 120, title="Sprite Movement")
pyxel.init(
# For this example, we'll assume there's a character sprite at position (0,0)
# and an enemy sprite at position (8,0) in image bank 0
# Player sprite
self.player_x = 80
self.player_y = 60
self.player_speed = 2
# Enemy sprite
self.enemy_x = 40
self.enemy_y = 30
self.enemy_dir_x = 1
self.enemy_dir_y = 1
self.update, self.draw)
pyxel.run(
def update(self):
if pyxel.btnp(pyxel.KEY_Q):
pyxel.quit()
# Update player position
if pyxel.btn(pyxel.KEY_LEFT):
self.player_x = self.player_x - self.player_speed
if pyxel.btn(pyxel.KEY_RIGHT):
self.player_x = self.player_x + self.player_speed
if pyxel.btn(pyxel.KEY_UP):
self.player_y = self.player_y - self.player_speed
if pyxel.btn(pyxel.KEY_DOWN):
self.player_y = self.player_y + self.player_speed
# Keep player within bounds
self.player_x = max(0, min(self.player_x, 160 - 8))
self.player_y = max(0, min(self.player_y, 120 - 8))
# Update enemy position
self.enemy_x = self.enemy_x + self.enemy_dir_x
self.enemy_y = self.enemy_y + self.enemy_dir_y
# Bounce enemy off walls
if self.enemy_x < 0 or self.enemy_x > 160 - 8:
self.enemy_dir_x = -self.enemy_dir_x
if self.enemy_y < 0 or self.enemy_y > 120 - 8:
self.enemy_dir_y = -self.enemy_dir_y
def draw(self):
1) # Clear the screen with dark blue
pyxel.cls(
# Draw the player sprite
# Parameters: x, y, img, u, v, w, h, colkey
self.player_x, self.player_y, 0, 0, 0, 8, 8, 0)
pyxel.blt(
# Draw the enemy sprite
self.enemy_x, self.enemy_y, 0, 8, 0, 8, 8, 0)
pyxel.blt(
# Display instructions
5, 5, "Use arrow keys to move", 7)
pyxel.text(
SpriteMovement()
This code uses the blt()
function to draw the player and enemy sprites from the image bank instead of using rectangles. We assume there are 8x8 sprites at positions (0,0) and (8,0) in image bank 0.
45.8 Creating a Simple Game: Collect the Coins
Let’s put everything together to create a simple game where the player moves around collecting coins:
import pyxel
class CoinCollectorGame:
def __init__(self):
160, 120, title="Coin Collector")
pyxel.init(
# Player properties
self.player_x = 80
self.player_y = 60
self.player_speed = 2
# Coins - list of [x, y, active]
self.coins = [
20, 20, True],
[60, 30, True],
[100, 40, True],
[140, 50, True],
[30, 80, True],
[70, 90, True],
[110, 100, True],
[
]
# Game state
self.score = 0
self.update, self.draw)
pyxel.run(
def update(self):
if pyxel.btnp(pyxel.KEY_Q):
pyxel.quit()
# Update player position
if pyxel.btn(pyxel.KEY_LEFT):
self.player_x = self.player_x - self.player_speed
if pyxel.btn(pyxel.KEY_RIGHT):
self.player_x = self.player_x + self.player_speed
if pyxel.btn(pyxel.KEY_UP):
self.player_y = self.player_y - self.player_speed
if pyxel.btn(pyxel.KEY_DOWN):
self.player_y = self.player_y + self.player_speed
# Keep player within bounds
self.player_x = max(0, min(self.player_x, 160 - 8))
self.player_y = max(0, min(self.player_y, 120 - 8))
# Check for coin collection
for i in range(len(self.coins)):
if self.coins[i][2]: # If coin is active
# Check for collision (simple rectangle collision)
if (self.player_x < self.coins[i][0] + 8 and
self.player_x + 8 > self.coins[i][0] and
self.player_y < self.coins[i][1] + 8 and
self.player_y + 8 > self.coins[i][1]):
# Collect coin
self.coins[i][2] = False
self.score += 10
def draw(self):
1) # Clear the screen with dark blue
pyxel.cls(
# Draw the player (green rectangle for simplicity)
self.player_x, self.player_y, 8, 8, 11)
pyxel.rect(
# Draw active coins (yellow circles)
for coin_x, coin_y, active in self.coins:
if active:
+ 4, coin_y + 4, 4, 10) # +4 for center offset
pyxel.circ(coin_x
# Draw score
5, 5, f"SCORE: {self.score}", 7)
pyxel.text(
# Display instructions
5, 110, "Use arrow keys to collect coins", 7)
pyxel.text(
CoinCollectorGame()
This simple game demonstrates:
- Player movement with keyboard controls
- Static objects (coins) placed around the screen
- Collision detection to collect coins
- Score tracking
- Game state management (active/inactive coins)
45.9 Practice Time: Your Movement Quest
Now it’s your turn to create a moving sprite application. Try these challenges:
Create a game with a player-controlled sprite that moves with the arrow keys
Add at least one independently moving enemy sprite that follows a pattern
Implement boundary checking to keep sprites on screen
Here’s a starting point for your quest:
import pyxel
class MyMovementGame:
def __init__(self):
160, 120, title="My Movement Game")
pyxel.init(
# Set up your player variables
self.player_x = 80
self.player_y = 60
self.player_speed = 2
# Set up your enemy variables
# Your code here
self.update, self.draw)
pyxel.run(
def update(self):
if pyxel.btnp(pyxel.KEY_Q):
pyxel.quit()
# Update player position based on keyboard input
# Your code here
# Update enemy position
# Your code here
# Keep sprites within screen boundaries
# Your code here
def draw(self):
1) # Clear the screen with dark blue
pyxel.cls(
# Draw your player sprite
# Your code here
# Draw your enemy sprite
# Your code here
# Draw instructions
5, 5, "Use arrow keys to move", 7)
pyxel.text(
# Create and start your game
MyMovementGame()
45.10 Common Bugs to Watch Out For
As you experiment with sprite movement, watch out for these common issues:
Forgetting Boundaries: Without boundary checks, sprites easily move off-screen and become uncontrollable. Always implement boundary checks.
Jerky Movement: If movement feels jerky, check that you’re using
btn()
(continuous) for movement rather thanbtnp()
(single press).Inconsistent Speed: Movement speed can vary based on frame rate. For consistent speed, consider frame-rate independent movement (we’ll cover this in a future lesson).
Z-Order Issues: Sprites drawn later appear on top. If your player disappears behind other elements, check your drawing order.
Off-by-One Errors: When calculating boundaries, remember to account for sprite width and height. The right boundary isn’t at x=160, but at x=160-sprite_width.
Direction Confusion: Remember that in Pyxel’s coordinate system, increasing Y moves down and increasing X moves right. Mixing these up leads to reversed controls.
Collision Detection Timing: Check for collisions after updating positions, not before, or you’ll detect collisions with the previous frame’s positions.
45.11 Conclusion and Resources for Further Exploration
You’ve now learned the fundamentals of sprite movement in Pyxel. These techniques form the foundation of virtually every 2D game, from simple arcade games to complex platformers.
To further enhance your movement programming skills, check out these resources:
Game Programming Patterns - A free online book with excellent chapters on game loops and update methods.
2D Game Movement Fundamentals - A deeper look at 2D movement techniques.
The Nature of Code - A fantastic resource for understanding physics-based movement.
Game Feel: A Game Designer’s Guide to Virtual Sensation - A book on making movement feel good.
In our next lesson, we’ll explore more advanced movement techniques, including path following, chasing behaviors, and platformer physics. Keep experimenting with the basics – mastering these fundamentals will prepare you for more complex movement systems in the future!