As ever, I'll try to have something new and interesting to talk about for every post rather than ranting about how everything sucks.
And a hearty thanks to everyone who tried out Isostrat last week, such feedback is invaluable.
Issues brought up, Answers to them
- Save/Load buttons don't work
- The "????" button doesn't do anything
- Far ends of the map sometimes don't work
- Clicks on Minimap don't recenter GameView
- Mouse-drag tile-painting in the editor is sporadic
- Needs better scrolling
- Editor needs an eraser tool
- Major slowdown at startup
- General slowness
I haven't implemented this yet. I've been working on getting the map into a savable state by moving all the rendering functions from the map class to the video class (where they should have been anyway). So: unimplemented.
I'm removing it now in a re-work of the editor interface: fixed.
It turns out that parts of the map were thought to be 'off map' by the game. I found a place where I was comparing an X coord to a Y coord and a Y coord to an X coord -- ridiculous! But now fixed.
I've been putting this one off because it requires me to code some unique behaviour into a GUI component. But why not? It's my game, after all, I can make something with unique functionality. I'll get around to it as I expand the functionality of the GUI (which is going to have to happen a lot more to get gameplay working properly): unimplemented.
As in, a few tiles show up painted, but there isn't a swath of, say, green grass between the start and end point of a mousedrag. As David O suggests, I'd do well to draw a line between points of mousedragging and paint every tile that collides with it -- and improve framerate generally: in-progress?
Yeah, the map needs to scroll on right-click drag (rather than kinda painting random tiles, if in edit mode). This should be very easy to do: unimplemented.
I didn't even think of this. Or I did a long time ago, but forgot with all the other stuff to do. It took no time to implement because I had code to do most of this already done: fixed.
Turned out this was due to blitting Borderstyle pieces tens of thousands of times.
...was due to the previous, and a horribly inefficient FindVisibleTiles function that was called on every render.
These both deserve their own sections, so:
Blit less, Blit bigger
Now this was interesting.
I'll talk more about how I found out that Borderstyle drawing was taking so long in the next section, so suffice to say: Borderstyle drawing was what was taking so long. And it makes sense, really, because to make the border for the Gameview I'd be blitting a little 4x4 image across the top and bottom, and down the left and right to make all the sides. Worst of all was blitting a 4x4 image of pure colorkey across a 796x596 field: 29651 (or so) blit calls for nothing that couldn't be done with a single fill.
The problem here is not necessary blitting for nothing but, rather, a process that increases quadratically -- in very rough terms, as the dimensions of a panel grew by x, I'd have to do x^2 blits. I solved my blit calls growing quadratically by increasing my blitting image quadratically as well to fit the area to-be-blitted by calling this function:
''' Make a new surface from four of given surf, in a square'''
surfquad = pygame.Surface( (surf.get_width() * 2, surf.get_height() * 2 ) )
for x in range(2):
for y in range(2):
surfquad.blit( surf, (x * surf.get_width(), y * surf.get_height() ) )
... which takes a surface and returns that surface squared, and the Borderstyle does this until the surface could be blitted across the width of the panel in 2 blit calls. By doing this, about 30,000 blit calls are reduced to about 30 blit calls. I made one for linear blits as well with less dramatic but still valuable savings in blit calls.
The lesson: A blit call has much more overhead than a hell of a lot of pixels in a blit call , so: Blit less, Blit bigger. Now I need to do this with terrain rendering.
And if a problem increases exponentially, increase the answer exponentially.
(Not that you can for all situations, but it sure sounds clever.)
Finding bottlenecks and FindVisibleTiles
First the bottlenecks.
A million thanks to Oluseyi for pointing out Python Call Graph to me. By giving me a tool that can bring my attention to processing bottlenecks, he has, well, indirectly lead to me learning a hell of a lot about programming (or so it feels over the last few days).
So: Python Call Graph makes graphs of Python programs and tells the number of calls and time spent for each function. Now this is very interesting. When I first ran this three places stood out as taking the most time. In order they were Borderstyles, which I've discussed, FindVisibleTiles, which I will discuss, and a far third was David O's vector.add function, which I'm going to let slide because it gets called a few ten thousands of times more than any other function. Oh, and tile.update is up there too I suppose, but that too is called like a hundred thousand times in a short period of time, and it doesn't really do anything yet except make sure that animation is working. Maybe I could only call that update on tiles which are visible.
Here's a shot of FindVisibleTiles being slow:
At first I tried re-arranging the deck chairs on the Titantic and got this:
Which was even slower than before. What I was doing was calling updateDrawPos, which found the absolute pixel position the tile was to be drawn at for every tile, then filtering out tiles that didn't fall within the Gameview area (plus a little). While making up new functions to obfuscate the problem I found myself only pulling tiles from those rows in the map matrix that the Gameview could possibly see, then I thought -- why not clip columns too? Why not just take a slice out of the map matrix of the tiles I know I will be able to see based on transforming the Gameview offset from (0,0) to an offset in the map matrix, and the size of the Gameview to a size in tile rows/cols in the matrix, so then:
for n in range(viewtop,viewrows):
viewTiles.extend( self.isomatrix[n][viewleft:viewcols] )
3500 times faster. And I only call updateDrawPos in the renderer where, you know, it's actually needed.
Optimizing was fun. I get this thrill from making things faster and more efficient. And I was only doing it to avoid thinking to hard about what to do next.
Here's the full program chart of the more or less current build for those who want their brains to explode: