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:

  1. Player Agency: Movement lets players interact with and affect the game world
  2. Challenge: Moving obstacles and enemies create gameplay challenges
  3. Visual Interest: Even simple movement patterns make a scene more engaging
  4. 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:

  1. Store the sprite’s position in variables
  2. Update those variables based on inputs or logic
  3. 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):
        pyxel.init(160, 120, title="Basic Movement")
        
        # 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
        
        pyxel.run(self.update, self.draw)
    
    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):
        pyxel.cls(1)  # Clear the screen with dark blue
        
        # 3. Draw the sprite at its new position
        pyxel.rect(self.square_x, self.square_y, 
                 self.square_size, self.square_size, 11)  # Green square
        
        # Display information
        pyxel.text(5, 5, "Basic Automatic Movement", 7)
        pyxel.text(5, 15, "Square moves right and wraps around", 7)

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):
        pyxel.init(160, 120, title="Keyboard Movement")
        
        # 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
        
        pyxel.run(self.update, self.draw)
    
    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):
        pyxel.cls(1)  # Clear the screen with dark blue
        
        # Draw the player at its new position
        pyxel.rect(self.player_x, self.player_y, 
                 self.player_size, self.player_size, 11)
        
        # Display instructions
        pyxel.text(5, 5, "Use arrow keys to move", 7)
        pyxel.text(5, 15, "Press Q to quit", 7)

KeyboardMovement()

This code creates a player-controlled square that responds to the arrow keys. Notice how we:

  1. Added a player_speed variable to control how many pixels the player moves per frame
  2. Use pyxel.btn() to check if arrow keys are being pressed
  3. 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):
        pyxel.init(160, 120, title="Bounded Movement")
        
        # Store position and size
        self.player_x = 80
        self.player_y = 60
        self.player_size = 16
        self.player_speed = 2
        
        pyxel.run(self.update, self.draw)
    
    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):
        pyxel.cls(1)  # Clear the screen with dark blue
        
        # Draw the screen boundaries
        pyxel.rectb(0, 0, 160, 120, 7)
        
        # Draw the player
        pyxel.rect(self.player_x, self.player_y, 
                 self.player_size, self.player_size, 11)
        
        # Display instructions
        pyxel.text(5, 5, "Use arrow keys to move", 7)
        pyxel.text(5, 15, "Player stays within bounds", 7)

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):
        pyxel.init(160, 120, title="Smooth Movement")
        
        # 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)
        
        pyxel.run(self.update, self.draw)
    
    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):
        pyxel.cls(1)  # Clear the screen with dark blue
        
        # Draw the player
        pyxel.rect(self.player_x, self.player_y, 
                 self.player_size, self.player_size, 11)
        
        # Draw velocity information
        pyxel.text(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)

SmoothMovement()

This code introduces a physics-based movement system with:

  1. Velocity: We track how fast the player is moving in both X and Y directions
  2. Acceleration: We increase velocity gradually when keys are pressed
  3. 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):
        pyxel.init(160, 120, title="Multiple Moving Sprites")
        
        # 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
        
        pyxel.run(self.update, self.draw)
    
    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):
        pyxel.cls(1)  # Clear the screen with dark blue
        
        # Draw the player (green)
        pyxel.rect(self.player_x, self.player_y, 
                 self.player_size, self.player_size, 11)
        
        # Draw the enemies (red)
        for i in range(4):
            pyxel.rect(self.enemies[i][0], self.enemies[i][1],
                     self.enemy_size, self.enemy_size, 8)
        
        # Display instructions
        pyxel.text(5, 5, "Use arrow keys to move", 7)
        pyxel.text(5, 15, "Avoid the red enemies", 7)

MultipleSprites()

This example introduces multiple independently moving objects:

  1. A player-controlled green square
  2. Four red enemy squares that move in different directions
  3. 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):
        pyxel.init(160, 120, title="Sprite Movement")
        
        # 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
        
        pyxel.run(self.update, self.draw)
    
    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):
        pyxel.cls(1)  # Clear the screen with dark blue
        
        # Draw the player sprite
        # Parameters: x, y, img, u, v, w, h, colkey
        pyxel.blt(self.player_x, self.player_y, 0, 0, 0, 8, 8, 0)
        
        # Draw the enemy sprite
        pyxel.blt(self.enemy_x, self.enemy_y, 0, 8, 0, 8, 8, 0)
        
        # Display instructions
        pyxel.text(5, 5, "Use arrow keys to move", 7)

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):
        pyxel.init(160, 120, title="Coin Collector")
        
        # 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
        
        pyxel.run(self.update, self.draw)
    
    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):
        pyxel.cls(1)  # Clear the screen with dark blue
        
        # Draw the player (green rectangle for simplicity)
        pyxel.rect(self.player_x, self.player_y, 8, 8, 11)
        
        # Draw active coins (yellow circles)
        for coin_x, coin_y, active in self.coins:
            if active:
                pyxel.circ(coin_x + 4, coin_y + 4, 4, 10)  # +4 for center offset
        
        # Draw score
        pyxel.text(5, 5, f"SCORE: {self.score}", 7)
        
        # Display instructions
        pyxel.text(5, 110, "Use arrow keys to collect coins", 7)

CoinCollectorGame()

This simple game demonstrates:

  1. Player movement with keyboard controls
  2. Static objects (coins) placed around the screen
  3. Collision detection to collect coins
  4. Score tracking
  5. 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:

  1. Create a game with a player-controlled sprite that moves with the arrow keys

  2. Add at least one independently moving enemy sprite that follows a pattern

  3. Implement boundary checking to keep sprites on screen

Here’s a starting point for your quest:

import pyxel

class MyMovementGame:
    def __init__(self):
        pyxel.init(160, 120, title="My Movement Game")
        
        # 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
        
        pyxel.run(self.update, self.draw)
    
    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):
        pyxel.cls(1)  # Clear the screen with dark blue
        
        # Draw your player sprite
        # Your code here
        
        # Draw your enemy sprite
        # Your code here
        
        # Draw instructions
        pyxel.text(5, 5, "Use arrow keys to move", 7)

# 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:

  1. Forgetting Boundaries: Without boundary checks, sprites easily move off-screen and become uncontrollable. Always implement boundary checks.

  2. Jerky Movement: If movement feels jerky, check that you’re using btn() (continuous) for movement rather than btnp() (single press).

  3. 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).

  4. Z-Order Issues: Sprites drawn later appear on top. If your player disappears behind other elements, check your drawing order.

  5. 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.

  6. 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.

  7. 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:

  1. Game Programming Patterns - A free online book with excellent chapters on game loops and update methods.

  2. 2D Game Movement Fundamentals - A deeper look at 2D movement techniques.

  3. The Nature of Code - A fantastic resource for understanding physics-based movement.

  4. 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!