pyglet, second steps…

With my first post on pyglet I wanted to figure out how to make simple primitive shapes. For this post, I wanted to understand the basics of how pyglet’s sprites work, learn their strengths and shorcomings.  In a nutshell I learned that:

  • It’s very easy to center their pivot, translate, rotate, and scale them.  Easier than PyGame.
  • Their drawing in OpenGL can be optimized via batches of vert lists.
  • Sprite have a draw() method(), but you don’t access it when using batches.
  • Sprites don’t have an update() method, so you need to roll your own.
  • I already knew this, but worth bringing up:  Unless I’m missing it, they have no concept of a rect (rectangle) representation, no build in collision of any type.
  • How pyglet sets up resource directories (easier than the docs make it once you figure it out).

Armed with that knowledge, I came up with the below example:  A simple window framework that will create randomly moving\scaling sprites when you click in the window.  They’ll bounce off the walls accurately based on a custom rect solution that can track the rotations to the rects.  There may be more optimized ways of computing \ drawing them, but as a first pass I’m pleased.

"""
sprite02_forBlog.py
Eric Pavey - www.akeric.com - 2011-04-03
Released under the Apache Licence, v2.0
http://www.apache.org/licenses/LICENSE-2.0
"""

import os
import sys
import math
import random
import pyglet

FPS = 60
pyglet.resource.path = ['resource/sprites']
pyglet.resource.reindex()
# The name of the sprite we're going to load:
IMAGE = 'boxOrange01.png'

def getSmoothConfig():
    """
    Sets up a configuration that allows of smoothing\antialiasing of the window.
    The return of this is passed to the config parameter of the created window.
    """
    try:
        # Try and create a window config with multisampling (antialiasing)
        config = pyglet.gl.Config(sample_buffers=1, samples=4,
                        depth_size=16, double_buffer=True)
    except pyglet.window.NoSuchConfigException:
        print "Smooth contex could not be aquiried."
        config = None
    return config

class Sprite(pyglet.sprite.Sprite):
    """
    Let's create a pyglet sprite, that will randomly move around the screen
    bouncing off the walls, accurately tracking it's collision rect even wheb
    rotated.
    """
    # Load the image and center the pivot:
    image = pyglet.resource.image(IMAGE) # pyglet.image.Texture
    image.anchor_x = image.width/2
    image.anchor_y = image.height/2

    def __init__(self, window, x, y, scale=1, batch=None):
        """
        window : pyglet.window.Window : The enclosing window that this sprite
            will be draw in.
        x, y, : float : init position
        scale : float : init scale
        batch : pyglet.graphics.Batch :  Default None.  the Batch to add the
            sprite to.
        """
        super(Sprite, self).__init__(Sprite.image, x, y, batch=batch)
        self.window = window
        self.scale = scale
        self.px = x
        self.py = y
        # Random starting speed\direction deltas:
        self.dx = (random.random() - 0.5) * 1000
        self.dy = (random.random() - 0.5) * 1000
        # how much to change the scale each frame
        self.scaleVal = .01

    def update(self, dt):

        # Cycle our scaling:
        if self.scale > 1.5 or self.scale < .5:
            self.scaleVal *= -1
        self.scale += self.scaleVal

        # Get our rotated rect, and then sort our x & y positions for wall
        # collision below:
        rect = self.getRect()
        xs = sorted(xy[0] for xy in rect)
        ys = sorted(xy[1] for xy in rect)

        # Do wall collision.  If a wall is hit, reverse direction, and offset
        # away from the wall based on the distance by which the wall was passed:
        if xs[0] <= 0:
            self.dx *= -1
            self.x += -xs[0]
        elif xs[-1] >= self.window.width:
            self.dx *= -1
            self.x -= xs[-1]-self.window.width

        if ys[0] <= 0:
            self.dy *= -1
            self.y += -ys[0]
        elif ys[-1] >= self.window.height:
            self.dy *= -1
            self.y -= ys[-1]-self.window.height

        self.px = self.x
        self.py = self.y
        self.x += self.dx * dt
        self.y += self.dy * dt

        # Using this, "forward" of the sprite is the "up" direction of the texture.
        self.radians = math.atan2((self.x-self.px), (self.y-self.py))
        self.rotation = math.degrees(self.radians)

    def getRect(self):
        """
        Returns the four scaled\rotated rect points in clockwise order :
        lt, rt, rb, lb
        """
        left = self.x - self.width/2
        right = self.x + self.width/2
        top = self.y + self.height/2
        bottom = self.y - self.height/2

        lt = (left,top)
        rt = (right,top)
        lb = (left,bottom)
        rb = (right,bottom)

        # Get rotated positions:
        if  self.rotation:
            # Note, as seen below, each of the y's in the first column to the left
            # are subtracted, rather than added like their 'x' counterpart.  I'm
            # not sure why this is needed, but it's very bad if you don't.
            ltx = self.x + ((lt[0]-self.x)*math.cos(self.radians) - \
                            (lt[1]-self.y)*math.sin(self.radians))
            lty = self.y - ((lt[0]-self.x)*math.sin(self.radians) + \
                            (lt[1]-self.y)*math.cos(self.radians))
            lt = (ltx, lty)

            rtx = self.x + ((rt[0]-self.x)*math.cos(self.radians) - \
                            (rt[1]-self.y)*math.sin(self.radians))
            rty = self.y - ((rt[0]-self.x)*math.sin(self.radians) + \
                            (rt[1]-self.y)*math.cos(self.radians))
            rt = (rtx, rty)

            rbx = self.x + ((rb[0]-self.x)*math.cos(self.radians) - \
                            (rb[1]-self.y)*math.sin(self.radians))
            rby = self.y - ((rb[0]-self.x)*math.sin(self.radians) + \
                            (rb[1]-self.y)*math.cos(self.radians))
            rb = (rbx, rby)

            lbx = self.x + ((lb[0]-self.x)*math.cos(self.radians) - \
                            (lb[1]-self.y)*math.sin(self.radians))
            lby = self.y - ((lb[0]-self.x)*math.sin(self.radians) + \
                            (lb[1]-self.y)*math.cos(self.radians))
            lb = (lbx, lby)

        return lt, rt, rb, lb

