I normally don’t share little strange bits of unfinished code – but I thought this was pretty cool. I was messing around with coding a little “fish tank” game in Python utilizing Pygame. One thing I’m bad about doing is attempting to make everything modular – which is why my Text Adventure game turned out to be a Text Adventure Engine. I wanted to utilize Pygame, but I wanted to load in sprite sheets in a predictable manner. In this case I found some sprites that are generally utilized for RPG Maker that always contain a predictable pattern (Multiple sprites on the same sheet, differing only by color). Given that there are so many available due to its popularity, it also makes things easy to play with. I believe there are licensing problems here, but this is just for personal use as an example. You could utilize any sheets that are uniform in size. Here’s a little example of one if those sheets.
I found some example code to use as a base, but it contained errors and didn’t account for multiple sprites in the same sheet. Below is the SpriteSheet class. Essentially – this code reads the size of the total image in pixels, and you pass in the number of columns/rows (distinct cells groupings of blue fish, green fish, etc.). In the image above, that would be two rows by four columns. After that, some simple (sort of) math determines the size of each fish individually (I believe this example is 32px by 32px). Essentially – this means you can load in any sprite sheet that follows this same pattern (regardless of size), and this class can take account of it. Really it’s just kind of cool, it minimizes the code a bit.
class SpriteSheet(): def __init__(self, filename, o_rows, o_cols, rows, cols, p_rows, p_cols): """SpriteSheet Contructor""" self.image = pg.image.load(filename).convert_alpha() self.o_rows = o_rows #Number of rows of options in sprite total self.o_cols = o_cols #Number of cols of options in sprite total self.rows = rows #Number of rows for sprite chosen self.cols = cols #Number of cols for sprite chosen self.p_rows = p_rows #Row number of chosen sprite in sheet self.p_cols = p_cols #Column number of chosen sprite in sheet self.totalCellCount = cols * rows #Total cells in the specific sprite option chosen self.option_w = self.image.get_rect().width / self.o_cols #Width of particular option in sheet self.option_h = self.image.get_rect().height / self.o_rows #Height of particular option in sheet self.offset_x = self.option_w * p_cols #Offset the X based on which option in the sheet self.offset_y = self.option_h * p_rows #Offset the Y based on which option in the sheet self.init_rect = pg.Rect(0, 0, self.option_w, self.option_h) #Form initial rectangle (before we break it up into cells) self.w = self.cellWidth = self.init_rect.width / cols self.h = self.cellHeight = self.init_rect.height / rows #Get a list of (X,Y) coordinates of each cell in the sprite sheet with associated heights/widths #self.cells = list([((index % cols * self.w) + self.offset_x , (index / cols * self.h) + self.offset_y, self.w, self.h) for index in range(self.totalCellCount)]) #self.cells = list([((index % cols * self.w), (index % rows * self.h), self.w, self.h) for index in range(self.totalCellCount)]) self.cells =  myCounter = 0 for ry in range(0, rows): for rx in range(0,cols): self.cells.append(((myCounter % cols * self.w) + self.offset_x, (ry * self.h) + self.offset_y, self.w, self.h)) myCounter += 1 #print(self.cells) def draw(self, cellIndex = 0, x = 25, y = 25): self.rect = pg.Rect(x,y,self.w,self.h) #Form rectangle around cell #pg.draw.rect(screen,(100,100,100),self.rect,0) #debug, show rectangles screen.blit(self.image, (x, y), self.cells[cellIndex])
You’ll notice a bit of debugging code that is disabled as it’s not 100% polished, but it works. In my Main function, I draw a randomized selection of fish. Really this means I’m choosing a sprite sheet and picking a random (X,Y) of the sheet which corresponds to (Row,Column) and therefore am essentially randomizing the color and number of fish to appear.
def main(): #My Fish fish =  #Tiny Fishes for _ in range(10): fish.append(Fish(1,"../Imgs/FishSheet3.png",2,4,4,3,random.randint(0,1),random.randint(0,3))) #Smallish Fishes for _ in range(3): fish.append(Fish(1,"../Imgs/FishSheet1.png",2,4,4,3,random.randint(0,1),random.randint(0,3))) #Medium Fishes for _ in range(3): fish.append(Fish(1,"../Imgs/FishSheet4.png",2,4,4,3,random.randint(0,1),random.randint(0,3))) for f in fish: #Draw all fish on the screen f.draw(x = random.randrange(0,screen.get_width()), y = random.randrange(0, screen.get_height())) while True: event = pg.event.poll() if event.type == pg.QUIT: return if event.type == pg.MOUSEBUTTONDOWN: # Set the x, y postions of the mouse click x, y = event.pos for f in fish: if f.rect.collidepoint(x, y): print("collide!") for f in fish: f.move() pg.display.update() clock.tick(30) #never run more than 30fps screen.fill((30, 30, 30)) if __name__ == "__main__" : pg.init() main() pg.quit()
The collision code is just a placeholder in case I need it later for interaction. Here’s where things took a weird turn. I thought to myself… well fish don’t really always move at a predictable speed or direction. So I need to randomize both of those depending on the size of the fish Obviously they will move Right/Left more often than Up/Down so I weight those first. Then I randomized the length of travel before switching directions.
To be honest this also highlights a big problem with the current version of the code, my sprites are also highly predictable in terms of what specific cell represents up/down/left/right. Without that, I would have to pass the values into each object created which would be annoying – so ignore that problem and make them static.
class WeightedChoice(object): def __init__(self, weights): self.totals =  self.weights = weights running_total = 0 for w in weights: running_total += w self.totals.append(running_total) def next(self): rnd = random.random() * self.totals[-1] i = bisect.bisect_right(self.totals, rnd) return self.weights[i] class Fish(SpriteSheet): rightPos = 6 #Image index to use while moving right leftPos = 3 #Image index to use while moving left upPos = 9 #Image index to use while moving up downPos = 0 #Image index to use while moving down switchImg = 6 #Switch image after every 'x' moves def __init__(self, speed, filename, o_rows, o_cols, rows, cols, p_rows, p_cols): super().__init__(filename, o_rows, o_cols, rows, cols, p_rows, p_cols) self.speed = speed self.traveled = 0 self.randomizeTravel() self.randomizeDirection() #Set current index of sprite - tried to randomize so that all fins weren't doing the same thing self.curRightPos = random.randint(Fish.rightPos,8) self.curLeftPos = random.randint(Fish.leftPos,5) self.curUpPos = random.randint(Fish.upPos,11) self.curDownPos = random.randint(Fish.downPos,3) def randomizeTravel(self): self.traveled = 0 self.counter = 0 self.lenOfTravel = random.randint(10,20) def randomizeDirection(self): # The weighted list of fish. Each item is a tuple: (VALUE, WEIGHT) dirList = ( ('Right', 85), ('Left', 85), ('Up', 15), ('Down', 15), ) weightedChoice = WeightedChoice(dirList); # do this each time you want to grab a random fish: self.direction = weightedChoice.next()
Finally – I can call the move (bottom of the Fish class.
def move(self): self.counter += 1 #print(self.counter) #Switch up images if self.counter >= Fish.switchImg and self.direction == 'Right': if self.curRightPos >= Fish.rightPos + 2: self.curRightPos = Fish.rightPos else: self.curRightPos += 1 self.counter = 0 if self.counter >= Fish.switchImg and self.direction == 'Left': if self.curLeftPos >= Fish.leftPos + 2: self.curLeftPos = Fish.leftPos else: self.curLeftPos += 1 self.counter = 0 if self.counter >= Fish.switchImg and self.direction == 'Up': if self.curUpPos >= Fish.upPos + 2: self.curUpPos = Fish.upPos else: self.curUpPos += 1 self.counter = 0 if self.counter >= Fish.switchImg and self.direction == 'Down': if self.curDownPos >= Fish.downPos + 2: self.curDownPos = Fish.downPos else: self.curDownPos += 1 self.counter = 0 #Move the fish #self.draw(self.curRightPos, self.rect.x + self.speed, self.rect.y) #self.traveled += self.speed if self.direction == 'Right': self.draw(self.curRightPos, self.rect.x + self.speed, self.rect.y) #self.rect.x += self.speed self.traveled += self.speed elif self.direction == 'Left': self.draw(self.curLeftPos, self.rect.x - self.speed, self.rect.y) #self.rect.x -= self.speed self.traveled -= self.speed elif self.direction == 'Up': self.draw(self.curUpPos, self.rect.x, self.rect.y - self.speed) #self.rect.y -= self.speed self.traveled -= self.speed else: self.draw(self.curDownPos, self.rect.x, self.rect.y + self.speed) #self.rect.y += self.speed self.traveled += self.speed #Check if the fish is off the screen if self.rect.top > screen.get_height() - self.h: self.direction = 'Up' elif self.rect.right > screen.get_width(): self.direction = 'Left' elif self.rect.left < 0: self.direction = 'Right' elif self.rect.top < 0: self.direction = 'Down' #Check if we're done traveling if self.traveled > self.lenOfTravel: self.randomizeTravel() self.randomizeDirection()
VOILA! – Here be fish.
I know what you’re thinking, what happens if I change my list to randomize 1000 fish. It looks amazing is what – why stop at 1000. The “final” code is posted here at GitHub in case your interested. I’ll probably keep tinkering and make it a fully fledged project sometime down the road or sooner.