The following additions and changes to the game engine will be covered:
- Adding the ability for the player to look up and down. (As requested by Matthew Matkava)
- Addition of basic spatial sound to the enemy.
- Bugfix that relates to the player footstep sounds.
Player Looking Up and Down
First, we will look at adding the ability for the player to look up and down, or more accurately, add the illusion of looking up and down. As mentioned in the first post in this series, the game engine being developed is not actually 3D but rather a pseudo-3D rendering of a 2D game world. This means, in essence, that there is no Z-axis (up and down) in the game world. Thus the player looking up and down is simply an illusion and does not affect the game in any way.
All sprite and walls rendered in the game engine, as well as the point where the skybox ends and the floor starts, use a pre-defined horizon as a reference point to determine the position and height of the items to be drawn to the screen, up to this point the horizon used was half of the game window resolution and was defined in the settings.py file with the name HALF_HEIGHT:
HALF_HEIGHT = resY // 2

To create the illusion of the player looking up and down, we will move this horizon up and down based on the users’ inputs:

The first thing we need to do is rename HALF_HEIGHT in the settings.py file, as it will still be required but only to determine the center of the game window:
BASE_HALF_HEIGHT = resY // 2
Next, a new horizon value needs to be declared, and this will be done inside the Player class in the player.py file as it will now be under the control of the player, so inside the Player __init__ method, the following has been added:
self.HALF_HEIGHT = BASE_HALF_HEIGHT
BASE_HALF_HEIGHT is set as a starting value, ensuring that the game starts with the player looking straightforward.
The mouse movement function in the Player class was updated to move newly defined Horizon value based on mouse movement:
def mouse_control(self):
if pygame.mouse.get_focused():
difference = pygame.mouse.get_pos()
difference_x = difference[0] - HALF_WIDTH
difference_y = difference[1] - BASE_HALF_HEIGHT
pygame.mouse.set_pos((HALF_WIDTH, BASE_HALF_HEIGHT))
self.angle += difference_x * self.sensitivity
if (resY - resY / 4) >= self.HALF_HEIGHT >= resY/4:
self.HALF_HEIGHT -= difference_y * self.look_sensitivity
elif (resY - resY / 4) <= self.HALF_HEIGHT:
self.HALF_HEIGHT = (resY - resY / 4)
elif self.HALF_HEIGHT <= resY/4:
self.HALF_HEIGHT = resY/4
Top and bottom boundaries are set to prevent the player from looking too far up and down, which can result in issues in the game engine.
All references to the original HALF_HEIGHT value defined in the settings.py file need to be changed to use the new Player.HALF_HEIGHT value. The locations of these references are as follows:
- The background method in the Drawing class (prawing.py)
- raycasting function (raycasting.py)
- locate_sprite method in the SpriteBase class (sprite.py)
Basic Enemy Spatial Sound
Next, let us look at adding basic spatial sound to the enemy. The idea behind this implementation is that the sound the enemy makes gets louder as it gets closer to the player. This is implemented in the following method located in the Enemy class (enemy.py):
def play_sound(self, distance):
if not pygame.mixer.Channel(4).get_busy():
volume = (1 / distance)*10
pygame.mixer.Channel(4).set_volume(volume)
pygame.mixer.Channel(4).play(pygame.mixer.Sound(self.sound))
Where the distance variable value is set to the distance between the enemy and the player.
The play_sound method is then called from the move function of the player class as per the code below:
def move(self, player, object_map, distance):
new_x, new_y = player.x, player.y
if self.activated:
if player.x > self.x:
new_x = self.x + ENEMY_SPEED
elif player.x < self.x:
new_x = self.x - ENEMY_SPEED
if player.y > self.y:
new_y = self.y + ENEMY_SPEED
elif player.y < self.y:
new_y = self.y - ENEMY_SPEED
self.x, self.y = check_collision(self.x, self.y, new_x, new_y, object_map, ENEMY_MARGIN)
if (self.x == new_x) or (self.y == new_y):
self.moving = True
self.play_sound(distance)
else:
self.moving = False
This will result in the enemy, while moving, making a sound with a loudness inversely proportional to the distance to the player.
Footstep Sound Bugfix
A bug in the previous version of the code resulted in the player only making footstep sounds if the player was not moving due to a collision with an object. The code below fixes this issue by checking if the players x or y positions changed, and if any of the two values have changed, the footstep sound will be played:
def keys_control(self, object_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)
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)
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)
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)
if keys[pygame.K_e]:
self.interact = True
if keys[pygame.K_LEFT]:
self.angle -= 0.02
if keys[pygame.K_RIGHT]:
self.angle += 0.02
The source code for everything discussed in the post can be downloaded here and the executable here.