DEVELOPING A RAYCASTING ‘3D’ ENGINE GAME IN PYTHON AND PYGAME – PART 6

The following changes will be covered in this post:

  • Improved skybox.
  • Bugfix for a bug related to the rendering of walls introduced with functionality to look up and down.
  • Red transparent screen effect added to act as a damage indicator for the player when enemy contact occurs.

Skybox Improvement


Due to the pattern and size of the image used for the skybox, an issue would occur where the image would suddenly switch to a different position. Although not game-breaking, it was somewhat jarring. To improve this, two changes needed to be made to the image used for the skybox:

  • the Image X (horizontal) resolution needed to be changed to be the same as the display window resolution (In this case, 1920 pixels).
  • The image needed to be replaced with a seamless image, i.e., the two sides of the image aligned to create an infinitely repeating pattern of clouds.

Wall Rendering Bugfix
A bug was introduced with the functionality for the player to look up and down that caused the rendering of walls to get miss-aligned if the player’s point of view was not vertically centered and the player was close to the wall in question. The image below shows an example of how the bug manifests:

This results from the game engine’s limitations and the lack of a z-axis for proper spatial positioning of items. To get around this, I added auto vertical centering of the player’s field of view every time the player moves. This will not completely fix the issue but will make it occur far less frequently.

To implement this change I added the following method in the Player class (in the playert.py file):

def level_out_view(self):
        if (self.HALF_HEIGHT - BASE_HALF_HEIGHT) > 50:
            self.HALF_HEIGHT -= 50
        elif self.HALF_HEIGHT - BASE_HALF_HEIGHT < -50:
            self.HALF_HEIGHT += 50
        else:
            self.HALF_HEIGHT = BASE_HALF_HEIGHT

And updated the keys_control method in the player class as follows:

def keys_control(self, object_map,enemy_map):
        sin_a = math.sin(self.angle)
        cos_a = math.cos(self.angle)
        keys = pygame.key.get_pressed()

        if keys[pygame.K_ESCAPE]:
            exit()
        if keys[pygame.K_w]:
            nx = self.x + player_speed * cos_a
            ny = self.y + player_speed * sin_a
            self.x, self.y = check_collision(self.x, self.y, nx, ny, object_map, HALF_PLAYER_MARGIN)
            if nx == self.x or ny == self.y:
                self.play_sound(self.step_sound)
            self.level_out_view()
        if keys[pygame.K_s]:
            nx = self.x + -player_speed * cos_a
            ny = self.y + -player_speed * sin_a
            self.x, self.y = check_collision(self.x, self.y, nx, ny, object_map, HALF_PLAYER_MARGIN)
            if nx == self.x or ny == self.y:
                self.play_sound(self.step_sound)
            self.level_out_view()
        if keys[pygame.K_a]:
            nx = self.x + player_speed * sin_a
            ny = self.y + -player_speed * cos_a
            self.x, self.y = check_collision(self.x, self.y, nx, ny, object_map, HALF_PLAYER_MARGIN)
            if nx == self.x or ny == self.y:
                self.play_sound(self.step_sound)
            self.level_out_view()
        if keys[pygame.K_d]:
            nx = self.x + -player_speed * sin_a
            ny = self.y + player_speed * cos_a
            self.x, self.y = check_collision(self.x, self.y, nx, ny, object_map, HALF_PLAYER_MARGIN)
            if nx == self.x or ny == self.y:
                self.play_sound(self.step_sound)
            self.level_out_view()
        if keys[pygame.K_e]:
            self.interact = True
            self.level_out_view()
        if keys[pygame.K_LEFT]:
            self.angle -= 0.02
            self.level_out_view()
        if keys[pygame.K_RIGHT]:
            self.angle += 0.02
            self.level_out_view()

Player Visual Damage Indicator
A Visual Damage Indicator is a way to let the player know he is taking damage. This will become more relevant at a later stage when the concept of health points is implemented, but for now, it provides a way of showing when the enemy is in touching range of the player.
The number of enemies has also been increased to three to increase the chances of a damage event.


The Visual Damage Indicator is implemented by drawing a semi-transparent red rectangle over the screen whenever a collision between the player and the enemy is detected.

To check for these collisions a new function was added in the common.py file as below:

def check_collision_enemy(x, y, map_to_check, margin):
    location = align_grid(x, y)
    if location in map_to_check:
        #  collision
        return True

    location = align_grid(x - margin, y - margin)
    if location in map_to_check:
        #  collision
        return True

    location = align_grid(x + margin, y - margin)
    if location in map_to_check:
        #  collision
        return True

    location = align_grid(x - margin, y + margin)
    if location in map_to_check:
        #  collision
        return True

    location = align_grid(x + margin, y + margin)
    if location in map_to_check:
        #  collision
        return True

    return False

This function is called from the keys_control function in player class:

self.hurt = check_collision_enemy(self.x, self.y, enemy_map, HALF_PLAYER_MARGIN)

In the drawing.py file the background method in the Drawing class was updated as follows:

def background(self, angle, half_height, hurt):
        sky_offset = -1 * math.degrees(angle) % resX
        print (sky_offset)
        self.screen.blit(self.textures['S'], (sky_offset, 0))
        self.screen.blit(self.textures['S'], (sky_offset - self.textures['S'].get_width(), 0))
        self.screen.blit(self.textures['S'], (sky_offset + self.textures['S'].get_width(), 0))
        pygame.draw.rect(self.screen, GREY, (0, half_height, resX, resY))
        if(hurt):
            RED_HIGHLIGHT = (240, 50, 50, 100)
            damage_screen = pygame.Surface((resX, resY)).convert_alpha()
            damage_screen.fill(RED_HIGHLIGHT)
            self.screen.blit(damage_screen, (0, 0, resX, resY))

RED_HIGHLIGHT is a Tuple with four values stored in it. The first three values represent the RGB color code, and the last value indicates transparency level, with 0 being completely transparent and 255 completely opaque.
The convert_alpha method tells Pygame to draw the rectangle to the screen applying the transparency effect.

Here is a video of the effect in action:

The source code for everything discussed in the post can be downloaded here and the executable here.

DEVELOPING A RAYCASTING ‘3D’ ENGINE GAME IN PYTHON AND PYGAME – PART 6