pythonpyglet

Using Pyglet how could someone apply multiple view like translations to multiple groups?


I understand the question doesn't say much, so I'll just try to explain my problem.

I'm working on a project in Pyglet that is a 2D top down game with Labels representing tiles on a grid (The reasoning for Labels is to emulate ASCII graphics, and that its easier then making art by hand). I already have all my batches and groups set up and even a camera, and what I'm looking for is an optimization over some of my code.

I figured that instead of having mass amounts of sprites off screen that represent the whole map, I could instead reuse pre-existing sprites to make the game run faster, to do this I split up my tiles into groups that would be children of the main group that held them originally.

Basically, I have one MapGroup instance (the parent group), which holds 36 MapGrid instances (the children groups) and each MapGrid holds 1024 RenderTile instances (the tiles).

What I initially did was that whenever a new MapGrid would be needed, say the camera moved to the right to where some empty space would be, MapGroup would look at every grid furthest in the opposite direction and set those MapGrid's internal coordinate values to cover that empty space, making it seem like a new grid appeared, but its just a reused grid that was offscreen.

The main issue then came with the actual RenderTiles, and is what I'm mostly trying to work around, my best way to move them so far has been to iterate through each RenderTile in every MapGrid that moved and setting there position that way. Its better then rendering a whole new MapGrid and I even scheduled each movement with a 1 ms delay from the last making it not lag much to at all, but its still not great, and I feel like there should be better.

My main attempt to optimize was by trying to apply the same translate that MapGroup has onto each MapGrid, but for some reason, the set_state only seems to run on the first MapGrid made, so from there I tried to make the MapGrid set_state iterate through each MapGrid and use values from the iteration instead, but that too didn't work. I even tried making it so that MapGroup, isn't a pyglet group, instead only something that manages groups, but that also had the issue where MapGrid still only ran the first instance made. If there is a way to separate out these set_state calls so that it does actually set and unset for each induvial MapGrid, that would be great, otherwise I don't have a clue where to take this.

Here is code that I'm working with extra comments on what does what:

from pyglet.text import Label
from pyglet.graphics import Group
from pyglet.clock import schedule_once
from pyglet.math import clamp,Vec2,Vec3
from pyglet.gl import glEnable,glDisable,glScissor
from random import randint

class RenderTile(Label): # Main Tile
    def __init__(self,internalCoords,*args,**kwargs):
        super().__init__(*args,**kwargs)
        self.internalCoords = internalCoords

        self.x = internalCoords[0] * 16 + self.group.gridCoord[0] * 512
        self.y = internalCoords[1] * 16 + self.group.gridCoord[1] * 512
        #Initial values, fine on its own, but idealy the self.group.gridCoord[0] * 512 can be removed as set_state would handle the relative position


    def rePos(self,dt,newPos): #Simply called by MapGrid's update
        self.position = newPos



