⇤ home

i love pyglet

introduction

i had a challenge.

i wanted to make an animated sun bounce around my screen.

(and by the end of this article, you will know how to make yours too!)

something like this:

with some added constraints:

why? well i do youtube videos, twitch streaming, and you know how the kids are, easily distracted unless you have a steady supply of dopamine.

did it work? hell yeah, look at that!

now, to make this happen, i would need a piece of software that could:

we usually call these things “game engines”. the name is fairly transparent.

i have some experience with pygame (and watch da fluffy potato’s videos), but before diving in i made a quick google search and found pyglet.

it seemed less cumbersome than pygame, and after dev’ing with it i can say it is.

mostly because:

i could go on, but code is worth a thousand word.

let’s go!

how to make funny suns

the trick here is to use a blue screen (just like in marvel movies).

you draw your stuff on a blue background, capture the window with obs, then use the “alpha key” feature of obs to remove the blue.

it’s kinda heavy on the processor. there’s a better way to do it, maybe?

anyway, let’s go and draw a background. the way we connoisseurs of the art do it is we draw a big fat rectangle on the screen:

import pyglet


WINDOW_W = 1920
WINDOW_H = 1080


window = pyglet.window.Window(WINDOW_W, WINDOW_H, fullscreen=True)

background = pyglet.shapes.Rectangle(0, 0, WINDOW_W, WINDOW_H, color=(0, 0, 255))

@window.event
def on_draw():
    window.clear()
    background.draw()

if __name__ == "__main__":
    pyglet.app.run()

i just love that i don’t have to explain anything. pyglet “hello world” is very self-explanatory, especially if you’re familiar with decorators and event systems (for example if you did some webdev).

if it doesn’t click, just stare at it a bit longer, i promise it will.

“rectangle” is not the only cool helper you get, you have all them shapes + text too:


count_label = pyglet.text.Label(
    "louis sunshine",
    font_name="Arial",
    font_size=16,
    x=10,
    y=WINDOW_H - 34,
    anchor_x="left",
    anchor_y="top",
    color=(255, 255, 255, 255),
)

# later...

@window.event
def on_draw():
    window.clear()
    background.draw()
    count_label.draw()

notice how anchor (the “reference” point of the element) can be nicely set up with shorthands.

okay, that’s nice and jazzy, but what about the suns?

well, we’re going to use this spritesheet:

sun

like this:

# this tells pyglet that folder "resources" is where we dump our assets
pyglet.resource.path = ["resources"]
pyglet.resource.reindex()

# we load the image and indicate the grid size
sun_image = pyglet.resource.image("sun2.png")
sun_grid  = pyglet.image.ImageGrid(sun_image, 1, 6)

# center the anchor
for frame in sun_grid:
    frame.anchor_x = frame.width  // 2
    frame.anchor_y = frame.height // 2

# turn the frames into a looping animation (0.1 s per frame)
sun_animation = pyglet.image.animation.Animation.from_image_sequence(
    sun_grid, duration=0.1, loop=True
)

# there you have it
# and you'll use the animation like this:
sprite = pyglet.sprite.Sprite(sun_animation, batch=batch)

hold on, what is that “batch” thing?

remember how we drew the background first, then the text? this was for the background not to be over the text.

batch mostly means “these things, i don’t care if they overlap, draw them in one go” and it speeds the rendering by quite a lot.

now let’s make the suns move around!

# let's pretend we have a function update_sun
# which takes dt (amount of time passed) as an argument
def update_sun(sun, dt):
    ...
    # here, we compute the new position of the sun
    # based on its initial position, velocity
    # and, importantly, dt!

# we then only have to attach this function to a clock
# so the function is called every dt
pyglet.clock.schedule_interval(update, 1 / 60.0)

the last thing we need to do is to handle inputs.


@window.event
def on_key_press(symbol, modifiers):
    if symbol == pyglet.window.key.SPACE:
        spawn_sun()
    elif symbol == pyglet.window.key.BACKSPACE:
        remove_last_sun()

once again, very straightforward with pyglet’s good defaults.


okay, thanks for sticking with me for so long.

this was not exactly a tutorial, what i wanted was to show you the small building blocks.

these small “lego” blocks are all you need. now it’s time to put them together, i will show you the big structure. don’t be scared by the collision code (it’s bad anyway)

here is the full code:

import math
import random
import time

import pyglet


# SETTINGS 

pyglet.resource.path = ["resources"]
pyglet.resource.reindex()

WINDOW_W = 1920
WINDOW_H = 1080

# in pixels per second
SUN_SPEED = 150          
# seconds between automatic spawns
AUTO_SPAWN_SUN = 0.01   

