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 RenderTile
s, 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.
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.