class MapGrid(Group): # Child Group
    def __init__(self,gridCoord,win,batchPass,order=0,parent=None):
        super().__init__(order,parent)
        self.gridCoord = gridCoord
        self._window = win
        # pyglet window, passed from parent

        self.internalTiles = {} 
        # Dict that holds each Tile, their keys are their coordinates in the grid

        gridTestColor = (randint(0,255),randint(0,255),randint(0,255),255) 
        # Random color for whole grid, mostly for distinction

        internalXSet = 0
        while internalXSet < 32:
            internalYSet = 0
            while internalYSet < 1:
                self.internalTiles[(internalXSet,internalYSet)] = RenderTile(internalCoords=(internalXSet,internalYSet),text="##",font_name="dayton",font_size=8,color=gridTestColor,batch=batchPass,group=self)
                internalYSet += 1
            internalXSet += 1
        # While loops that add each RenderTile to the grid, currently makes the grid 32x1 for testing purposes, it will be 32x32 by the end

        self.testInternalGrid = RenderTile(internalCoords=(16,16),text=str(self.gridCoord),font_name="dayton",font_size=24,color=(255,255,255,255),batch=batchPass,group=self,anchor_x="center",anchor_y="center")
        # Debug item, simply shows the grids position at its center

    def update(self):
        eventIterations = 0
        for tile in self.internalTiles:
            schedule_once(self.internalTiles[tile].rePos,eventIterations,(tile[0] * 16 + self.gridCoord[0] * 512,tile[1] * 16 + self.gridCoord[1] * 512,0))
            eventIterations += 0.001
        self.testInternalGrid.position = (256 + self.gridCoord[0] * 512,256 + self.gridCoord[1] * 512,0)
        self.testInternalGrid.text = str(self.gridCoord)
        # Updates the grids position, this is the unideal solution that I want to replace


    # def set_state(self):
    #     for grid in self.parent.internalGrids:
    #         self.parent.internalGrids[grid]._window.view = self.parent.internalGrids[grid]._window.view.translate(Vec3(-grid[0]*10, -grid[1]*10, 0))
    #         print(grid)

    # def unset_state(self):
    #     for grid in self.parent.internalGrids:
    #         self.parent.internalGrids[grid]._window.view = self.parent.internalGrids[grid]._window.view.translate(Vec3(grid[0]*10, grid[1]*10, 0))

    # This set_state was what I was messing with, if there is some way to make this work as intended that would be absolutley amazing



class MapGroup(Group):
    def __init__(self,win,map,batchPass,x=-1040,y=-540,z=1.0,order=0,parent=None):
        super().__init__(order,parent)
        self._window = win
        # pyglet window

        self.mapData = map
        # currently unused, will hold the data for the map, like what a tile should look like and what not

        self.x = x
        self.y = y
        self._z = z
        # Probaly should have just called this zoom instead of z, but it doesn't break anything yet, so its fine

        self.internalGrids = {}
        # Dict that holds each Grid, their keys are their coordinates in the map

        self.memoryPos = 0

        internalXSet = -3
        while internalXSet < 3:
            internalYSet = -3
            while internalYSet < 3:
                self.internalGrids[(internalXSet,internalYSet)] = MapGrid((internalXSet,internalYSet),win,batchPass,parent=self)
                internalYSet += 1
            internalXSet += 1
        # While loops that add each MapGrid to the MapGroup


    @property
    def position(self):
        return Vec2(self.x,self.y)

    @position.setter
    def position(self,new_pos):
        self.x,self.y = new_pos
        self.update()

    @property
    def z(self):
        return self._z

    @z.setter
    def z(self,new_z):
        self._z = clamp(round(new_z,1),0.5,2)

    def update(self):
        # All the following is frankly a pain to explain, but this is the code that looks for the furthest opposite grid and tells it to move
        XGridOffset = (self.position[0]+1040)/128-self.memoryPos*2
        XGridCheck = abs(XGridOffset)
        self.memoryPos += int(XGridOffset)
        while 1 <= XGridCheck:
            xCheckList = [grid[0] for grid in self.internalGrids]
            xMin = min(xCheckList)
            xMax = max(xCheckList)
            if 0 < XGridOffset:
                xChangeList = [grid for grid in self.internalGrids if self.internalGrids[grid].gridCoord[0] == xMin]
                for grid in xChangeList:
                    shiftingGrid = self.internalGrids.pop(grid,None)
                    newCoord = (xMax+1,shiftingGrid.gridCoord[1])
                    shiftingGrid.gridCoord = newCoord
                    self.internalGrids[newCoord] = shiftingGrid
                    shiftingGrid.update()
            elif 0 > XGridOffset:
                xChangeList = [grid for grid in self.internalGrids if self.internalGrids[grid].gridCoord[0] == xMax]
                for grid in xChangeList:
                    shiftingGrid = self.internalGrids.pop(grid,None)
                    newCoord = (xMin-1,shiftingGrid.gridCoord[1])
                    shiftingGrid.gridCoord = newCoord
                    self.internalGrids[newCoord] = shiftingGrid
                    shiftingGrid.update()
            else:
                xChangeList = [grid for grid in self.internalGrids if self.internalGrids[grid].gridCoord[0] == xMin]
                for grid in xChangeList:
                    shiftingGrid = self.internalGrids.pop(grid,None)
                    newCoord = (xMax+1,shiftingGrid.gridCoord[1])
                    shiftingGrid.gridCoord = newCoord
                    self.internalGrids[newCoord] = shiftingGrid
                    shiftingGrid.update()
            XGridCheck -= 1

    # Main set_state
    def set_state(self):
        glScissor(400, 0, 1280, 1080)
        # Basically the space the map takes up, assuming a 1920x1080 display
        glEnable(3089) #GL_SCISSOR_TEST = 3089
        # Didn't feel like importing from pyglet.gl.gl, so I just put its number in instead

        map_matrix = self._window.view.translate(Vec3(-self.x, -self.y, 0))
        map_matrix = map_matrix.scale(Vec3(self.z, self.z, 1))

        self._window.view = map_matrix

    def unset_state(self):
        glDisable(3089) #GL_SCISSOR_TEST = 3089

        map_matrix = self._window.view.scale(Vec3(1/self.z, 1/self.z, 1))
        map_matrix = map_matrix.translate(Vec3(self.x, self.y, 0))

        self._window.view = map_matrix

