For the past week, I've been developing a simple raycasting algorithm. And I've finally managed to work out how to add textures and to get everything working.as I'd like it to. However, even at a rather low resolution of 5 pixels per vertical scan line, the program still runs incredibly slowly. It runs a bit better in fullscreen mode, as here the mouse input works properly, but it's still really bad.

It ran smoothly before I added the textures, and just had plain colour, even at a resolution of 1 pixel per vertical scan line. But the game is nearly unplayable now. So I want to know how I can optimize my code to make the program run faster?

I'd also like it if somebody would review the code normally so that I can improve it further.

Here are the two images that I'm using to test the program for those interested:

HUD.png

Blood Wall Dark.bmp

The file Level.txt contains 256 1's and spaces in a 16x16 configuration, with the 1's as the walls and the spaces as the gaps. But the code does work with any sized grid. The create_level() converts this file into a list.

\$\begingroup\$I haven't read all your code, but you're trying to implement a slow algorithm, without a GPU, in JITless, interpreted Python, without any libraries like NumPy that people use when they need to do serious math in Python. Of course it'll be slow.\$\endgroup\$
– user2357112Apr 5 '17 at 18:22

\$\begingroup\$@user2357112 Is there a way of using the GPU in my computer to do this?\$\endgroup\$
– George WillcoxApr 5 '17 at 18:24

1

\$\begingroup\$OpenGL? DirectX? CUDA? There are Python bindings for all those, if you want to stick to Python.\$\endgroup\$
– user2357112Apr 5 '17 at 18:30

1 Answer
1

Python is not a fast language — it trades execution speed for flexibility and introspectability — and the kind of repetitive numerical computation involved in CPU rendering is pretty much the worst case for Python. It would make much more sense to use a 3D rendering toolkit like PyOpenGL.

But so long as we understand that this is just an exercise, there are bound to be some improvements we can make. An important step is to measure the framerate so that we can be sure we're making improvements and not just fiddling with code. Here's code for an exponentially weighted moving average:

The first step in speeding this up is to understand where all the time is going. In comments, Caridorc suggested that you might use the profiler to figure this out. But the structure of the code makes it clear that the biggest fraction of the runtime is going to be spent in this loop (over the pixels in a column in a texture):

for y in range(texHeight):

So instead of looping over the pixels in the column and drawing each pixel using pygame.draw.line, let's extract the column from the texture as a surface, colour-multiply it to apply the lighting effects, scale it, and blit it onto the screen.

Scale it to the height at which we're going to draw it using transform.scale:

column = pygame.transform.scale(column, (resolution, yHeight))

Blit it to the screen:

SCREEN.blit(column, (x, int(yStart)))

I find that this change more than doubles the framerate, to about 40 fps.

As written above, this change causes some ugliness in the rendering of walls near the camera because of the way the computation of yStart causes the texels to line up with the screen edge. This can be avoided by recomputing yStart and yHeight after colStart and colHeight, like this:

\$\begingroup\$Thanks! This has really helped a lot, there are just a few things that I'd like to mention. I couldn't get your way of getting the frame rate to work properly, so instead I ran print(CLOCK.get_fps()) which worked perfectly. And the variable texStart doesn't exist, I assumed you meant yStart but this did not work either. And although it isn't as good to just blit the entire column, as you were only bliting the part on-screen, I just ran the last part of the code that you provided to get it to work. But the program now runs at 60 frames per second for me, and so I am pleased!\$\endgroup\$
– George WillcoxApr 7 '17 at 22:03

\$\begingroup\$Sorry about the mistake — texStart should be colStart. For avoidance of doubt, I added the revised code to the post. The problem with your version (where you scale up to lineHeight) is that lineHeight can get arbitrarily large (when the wall is very close to the camera) and so the call to scale can take arbitrarily long (and use arbitrary amounts of memory). It's necessary to scale only the portion of the texture that is going to be drawn.\$\endgroup\$
– Gareth ReesApr 8 '17 at 18:52

\$\begingroup\$You started the answer by a good paragraph (I experienced that statement when I worked with Python and OpenCV )\$\endgroup\$
– Billal BegueradjApr 18 '17 at 17:32