class SpriteWindow(pyglet.window.Window):

    def __init__(self):
        super(SpriteWindow, self).__init__(fullscreen=False,
                                           caption='pyglet sprite test',
                                           config=getSmoothConfig())

        # Schedule the update of this window, so it will advance in time.  If we
        # don't, the window will only update on events like mouse motion.
        pyglet.clock.schedule_interval(self.update, 1.0/FPS)

        # Set the background color:
        pyglet.gl.glClearColor(0,0,1,0)

        # Used for optimized sprite *drawing*.  It holds vertex lists, not Sprite objects.
        self.sprite_batch = pyglet.graphics.Batch()
        # Used for sprite *updating*, holds our Sprite objects.
        self.sprites = []

        # A label to draw how many sprites we have:
        self.spriteLabel = pyglet.text.Label(str(len(self.sprites)), font_name='Courier',
                                  font_size=36, x=self.width/2, y=32)

        # Setup debug framerate display:
        self.fps_display = pyglet.clock.ClockDisplay()

        # Run the application
        pyglet.app.run()

    #----------------------------
    # Scheduled Events:
    # via pyglet.clock.schedule_interval in __init__

    def update(self, dt):
        """
        Do all upating here:
        """
        for sprite in self.sprites:
            sprite.update(dt)

        self.spriteLabel.text=str(len(self.sprites))

    #----------------------------
    # Window() events:
    # Overridden Window() methods:

    def on_draw(self):
        """
        Do all drawing here.
        """
        self.clear()

        # Draw all our sprites:
        self.sprite_batch.draw()

        # Draw text:
        self.fps_display.draw()
        self.spriteLabel.draw()

    def on_mouse_press(self, x, y, button, modifiers):
        """
        Interaction with mouse.
        LMB creates sprite, RMB deletes sprite.
        """
        if button == 1:
            # Create,... a SPRITE, added to our render batch:
            sprite = Sprite(self, x, y, batch=self.sprite_batch)
            self.sprites.append(sprite)
        else:
            if len(self.sprites):
                # Make sure it's deleted:
                self.sprites[-1].delete()
                self.sprites.pop()

if __name__ == '__main__':
    """
    Launch the app from an icon.
    """
    sys.exit(SpriteWindow())
pyglet: First steps
New Processing sketch: adventureLines
Comment are closed.