I would much rather just use a dictionary or array, so I can loop through and draw (or update) things in a specific order. But all guides and posts I've seen on this module use a batch instead. If there is a difference, is it big enough for me to go out of my way to use my less preferred method?
Here's an example of using a batch to draw out multiple items...
mainBatch = pyglet.graphics.Batch()
background = pyglet.sprite.Sprite(pyglet.image.load(image), x=xCor, y=yCor, batch=batch)
player = pyglet.sprite.Sprite(pyglet.image.load(image), x=xCor, y=yCor, batch=batch)
enemy = pyglet.sprite.Sprite(pyglet.image.load(image), x=xCor, y=yCor, batch=batch)
class gameWindow(pyglet.window.Window):
def on_draw(self):
self.clear()
mainBatch.draw()
From what I've seen online, there's no specific order in what's drawn first and last, and in my experience, the background was drawn above the player.
Here's an example of using a dictionary and looping through each one to draw them out in order...
gameObjects = {
'background': pyglet.sprite.Sprite(pyglet.image.load(image), x=xCor, y=yCor),
'player': pyglet.sprite.Sprite(pyglet.image.load(image), x=xCor, y=yCor),
'enemy': pyglet.sprite.Sprite(pyglet.image.load(image), x=xCor, y=yCor),
}
class gameWindow(pyglet.window.Window):
def on_draw(self):
self.clear()
for i in gameObjects:
gameObjects[i].draw()
I like this way better, but I'm all for changing things if they have clear advantages.
Batch()
objects are rendered "in one go", where as drawing sprites one by one will call several draw (and other meta functions), which in turn causes a lot of over head events to be triggered (like updating screen etc). So it's better to use a batch and call one draw function and one update screen than multiple. Same goes for positioning, instead of doing sprite.x = ...
individually, you're better off using sprite.position which doesn't re-calculate on both calls.
There performance increase is roughly <num of elements> * 3
of using batch vs manual rendering. If you're interested in having "layers" or render things in order, you can use OrderedGroup
as a layering option, a simpler platform example would be:
from pyglet import *
from pyglet.gl import *
key = pyglet.window.key
class collision():
def rectangle(x, y, target_x, target_y, width=32, height=32, target_width=32, target_height=32):
# Assuming width/height is *dangerous* since this library might give false-positives.
if (x >= target_x and x < (target_x + target_width)) or ((x + width) >= target_x and (x + width) <= (target_x + target_width)):
if (y >= target_y and y < (target_y + target_height)) or ((y + height) >= target_y and (y + height) <= (target_y + target_height)):
return True
return False
class GenericSprite(pyglet.sprite.Sprite):
def __init__(self, x, y, width, height, color=(255,255,255), batch=None, group=None):
self.texture = pyglet.image.SolidColorImagePattern((*color, 255)).create_image(width, height)
super(GenericSprite, self).__init__(self.texture, batch=batch, group=group)
self.x = x
self.y = y
def change_color(self, r, g, b):
self.texture = pyglet.image.SolidColorImagePattern((r, g, b, 255)).create_image(self.width, self.height)
class Block(GenericSprite):
def __init__(self, x, y, batch, group):
super(Block, self).__init__(x, y, 30, 30, color=(255, 255, 255), batch=batch, group=group)
class Player(GenericSprite):
def __init__(self, x, y, batch, group):
super(Player, self).__init__(x, y, 30, 30, color=(55, 255, 55), batch=batch, group=group)
class main(pyglet.window.Window):
def __init__ (self, width=800, height=600, fps=False, *args, **kwargs):
super(main, self).__init__(width, height, *args, **kwargs)
self.keys = {}
self.status_labels = {}
self.batch = pyglet.graphics.Batch()
self.background = pyglet.graphics.OrderedGroup(0)
self.foreground = pyglet.graphics.OrderedGroup(1)
self.player_obj = Player(40, 40, self.batch, self.foreground)
self.status_labels['player_position'] = pyglet.text.Label(f'Player position: x={self.player_obj.x}, y={self.player_obj.y}, x+w={self.player_obj.x+self.player_obj.width}, y+h={self.player_obj.y+self.player_obj.height}', x=10, y=self.height-30, batch=self.batch, group=self.background)
self.blocks = {}
for index, i in enumerate(range(10, 120, 30)):
self.blocks[i] = Block(i, 10, self.batch, self.background)
self.status_labels[f'block{i+1}_position'] = pyglet.text.Label(f'Block #{index+1}: left={self.blocks[i].x}, bottom={self.blocks[i].y}, top={self.blocks[i].y+self.blocks[i].height}, right={self.blocks[i].x+self.blocks[i].width}', x=10, y=self.height-(50+(index*16)), batch=self.batch, group=self.background)
self.alive = 1
def on_draw(self):
self.render()
def on_close(self):
self.alive = 0
def on_key_release(self, symbol, modifiers):
try:
del self.keys[symbol]
except:
pass
def on_key_press(self, symbol, modifiers):
if symbol == key.ESCAPE: # [ESC]
self.alive = 0
self.keys[symbol] = True
def render(self):
self.clear()
for key_down in self.keys:
if key_down == key.D:
self.player_obj.x += 1
elif key_down == key.A:
self.player_obj.x -= 1
elif key_down == key.W:
self.player_obj.y += 1
elif key_down == key.S:
self.player_obj.y -= 1
self.status_labels['player_position'].text = f'Player position: x={self.player_obj.x}, y={self.player_obj.y}, x+w={self.player_obj.x+self.player_obj.width}, y+h={self.player_obj.y+self.player_obj.height}'
for index, i in enumerate(range(10, 120, 30)):
if collision.rectangle(self.player_obj.x, self.player_obj.y, self.blocks[i].x, self.blocks[i].y, width=30, height=30, target_width=30, target_height=30):
# self.blocks[i].change_color(255,55,55)
self.status_labels[f'block{i+1}_position'].color = (255,55,55,255)
else:
self.status_labels[f'block{i+1}_position'].color = (55,255,55,255)
self.batch.draw()
self.flip()
def run(self):
while self.alive == 1:
self.render()
# -----------> This is key <----------
# This is what replaces pyglet.app.run()
# but is required for the GUI to not freeze
#
event = self.dispatch_events()
if __name__ == '__main__':
x = main()
x.run()
This example was created to help someone with collision detection, but I think it'll work here since it uses OrderedGroup
for two layers and batch rendering.
There's some other interesting optimizations and examples that you can check out, like a particle system which has been enhanced to use some cool GL Features to render and update more than well over 1M objects on screen:
from array import array
import random
import pyglet
import arcade
from arcade import gl
window = pyglet.window.Window(720, 720)
ctx = gl.Context(window)
print("OpenGL version:", ctx.gl_version)
size = window.width // 4, window.height // 4
def gen_initial_data(width, height):
dx, dy = window.height / width, window.width / height
for y in range(height):
for x in range(width):
# current pos
# yield window.width // 2
# yield window.height // 2
yield x * dx + dx / 2
yield y * dy + dy / 2
# desired pos
yield x * dx + dx / 2
yield y * dy + dy / 2
def gen_colors(width, height):
for _ in range(width * height):
yield random.uniform(0, 1)
yield random.uniform(0, 1)
yield random.uniform(0, 1)
buffer1 = ctx.buffer(data=array('f', gen_initial_data(*size)))
buffer2 = ctx.buffer(reserve=buffer1.size)
colors = ctx.buffer(data=array('f', gen_colors(*size)))
geometry1 = ctx.geometry([
gl.BufferDescription(buffer1, '2f 2x4', ['in_pos']),
gl.BufferDescription(colors, '3f', ['in_color']),
])
geometry2 = ctx.geometry([
gl.BufferDescription(buffer2, '2f 2x4', ['in_pos']),
gl.BufferDescription(colors, '3f', ['in_color']),
])
transform1 = ctx.geometry([gl.BufferDescription(buffer1, '2f 2f', ['in_pos', 'in_dest'])])
transform2 = ctx.geometry([gl.BufferDescription(buffer2, '2f 2f', ['in_pos', 'in_dest'])])
# Is there a way to make ortho projection in pyglet?
projection = arcade.create_orthogonal_projection(0, window.width, 0, window.height, -100, 100).flatten()
points_program = ctx.program(
vertex_shader="""
#version 330
uniform mat4 projection;
in vec2 in_pos;
in vec3 in_color;
out vec3 color;
void main() {
gl_Position = projection * vec4(in_pos, 0.0, 1.0);
color = in_color;
}
""",
fragment_shader="""
#version 330
in vec3 color;
out vec4 fragColor;
void main() {
fragColor = vec4(color, 1.0);
}
""",
)
points_program['projection'] = projection
transform_program = ctx.program(
vertex_shader="""
#version 330
uniform float dt;
uniform vec2 mouse_pos;
in vec2 in_pos;
in vec2 in_dest;
out vec2 out_pos;
out vec2 out_dest;
void main() {
out_dest = in_dest;
// Slowly move the point towards the desired location
vec2 dir = in_dest - in_pos;
vec2 pos = in_pos + dir * dt;
// Move the point away from the mouse position
float dist = length(pos - mouse_pos);
if (dist < 60.0) {
pos += (pos - mouse_pos) * dt * 10;
}
out_pos = pos;
}
""",
)
frame_time = 0
mouse_pos = -100, -100
@window.event
def on_draw():
global buffer1, buffer2, geometry1, geometry2, transform1, transform2
window.clear()
ctx.point_size = 2
geometry1.render(points_program, mode=gl.POINTS)
transform_program['dt'] = frame_time
transform_program['mouse_pos'] = mouse_pos
transform1.transform(transform_program, buffer2)
buffer1, buffer2 = buffer2, buffer1
geometry1, geometry2 = geometry2, geometry1
transform1, transform2 = transform2, transform1
def update(dt):
global frame_time
frame_time = dt
@window.event
def on_mouse_motion(x, y, dx, dy):
global mouse_pos
mouse_pos = x, y
if __name__ == '__main__':
pyglet.clock.schedule(update)
pyglet.app.run()
Which uses the arcade library.
Your PC would literally crash if you tried to do these things without batches and some GL optimizations. All credit goes to einarf and Rafale25 [FR] in the official discord server for this last example.