Here is a new chapter in the “Learn Pyxel the Soft Way” book. Pyxel is a retrogaming framework built with Rust for the smoothness of the games, exposing a Python API for simplicity and expressivity.
This demo is a bit more complex than the previous one : keyboard and flying saucer. Don’t worry; the learning curve is still flat.
Books for learning Python
- No Starch Press’ Python for Kids, second edition
- O’Reilly’s Learning Python, sixth edition
You already found a great book for learning Python, didn’t you?
And you already start to read it ?
Please, RTFB (Read The Python Book): you can’t learn development from Youtube videos or copy-pasting tutorials.
Demo
This time, with the mouse, the player will tell a cute bat where to fly in its dark cave.
Assets
The assets are two sprites of the bat, two square images of 16x16px. The first one has wings up, the second one has wings down.
You can also download the bat.pyxres file.
Code
For clarity, the bat has its own class : Bat
.
The purpose of a class is to wrap all its needs.
A bat flapping its wings
The frame
attibute points to the current sprite : 0 for wings up, 1 for wings down.
The swap_wings
methode switches from one state to the other : up, down, up …
In the draw
method, the wings are swapped every 10 frames, then drawn on the screen.
One flap every 10 seconds is slow, but it’s a bat, not a hummingbird.
The App
class is bare-bones: setting the game, starting the main loop.
The only difference with the flying saucer App
is instantiating the Bat
class and asking it to draw itself.
import pyxel
class Bat:
def __init__(self, x, y):
self.x = x
self.y = y
self.images = [ # The bat uses two images : wings up, wings down
(0, 0, 16, 16, 0), # x, y, width, height, transparent color
(16, 0, 16, 16, 0),
]
self.frame = 0 # 0 : bat wings are up, 1 : down
def swap_wings(self):
"The wings switch from up to down."
if self.frame == 0:
self.frame = 1
else:
self.frame = 0
def draw(self):
"Draw the sprite"
if pyxel.frame_count % 10 == 0: # Every 10 frames, the wings flip
self.swap_wings()
pyxel.blt(self.x, self.y, 0, *(self.images[self.frame]))
class App:
def __init__(self):
pyxel.init(160, 120, title="Flying bat") # width, height, title
pyxel.load("bat.pyxres") # Load the assets.
self.bat = Bat(72, 72) # Spawn a new bat 🦇
pyxel.run(self.update, self.draw) # Starts Pyxel loop
def update(self):
if pyxel.btnp(pyxel.KEY_Q): # Hit Q to quit
pyxel.quit()
def draw(self):
pyxel.cls(0) # Clear screen
self.bat.draw() # Draw the bat at its current position
App()
The game just drew a flapping bat in the middle of the screen. You can do nothing, just quit when hitting the Q key. Yes, it’s frustrating.
A bat following the mouse when you click
Show the cursor with pyxel.mouse(True)
.
A left click (pyxel.MOUSE_BUTTON_LEFT
) moves the bat to the cursor position.
If the left click is maintained, the bat follows the movements of the mouse. You can shake the bat (but it’s not nice to do that).
import pyxel
class Bat:
def __init__(self, x, y):
self.x = x
self.y = y
self.images = [ # The bat uses two images : wings up, wings down
(0, 0, 16, 16, 0), # x, y, width, height, transparent color
(16, 0, 16, 16, 0),
]
self.frame = 0 # 0 : bat wings are up, 1 : down
def swap_wings(self):
"The wings switch from up to down."
if self.frame == 0:
self.frame = 1
else:
self.frame = 0
def draw(self):
"Draw the sprite."
if pyxel.frame_count % 10 == 0: # Every 10 frames, the wings flip
self.swap_wings()
pyxel.blt(self.x, self.y, 0, *(self.images[self.frame]))
class App:
def __init__(self):
pyxel.init(160, 120, title="Flying bat") # width, height, title
pyxel.load("bat.pyxres") # Load the assets.
self.bat = Bat(72, 72) # Spawn a new bat 🦇
pyxel.mouse(True) # Show the mouse
pyxel.run(self.update, self.draw) # Starts Pyxel loop
def update(self):
if pyxel.btnp(pyxel.KEY_Q): # Hit Q to quit
pyxel.quit()
if pyxel.btn(pyxel.MOUSE_BUTTON_LEFT):
self.bat.x = pyxel.mouse_x
self.bat.y = pyxel.mouse_y
pyxel.mouse(False)
else:
pyxel.mouse(True)
def draw(self):
pyxel.cls(0) # Clear screen
self.bat.draw() # Draw the bat at its current position
App()
Yes! In this step you can do something. Not so much, but something.
A bat flying to the click position
Let’s remove the code that links the bat to the mouse.
In this ambitious step, some math (8th grade) will be used to compute the path:
- The
distance
function computes the distance between two points with a little help from our friend Euclid. - The
angle
function computes the angle from one point to the other (it’s trigonometry).
Technically, we switch from Cartesian to Polar coordinates.
The vector between the actual position and the clicked target is computed.
Every frame, the bat moves one step (its speed) following the vector. If the distance between the bat and the target is lower than the step, the bat stops. Here, vectors and steps use real numbers (ℝ aka float), and the screen uses natural numbers (ℕ aka integer), and there are rounding troubles between them.
Pyxel uses the top left corner of a sprite as its zero. Without correction, the top left corner of the bat will stop on the clicked target. It’s counterintuitive and ugly.
The vector must use the center of the sprite: x + width / 2
and y + height / 2
.
import pyxel
class Bat:
def __init__(self, x, y):
self.x = x
self.y = y
self.images = [ # The bat uses two images : wings up, wings down
(0, 0, 16, 16, 0), # x, y, width, height, transparent color
(16, 0, 16, 16, 0),
]
# self.x is the top left corner of the image,
# computing the position of the center of the bat is more intuitive
self.center_x = self.images[0][2] / 2
self.center_y = self.images[0][3] / 2
self.frame = 0 # 0 : bat wings are up, 1 : down
self.angle = 0 # Where does the bat look ?
self.target_x: int # The bat goes to this target.
self.target_y: int
self.speed = 0
def move_to(self, x, y, speed):
"The bat rotates and will move to the target."
self.target_x = x
self.target_y = y
self.speed = speed
self.rotate_to(x, y)
def rotate_to(self, x, y):
self.angle = angle(x, y, self.x + self.center_x, self.y + self.center_y)
def one_step(self):
"The bat moves to its target."
if self.speed != 0:
delta = distance(
self.target_x,
self.target_y,
self.x + self.center_x,
self.y + self.center_y,
)
if delta <= self.speed:
self.speed = 0
else:
self.x += pyxel.sin(self.angle) * self.speed
self.y += pyxel.cos(self.angle) * self.speed
def swap_wings(self):
"The wings switch from up to down."
if self.frame == 0:
self.frame = 1
else:
self.frame = 0
def draw(self):
"Draw the sprite."
if pyxel.frame_count % 10 == 0: # Every 10 frames, the wings swap
self.swap_wings()
pyxel.blt(self.x, self.y, 0, *(self.images[self.frame]))
def distance(x1, y1, x2, y2: float) -> float:
"Euclidean distance"
dx = x1 - x2
dy = y1 - y2
return pyxel.sqrt(dx**2 + dy**2)
def angle(x1, y1, x2, y2: int) -> float:
"Get the angle between two points with trigonometry."
dx = x1 - x2
dy = y1 - y2
return pyxel.atan2(dx, dy)
class App:
def __init__(self):
pyxel.init(160, 120, title="Flying bat") # width, height, title
pyxel.load("bat.pyxres") # Load the assets.
self.bat = Bat(72, 72) # Spawn a new bat 🦇
pyxel.mouse(True) # Show the mouse
pyxel.run(self.update, self.draw) # Starts Pyxel loop
def update(self):
if pyxel.btnp(pyxel.KEY_Q): # Hit Q to quit
pyxel.quit()
if pyxel.btn(pyxel.MOUSE_BUTTON_LEFT): # Mouse click set the target
self.bat.move_to(pyxel.mouse_x, pyxel.mouse_y, 2) # x, y, speed
self.bat.one_step() # The bat flies, one step at a time
def draw(self):
pyxel.cls(0) # Clear screen
self.bat.draw() # Draw the bat at its current position
App()
The path is computed for each click; you can force the bat to swing chaotically if you click frenetically on the screen.
In genuine 8-bit games, sprites are squares. If you need to compute collisions, or if one sprite overlaps the other, don’t bother with Euclid. Just check if the pointer is lower than the top of the bat, higher than the bottom, further left than the right side, and further right than the left side.
You can modify the code for replacing the Euclid + Trigonometry part with the two squares dance as a tribute to the Game Boy with its tiny Sharp Z80 processor, clocked at 4.194304 MHz, and its 8ko of RAM.
With the current computers, even a Raspberry Pi, nobody will notice the performance difference.
The sources of Bat are hosted on Github.
Beyond
Own the code, change it, break it. Add missing behaviors.
A little bit of randomness in its flight to make it more shaky?
One more sprite for the standby position, upside down, wings folded ?
Some “flap flap” sounds when the bat flies?