[Pygame - FILES INCLUDED] Performance issues in RPG prototype

Started by
9 comments, last by GarrickW 13 years, 1 month ago
[Solved - thanks to Enders!]

So I've been fiddling around with PyGame and I've started building a basic prototype of a 2D RPG. I've encountered some performance problems, however, and before I proceed, I wanted the advice of some experienced programmers on what I'm doing wrong. I'm seeing really low framerates when using a map measuring 256x256, whereas a 128x128 map is all right - and there isn't even much game logic running in the backgorund. My goal is to have maps be at least 1024x1024 in size, if not more, with a considerable amount of game logic running in the background. I'm planning on rewriting some of the code in C++ to speed up the math stuff (I am currently, slowly, learning C++) but I've got a feeling I can get some of this solved in Python simply by programming more intelligently. So here's the rundown:

I have my map divided into square tiles measuring 16 pixels. A 128-size map means 128 pixels by 128 pixels. My program scans a .png image, interprets the color of the pixels into code, and makes a game map out of that code. At any given time, roughly 32x32 tiles are on the game screen, and are part of a list (loadedTiles) that is dealt with for most of the program. All the tiles collectively belond to another list (mapTiles), with which I try to do as little as possible, to keep the workload down. Problem #1 is that more tiles = worse performance, although I don't mean to be doing much with the map tiles other than calculating their positions and checking whether they are within viewing range. One possible help would be to check whether they are in viewing range less often, say every half-tile or something, but I'm not sure what the cleverest way of doing this would be (at the moment, all tiles are being checked every cycle, which is probably not smart).

Problem #2 was with mobs, who were getting jammed at the edge of the screen whenever the avatar moved around. I disabled this by deactivating all mobs that are not in viewing range, but this isn't a great solution if I want to expand mob behavior in the future. Not critical, at the moment, but not great.

Problem #3 is just bizarre; there are three projectile types, and of the three, 2 rotate correctly - FireBlast and IceBlast. But when Arrow rotates, instead of getting an invisible, colorkey expanded rectangle for the sprite, I get a flashing white one. I don't understand this at all, since the code is otherwise the same for the three projectiles.

I've put up my code and art in the files in a link at the end (I couldn't upload it, the board wouldn't let me) and if anyone could take a look at it and give me some pointers, that'd be great. I'm a novice at best, but I'm more than willing to learn the tricks of the trade. For the prototype, controls are as follows:

Arrows or WASD - movement
M - toggles between Editing and Gameplay modes (visible in the upper right)

EDITING MODE:

Left Click - change the tile's type
Right Click - place a mob
Mouse Wheel - cycle through the available tile types

GAMEPLAY MODE

Left Click - shoot
Mouse Wheel - cycle through the available weapons (visible under the hearts)

FILES: http://www.mediafire...oou64zwuavk8b7b

Thanks for any tips or advice!
Advertisement
ahh man. That is nostalgic for me. Reminds me of my first RPG engine I made. I will take look and see what I can come up with. :) So far, it loads okay for me.
hmm...a little confused on how you are selecting which tiles to draw. Can you tell me how you are determining what to draw and where?
Alright, been going over your source. I think i understand how you load your map.

You put all your map data in the game.mapTiles array. Looking around your source code, I saw that you are looping that array WAY to many times. You dont need to loop that array at all in fact.

All you need to do is know where your player is, and draw around your player.

I see how you are using a small image to map the tiles for the larger world. I used the same technique for collision detection. You can use that idea and build upon it. I am not sure what is faster: reading an array, or reading a bitmap. But in the end it will do the same thing.

What you can do is since you know the size of the world map and you know the size of the tiles. you just multiply them together.

world map size: 20x20
tile size: 16x16

so your position map is 320x320

sets say we want our viewport to only show 8 tiles across and 8 tiles down. So the view port will only show 128 pixels across and 128 pixels down. In the middle will be the player.

Lets make our player start position to be 123x98. A strange number, yes?
We will get what tile our player is on by dividing 16 from those numbers. *note: you want integers, so cast to an integer*
123x98 / 16x16 = 7x6

So we will be able to draw our tiles by offsetting them according to our player.


for tiley in range(0, mapsizey):
for tilex in range(0, mapsizex):
posx = -playerx + int(windowWidth/2) + (tilex * tileWidth)
posy = -playery + int(windowHeight/2) + (tiley * tileheight)
tile[tilex, tiley].draw(posx, posy)


That will off course draw the whole map around the player. You really just want to draw a small section of the map.



