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())