Outside of rendering, here is what is minimally required to get the code working:

import pyglet
from render import MapGroup,MapGrid,RenderTile
# render.py is the name of the file holding the other code

config = Config(double_buffer=True)
display = pyglet.canvas.get_display()
screens = display.get_screens()

window = pyglet.window.Window(1920,1080,fullscreen=True,vsync=True,screen=screens[2])
# Change screen as needed, its just what I have it set to

main_batch = pyglet.graphics.Batch()
map_group = MapGroup(window,{},main_batch)
# Main batch and group

fps_display = pyglet.window.FPSDisplay(window=window,color=(0,255,0,255))

#gui_group = pyglet.graphics.Group(order=2)
#holdThingTest = pyglet.shapes.Rectangle(0,0,400,1080,color=(75,75,75,255),batch=main_batch,group=gui_group)
#holdThingTest2 = pyglet.shapes.Rectangle(1680,0,240,1080,color=(75,75,75,255),batch=main_batch,group=gui_group)
#Bad naming I know, but its just simple boxes to help show where the map should be, uncomment if wanted

@window.event
def on_mouse_drag(x,y,dx,dy,buttons,modifiers):
    if buttons & pyglet.window.mouse.MIDDLE:
        map_group.position = (map_group.x - dx,map_group.y - dy)
# Event for camera movement, just hold and drag middle mouse

@window.event
def on_mouse_scroll(x,y,scroll_x,scroll_y):
    map_group.z += scroll_y/10
# Event for zooming, just scroll your mouse wheel

@window.event
def on_draw():
    window.clear()

    main_batch.draw()
    fps_display.draw()
# Event for drawing, kind of a well duh

if __name__ == "__main__":
    pyglet.app.run(1/60)
# Thing that runs the code, not much else to say

I understand that I should only show what's necessary, and its a slight pain to craw through someone else's 170 some odd lines of code, but as far as I know, anything could be the issue, so I rather it be available in case I simply overlooked a one line change that fixes everything.


Solution

  • After some more messing around, I found out that by simply setting each MapGrid to have a unique order fixed it, so the fixed change was simply this:

            orderSet = 0
            internalXSet = -3
            while internalXSet < 3:
                internalYSet = -3
                while internalYSet < 3:
                    self.internalGrids[(internalXSet,internalYSet)] = MapGrid((internalXSet,internalYSet),win,batchPass,order=orderSet,parent=self)
                    orderSet += 1
                    internalYSet += 1
                internalXSet += 1
    

    The only thing I would want to note is that the order needs to be unique for each MapGrid, setting them all to 1, or 2 they seem to cause the same error again.