startMapx = int(playerx/tileWidth) - int(windowWidth/tileWidth/2) - 1
startMapy = int(playery/tileHeight) - int(windowHeight/tileheight/2) - 1

endMapx = int(playerx/tileWidth) + int(windowWidth/tileWidth/2) + 1
endMapy = int(playery/tileHeight) + int(windowHeight/tileheight/2) + 1

for tiley in range(startMapy, endMapy):
for tilex in range(startMapx, endMapx):
posx = -playerx + int(windowWidth/2) + (tilex * tileWidth)
posy = -playery + int(windowHeight/2) + (tiley * tileheight)
tile[tilex, tiley].draw(posx, posy)


This will only draw the tiles that are around the player. And if you have a really really big map, it wont loop through the whole thing. Only the part you want. I padded the start and end position with 1 so when drawing it wont show a blank area when moving.

I know you have to change a lot of code to be able to do this. Particularly making your map array into a multidimensional array. You could also use the bitmap, and reference that to draw the rest. But like i said, I do not know what one is faster.

Anywho, this was a good way i made my RPG back in the day. Maybe you could use something from it.
Wow, thanks for taking the time to look into my code! I think I get what you're doing here; I'll take your advice and see what can't be done with it, and I'll post again when I've made some progress.

Using the bitmap for collision detection is an interesting idea, too. I'll test it, and see if I can tell what's faster.
All right! So, I made a lot of changes with my code, mostly along the lines you suggested, and now I am able to play a 256x256 map with as little lag as a 64x64 sized map! I've eliminated all instances of looping through lists of all tiles, except during initialization. I no longer modify all tile positions when the player moves; I modify the positions of the tiles I need to blit, immediately before blitting them, by comparing them to the player's position in the map.

Also, I adapted the code used to determine the view-screen to help mobs and projectiles detect collisions more quickly, by only making them check collisions with the tiles in a 3x3 square around their position, rather than all visible tiles (or, python forbid, all tiles in the game world).

This is great! Now that performance is back to reasonable , I can move onwards with some more mechanics.#

For anyone else who ever happens upon this and wants to take a peek, here is the code I use for determining what tiles to blit; note that all objects (tiles, projectiles, the player, mobs, etc) have a self.screenPosX, self.screenPosY, self.mapPosX and self.mapPosY. My game.orderedMap is a list of lists, with each sub-list representing a row or tiles on the map. The tile's .updatePos() function just draws a new rectangle at the current position, for bliting.

#This is the new blitting code, for the map.
startMapX = (player.mapPosX / TILEWIDTH) - (VIEWWIDTH / 2) - 2
endMapX = (player.mapPosX / TILEWIDTH) + (VIEWWIDTH / 2) + 3

startMapY = (player.mapPosY / TILEHEIGHT) - (VIEWHEIGHT / 2) - 2
endMapY = (player.mapPosY / TILEHEIGHT) + (VIEWHEIGHT / 2) + 3

xShift = player.mapPosX - player.screenPosX
yShift = player.mapPosY - player.screenPosY

for tileRow in range(startMapY, endMapY):
for tileCol in range(startMapX, endMapX):
if tileRow in range(len(game.orderedMap)) and tileCol in range(len(game.orderedMap[tileRow])):
tile = game.orderedMap[tileRow][tileCol]
tile.screenPosX = tile.mapPosX - yShift
tile.screenPosY = tile.mapPosY - xShift
tile.updatePos()
windowSurface.blit(tile.data["texture"], tile.data["rect"])
ALRIGHT!

I am glad it worked out with you :D
Can i download it? I want to try it out again :)
Sure thing; I've also added code that counts the number of structures on the map, which I intend to use to have the game delineate settlements and kingdoms based on the distribution of buildings; loading is really slow, but now there are nice little numbers at the start to say how far things are going. The first number is the percentage of tiles generated in the world, and the second is the number of buildings detected. Here's a link, I hope it works!

http://www.mediafire.com/?uvdam6zmdysvdu3
whoa, big increase on the movement speed. Much better. But now I am curious as to why it would take soo long to load. Sure was fun running around the map. Wonder what a 1024x1024 map would be like O.O
Hey, minor bug (or maybe not a bug) is that when the player collides with something, the movement no longer in that direction no longer is being updated, that is, if I'm going Northeast and I encounter a wall to the right of me, the character will just move up, even when I'm not longer colliding with the wall. I have to release the key and press it again to go in that direction again.

Other than that, it looks pretty good :) Nice sprites, btw. Did you draw the yourself?

Yo dawg, don't even trip.

This topic is closed to new replies.

Advertisement