# streamer box
BOX_W = 460
BOX_H = 340
BOX_X = WINDOW_W - BOX_W
BOX_Y = 0


# WINDOW 

window = pyglet.window.Window(WINDOW_W, WINDOW_H, fullscreen=True)

# SHAPES

background = pyglet.shapes.Rectangle(0, 0, WINDOW_W, WINDOW_H, color=(0, 0, 255))
red_box = pyglet.shapes.Rectangle(BOX_X, BOX_Y, BOX_W, BOX_H, color=(0, 0, 255))

# TEXT 

fps_display = pyglet.window.FPSDisplay(window)
count_label = pyglet.text.Label(
    "",
    font_name="Arial",
    font_size=16,
    x=10,
    y=WINDOW_H - 34,
    anchor_x="left",
    anchor_y="top",
    color=(255, 255, 255, 255),
)


#  SUN

sun_image = pyglet.resource.image("sun2.png")
sun_grid  = pyglet.image.ImageGrid(sun_image, 1, 6)

for frame in sun_grid:
    frame.anchor_x = frame.width  // 2
    frame.anchor_y = frame.height // 2

sun_animation = pyglet.image.animation.Animation.from_image_sequence(
    sun_grid, duration=0.1, loop=True
)


#  SUNS 

suns = []  # list of sun dicts

batch = pyglet.graphics.Batch()

def make_sun():
    x = random.uniform(100, WINDOW_W - BOX_W - 100)
    y = random.uniform(100, WINDOW_H - BOX_H - 100)

    sprite = pyglet.sprite.Sprite(sun_animation, x=x, y=y, batch=batch)

    angle = random.uniform(0, 2 * math.pi)
    vx = math.cos(angle) * SUN_SPEED
    vy = math.sin(angle) * SUN_SPEED
    phase = random.randint(1,234) # wobble offset

    return {"sprite": sprite, "vx": vx, "vy": vy, "phase": phase}


# we'll call it on a clock so dt has to be provided
# we attribute it to none to please the linter
def spawn_sun(dt=None):
    suns.append(make_sun())


def remove_last_sun():
    if suns:
        suns[-1]["sprite"].delete()  # free the sprite from video memory
        suns.pop()


#  UPDATE 

def update_sun(sun, dt):
    sprite = sun["sprite"]

    nx = sprite.x + sun["vx"] * dt
    ny = sprite.y + sun["vy"] * dt

    # Start of terrible collision code
    if nx < 0:
        sun["vx"] = abs(sun["vx"])      # push right
    elif nx > WINDOW_W:
        sun["vx"] = -abs(sun["vx"])     # push left

    if ny < 0:
        sun["vy"] = abs(sun["vy"])      # push up
    elif ny > WINDOW_H:
        sun["vy"] = -abs(sun["vy"])     # push down

    hits_box = (
        nx > BOX_X           and
        nx < BOX_X + BOX_W   and
        ny > BOX_Y           and
        ny < BOX_Y + BOX_H
    )
    if hits_box:
        if BOX_Y < sun["sprite"].y < BOX_Y + BOX_H:
            sun["vx"] = -sun["vx"]
        else:
            sun["vy"] = -sun["vy"]
    # End of terrible collision code

    sprite.x = nx
    sprite.y = ny

    # wobble effect oscillates between 0.8 and 1.2
    # with added random phase offset
    sprite.scale = 1 + 0.2 * math.cos(time.time() + sun["phase"])


def update(dt):
    for sun in suns:
        update_sun(sun, dt)
    count_label.text = f"Suns: {len(suns)}"


#  CLOCKS 

pyglet.clock.schedule_interval(update, 1 / 60.0)

# auto-spawn a sun every now and then
pyglet.clock.schedule_interval(spawn_sun, AUTO_SPAWN_SUN)

#  INPUT

@window.event
def on_key_press(symbol, modifiers):
    if symbol == pyglet.window.key.SPACE:
        spawn_sun()
    elif symbol == pyglet.window.key.BACKSPACE:
        remove_last_sun()

#  DRAW 

@window.event
def on_draw():
    window.clear()
    background.draw()
    red_box.draw()
    batch.draw()       # draws all sun sprites at once
    fps_display.draw()
    count_label.draw()


#  RUN

if __name__ == "__main__":
    pyglet.app.run()
    

hold on a second

okay, you like pyglet but why should i use it?

especially when i could use a real(tm) video game editor like unity or stuff like that?

good spot. it’s because i think there is a secret superpower to using pyglet… but that’s for a next post ;)

stay tuned in!