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