Search code examples
python-3.xpyglet

Is there a reason to use a pyglet batch over iterating and drawing through a dictionary or array?


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.


Solution

  • 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.

    animation

    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.