-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathmain.py
1702 lines (1350 loc) · 69.3 KB
/
main.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
"""
This script implements a dynamic, interactive car-racing arcade game, using Pygame.
The game features a player-controlled car,
dynamic enemy spawns (cars, bikes, pedestrians), fuel canisters for gaining additional lives,
and a day-to-night transition effect to enhance visual appeal.
Key functionalities include collision detection, object movement and animation, and sound effect
management.
The game operates across different screens (start, main game, game over).
These can be accessed by changing the self.state attribute.
So our run() method with the "main game loop" looks like this:
def run(self):
while True:
self.state()
Any calculation of length is done with our own unit(s): self.perc_H and self.perc_W
These units represent 1% of the user's actual screen height/width.
This unit is a floating point number and when actually using this unit,
it has to be multiplied by the wanted percentage, and converted to int (for precision).
Example: int(11*self.perc_H) will return the amount of pixels for 11% of the screen in int.
Key Classes:
- Game: Manages the main game logic, screen updates, and game states.
- Pedestrian, Bike, Canister: Handle specific game objects' behaviors and rendering.
- Button: Facilitates interactive button elements in the game's UI.
The script initializes Pygame, sets up game constants (for tweaking), attributes, loads rescources, and runs the main game loop.
We have a bunch of functionality in our main_game() method.
We update object positions, handle user inputs, render the game screen, spawn and remove enemies,
handle collision logic and keep track of high score and remaining lives.
While a lot of this behaviour is abstracted into methods,
there still is potential for more abstraction and refactoring.
Not only for these methods, but across the board.
Additional Remark 1: The order in which elements are drawn in the main_game() method is important.
We use this to create a layered effect, in which elements seem to be passing underneath the tree tops.
Additional Remark 2: The order in which the Authors are listed (for each method, or class) has a meaning.
If there are 2 Authors, the first mentioned worked the most on this section of code (important for oral exam).
Want to play?
We recommend a large screen and a headset or speakers (better than laptop quality speakers) for playing.
The streets are buzzing, but you have to get where you are going.
Not even a traffic jam can deter you. Can you survive 5, or even 10 minutes?
Authors: Florian Goldbach, Christian Gerhold
Requires: Pygame library
"""
import sys
import os
import random
import time
import pygame
def resource_path(relative_path):
""" Get absolute path to resource, works for dev and for PyInstaller """
try:
# PyInstaller creates a temp folder and stores path in _MEIPASS
base_path = sys._MEIPASS
except Exception:
base_path = os.path.abspath(".")
return os.path.join(base_path, relative_path)
# Initialize Pygame
pygame.init()
pygame.font.init()
pygame.mixer.init(64)
class Game:
"""The main Game class - everything happens here."""
# Screen size and colors
SCREEN_WIDTH, SCREEN_HEIGHT = 1200, 800
BG_COLOR = (0, 0, 0)
WHITE = (255, 255, 255)
GREEN = (0, 255, 0)
BLACK = (0, 0, 0)
# Game over screen sizes
GAME_OVER_SCREEN_WIDTH, GAME_OVER_SCREEN_HEIGHT = 1200, 800
# Timer font size
TIMER_FONT_SIZE = 80
HIGH_SCORE_FONT_SIZE = 160
# Constants to control game speed
BACKGROUND_SPEED = 3
ENEMY_SPEED = 2
CANISTER_SPEED = 6
PLAYER_ACCELERATION = 0.5
PLAYER_SPEED_MAX = 5
PLAYER_SPEED_MIN = -5
# Night day transition
HIGH_NOON_TIME = 20000 # More milliseconds, means longer day/night
TRANSITION_SPEED = 8000 # Less milliseconds, means faster transition
# Enemy Spawning timers
car_spawn_time = 3000
bike_spawn_time = 10000
pedestrian_spawn_time = 6000
CANISTER_SPAWN_TIME = 20000
# Wave Time
WAVE_TIME = TRANSITION_SPEED * 10
WAVE_DOWN_TIME = TRANSITION_SPEED * 2
# Resources
transition_images = []
enemy_images = []
cut_out_tree_images = []
# Default background
current_background = None
# Background Position X
bg_x = 0
# Default trees
current_trees = None
player_rect = None
# Initial speed in X and Y direction
player_speed_x = 0
player_speed_y = 0
player_acceleration = 0
# Start screen
start_screen_image = None
game_over_screen_image = None
# Fullscreen
is_fullscreen = True
def __init__(self):
# Get actual screen size of user
info = pygame.display.Info()
self.ACTUAL_SCREEN_WIDTH, self.ACTUAL_SCREEN_HEIGHT = info.current_w, info.current_h
# Initialize fonts
self.font_timer = pygame.font.Font(resource_path('fonts/Pixeltype.ttf'), self.TIMER_FONT_SIZE)
self.font_high_score = pygame.font.Font(resource_path('fonts/Pixeltype.ttf'), self.HIGH_SCORE_FONT_SIZE)
# High score (final timer string)
self.high_score = None
# Counter for increasing difficulty
self.difficulty_increase_counter = 0
# Create screen dimension-dependent units (1% of screen)
self.perc_H = self.ACTUAL_SCREEN_HEIGHT / 100
self.perc_W = self.ACTUAL_SCREEN_WIDTH / 100
# Background size
self.BACKGROUND_WIDTH = int(180 * self.perc_W)
self.BACKGROUND_HEIGHT = int(75 * self.perc_H)
# Player size
self.PLAYER_WIDTH = int(5.9 * self.perc_W)
self.PLAYER_HEIGHT = int(5 * self.perc_H)
# Enemy car sizes
self.ENEMY_WIDTH_CAR = int(5.9 * self.perc_W)
self.ENEMY_HEIGHT_CAR = int(5 * self.perc_H)
# Enemy bike sizes
self.BIKE_WIDTH = int(4.2 * self.perc_W)
self.BIKE_HEIGHT = int(3.4 * self.perc_H)
# Enemy pedestrian sizes
self.PEDESTRIAN_WIDTH = int(3.5 * self.perc_W)
self.PEDESTRIAN_HEIGHT = int(3 * self.perc_H)
# Define minimum and maximum car positions - invisible barriers, the player can not pass through
self.MIN_Y = self.ACTUAL_SCREEN_HEIGHT // 8 + int(3 * self.perc_H)
self.MAX_Y = (self.ACTUAL_SCREEN_HEIGHT // 8) * 7 - int(2 * self.perc_H)
self.MIN_X = 0
self.MAX_X = self.ACTUAL_SCREEN_WIDTH
# Define car lanes
self.car_lanes_fullscreen = [
self.ACTUAL_SCREEN_HEIGHT // 3 + int(2.5 * self.perc_H),
self.ACTUAL_SCREEN_HEIGHT // 3 + int(12 * self.perc_H),
self.ACTUAL_SCREEN_HEIGHT // 3 + int(21.5 * self.perc_H),
self.ACTUAL_SCREEN_HEIGHT // 3 + int(31 * self.perc_H)
]
# Define bike lanes
self.bike_lanes_fullscreen = [
self.ACTUAL_SCREEN_HEIGHT // 3 - int(6.5 * self.perc_H),
self.ACTUAL_SCREEN_HEIGHT // 3 + int(38 * self.perc_H)
]
# Define sidewalk lanes
self.side_walk_lanes = [
self.ACTUAL_SCREEN_HEIGHT // 3 - int(14.5 * self.perc_H),
self.ACTUAL_SCREEN_HEIGHT // 3 + int(45 * self.perc_H)
]
# Initialize lists for enemies, bikes, pedestrians
self.enemies = []
self.bikes = []
self.pedestrians = []
# Initialize variables for spawn timings
self.last_pedestrian_spawn_time = None
# Track game start time
self.start_time = pygame.time.get_ticks()
# Initialize wave status
self.wave = True
# Variables for night to day transition
self.transition_start_time = None
self.reverse_transition = False
# Initialize screen (windowed start screen)
self.screen = pygame.display.set_mode((self.SCREEN_WIDTH, self.SCREEN_HEIGHT), pygame.NOFRAME)
# Initialize game surface
self.game_surface = pygame.Surface((self.SCREEN_WIDTH, self.SCREEN_HEIGHT))
# Initialize buttons for various screens
self.start_button = Button(self.SCREEN_WIDTH // 2 + 110, self.SCREEN_HEIGHT // 2 + 180, "START", font_size=90)
self.quit_button_start_screen = Button(self.SCREEN_WIDTH // 2 - 18, self.SCREEN_HEIGHT // 2 + 60, "Quit", font_size=55)
self.quit_button = Button(50, 30, "Quit", font_size=40)
self.continue_button = Button(self.SCREEN_WIDTH // 2 + 310, self.SCREEN_HEIGHT // 2 + 110, "CONTINUE", font_size=70)
self.quit_button_game_over_screen = Button(self.SCREEN_WIDTH // 2 - 260, self.SCREEN_HEIGHT // 2 + 110, "QUIT", font_size=60)
# Set initial game state
self.state = self.start_screen
# Initialize time for spawning cars
self.last_spawn_time = pygame.time.get_ticks()
# Initialize list for collided enemies - for later version of game
self.enemies_collided = []
# Initialize game clock
self.clock = pygame.time.Clock()
# Initialize canister variables
self.remaining_lives = 3
self.last_canister_spawn_time = 0
self.canisters = []
# Load game resources
self.load_resources()
def load_resources(self):
"""
Loads and prepares all the necessary game resources.
This includes loading and scaling images for the canister, backgrounds, cut-out trees,
enemy cars, bikes, pedestrians, player car, start screen, and game over screen.
It also initializes the current background and tree images, and loads sounds (calls load_sound() method).
Author: Florian Goldbach, Christian Gerhold
"""
# Loading Sounds
self.load_sound()
# Loading Canister image
self.canister_image = pygame.image.load(resource_path("items/fuel.png")).convert_alpha() # Loading image
self.canister_image = pygame.transform.scale(self.canister_image, (int(2.5*self.perc_W), int(4.5*self.perc_H))) # Scaling
# Loading background images
self.transition_images = [
# .. and scale backgrounds.
pygame.transform.scale(
# .. rotate ..
pygame.transform.rotate(
# Load, ..
pygame.image.load(resource_path(f"backgrounds/day_to_night_transition_long_roads/background_2_day_to_night_{i}.png")).convert(),
90
),
(self.BACKGROUND_WIDTH, self.BACKGROUND_HEIGHT)
)
for i in range(1, 10)
]
# Loading cut out tree images
self.cut_out_tree_images = [
# .. and scale backgrounds.
pygame.transform.scale(
# .. rotate ..
pygame.transform.rotate(
# Load, ..
pygame.image.load(resource_path(f"trees/transparent_background_2_day_to_night_{i}.png")).convert_alpha(),
90
),
(self.BACKGROUND_WIDTH, self.BACKGROUND_HEIGHT)
)
for i in range(1, 10)
]
# Initializing current background and trees
self.current_background = self.transition_images[0]
self.current_trees = self.cut_out_tree_images[0]
# Loading enemy car images
self.enemy_images = [
# .. and scale enemy cars.
pygame.transform.scale(
# .. rotate
pygame.transform.rotate(
# Load, ..
pygame.image.load(resource_path(f"enemies/enemy{i}.png")).convert_alpha(),
90
),
(self.ENEMY_WIDTH_CAR, self.ENEMY_HEIGHT_CAR)
)
for i in range(1, 5) # We assume to have 4 enemy car images - this is subject to change when new cars are added
]
# Loading bike images
self.bike_animation_images = [
pygame.transform.scale(
pygame.transform.rotate(
pygame.image.load(resource_path(f"enemies/bike1_animation/bike1_animation_part{i}.png")).convert_alpha(),
90
),
(self.BIKE_WIDTH, self.BIKE_HEIGHT)
)
for i in range(1, 4) # 3 bike animation images
]
# Loading pedestrian images (1 and 2)
self.pedestrian1_animation_images = [
pygame.transform.scale(
pygame.transform.rotate(
pygame.image.load(resource_path(f"enemies/pedestrian1_animation/pedestrian1_{i}.png")).convert_alpha(),
90
),
(self.PEDESTRIAN_WIDTH, self.PEDESTRIAN_HEIGHT)
)
for i in range(1, 4) # 3 pedestrian animation images
]
self.pedestrian2_animation_images = [
pygame.transform.scale(
pygame.transform.rotate(
pygame.image.load(resource_path(f"enemies/pedestrian2_animation/pedestrian2_{i}.png")).convert_alpha(),
90
),
(self.PEDESTRIAN_WIDTH, self.PEDESTRIAN_HEIGHT)
)
for i in range(1, 4) # 3 pedestrian animation images
]
# This can be used to examplorarily understand how the lists enemy_images and transition_images are formed
self.player_image = pygame.image.load(resource_path("car2.png")).convert_alpha() # Loading image
self.player_image = pygame.transform.rotate(self.player_image, 90) # Rotating
self.player_image = pygame.transform.scale(self.player_image, (self.PLAYER_WIDTH, self.PLAYER_HEIGHT)) # Scaling
self.player_rect = self.player_image.get_rect()
# Preparing the start screen image
self.start_screen_image = pygame.image.load(resource_path("start_screen3.png")).convert()
self.start_screen_image = pygame.transform.scale(self.start_screen_image, (self.SCREEN_WIDTH, self.SCREEN_HEIGHT))
# Preparing the game over screen image
self.game_over_screen_image = pygame.image.load(resource_path("game_over_screen_image.png")).convert()
self.game_over_screen_image = pygame.transform.scale(self.game_over_screen_image, (self.GAME_OVER_SCREEN_WIDTH, self.GAME_OVER_SCREEN_HEIGHT))
def initialize_behaviour(self):
"""
Initialize the game behavior and set initial parameters.
This method sets up the game environment, including screen mode,
player's position, speed, acceleration, and initializes various game
elements such as enemies, bikes, pedestrians, and canisters. It also
initializes timers for bike, pedestrian, and fuel spawns and spawns
an initial car.
Authors: Florian Goldbach, Christian Gerhold
"""
# Starting in Fullscreen - Here you can decide in which mode to start
self.screen = pygame.display.set_mode(
(self.ACTUAL_SCREEN_WIDTH, self.ACTUAL_SCREEN_HEIGHT), pygame.FULLSCREEN
)
# self.screen = pygame.display.set_mode(
# (self.SCREEN_WIDTH, self.SCREEN_HEIGHT), pygame.NOFRAME
# )
self.is_fullscreen = True
self.player_rect.centerx = self.SCREEN_WIDTH // 4 # Change position on the X-axis
self.player_rect.centery = (
random.choice(self.car_lanes_fullscreen)
if self.is_fullscreen
else random.choice(self.car_lanes_windowed)
) # Change position on the Y-axis to a predefined lane
self.player_speed_x = 0 # Initial speed in the X-direction
self.player_speed_y = 0 # Initial speed in the Y-direction
self.player_acceleration = self.PLAYER_ACCELERATION # Acceleration
# Remove enemy cars and bikes
self.enemies = []
self.bikes = []
self.pedestrians = []
self.canisters = []
# Initializing self.last_bike_spawn_time for spawning bikes every x milliseconds
self.last_bike_spawn_time = pygame.time.get_ticks()
# Initializing self.last_pedestrian_spawn_time for spawning pedestrians every x milliseconds
self.last_pedestrian_spawn_time = pygame.time.get_ticks()
# Initializing self.last_fuel_spawn_time for spawning fuel every x milliseconds
self.last_fuel_spawn_time = pygame.time.get_ticks()
# Spawn an initial car
self.spawn_car()
def draw_level(self):
"""
Draws the current remaining lives on the screen.
Fills the screen with the background color (erases left over fuel containers)
and displays the fuel icons corresponding to the player's remaining lives.
The fuel icons are scaled to fit the screen dimensions and are placed on the correct position on screen.
Authors: Christian Gerhold, Florian Goldbach
"""
self.screen.fill(self.BG_COLOR)
scaled_width = int(2.5*self.perc_W)
scaled_height = int(4.5*self.perc_H)
distance_left = int(1.5*self.perc_W)
fuel_image = pygame.image.load(resource_path("./items/fuel.png")).convert_alpha()
scaled_image = pygame.transform.scale(fuel_image, (scaled_width, scaled_height))
# remaining lives
for i in range(self.remaining_lives):
self.screen.blit(scaled_image, (distance_left + i * (scaled_width + int(0.5*self.perc_W)), int(6*self.perc_H)))
def handle_canister_collision(self, canister):
"""
Handles the player's collision with a canister.
Increases the player's remaining lives by one, plays a sound effect,
and removes the canister from the game.
Authors: Florian Goldbach, Christian Gerhold
"""
print("Picked up canister!")
self.remaining_lives += 1 # add a canister/live
self.canister_sound.play()
self.canisters.remove(canister)
def spawn_canister(self):
new_canister = Canister(self.ACTUAL_SCREEN_WIDTH + int(4*self.perc_W), random.randint(self.MIN_Y, self.MAX_Y - int(3*self.perc_H)), random.choice([7, 8, 9]), self.canister_image)
self.canisters.append(new_canister)
def handle_canister_behaviour(self):
# Drawing, moving, collecting, removing canisters
for canister in self.canisters:
canister.draw(self.screen)
canister.move()
if self.player_rect.colliderect(canister.rect):
self.handle_canister_collision(canister)
# Remove canisters that are out of the screen
if canister.rect.right < 0:
self.canisters.remove(canister)
def handle_pedestrian_behaviour(self):
for pedestrian in self.pedestrians:
pedestrian.animate()
pedestrian.move()
pedestrian.draw(self.screen)
# Removing pedestrians
if pedestrian.rect.right < 0:
self.pedestrians.remove(pedestrian)
def fade_to_black(self, duration=2000):
"""
Fades the screen to black over a specified duration.
This is mostly used, so that the vroom sound can be heard :)
Author: Florian Goldbach
"""
fade_surface = pygame.Surface((self.screen.get_width(), self.screen.get_height()))
fade_surface.fill((0, 0, 0))
for alpha in range(0, 256//4):
fade_surface.set_alpha(alpha)
self.screen.blit(fade_surface, (0, 0))
pygame.display.update()
pygame.time.delay(duration // (256//4)) # Total duration divided by number of alpha increments
def will_collide(self, new_enemy_rect):
"""
Determines if the given rectangle will collide with any existing enemies.
This method checks if the specified rectangle (representing a new enemy)
intersects with any of the existing enemies' rectangles, considering an
additional buffer space around each enemy.
Args:
new_enemy_rect (pygame.Rect): The rectangle to check for potential collisions.
Returns:
bool: True if a collision is detected, False otherwise.
Author: Florian Goldbach
"""
buffer_space = 20 # 20 pixels buffer, adjustable
for enemy in self.enemies:
# Inflate the existing enemy rect by the buffer space
if enemy["rect"].inflate(buffer_space, 0).colliderect(new_enemy_rect):
return True
return False
def update_state_of_wave(self):
"""
Updates the state of the 'wave' in the game.
Tracks the elapsed time to toggle the 'wave' state between on and off.
The wave turns on after a set duration (WAVE_TIME + WAVE_DOWN_TIME) and
then turns off at the end of the WAVE_TIME. This creates a cycle of wave
states to add dynamics to the game.
Author: Florian Goldbach
"""
current_time = pygame.time.get_ticks()
# We initialize wave_cycle_start_time when the player presses the start button
elapsed_time = current_time - self.wave_cycle_start_time
# Once the wave time and wave down time is over, we reset wave_cycle_start_time -> infinite loop
# Also we reset wave to True
if elapsed_time >= self.WAVE_TIME + self.WAVE_DOWN_TIME:
self.wave = True
print("wave is on")
self.wave_cycle_start_time = current_time
# When we reach the end of the WAVE_TIME the wave is over
elif self.WAVE_TIME + 100 > elapsed_time >= self.WAVE_TIME:
self.wave = False
print("wave is off")
def spawn_car(self):
"""
Spawns a car enemy on the game screen.
Selects a random image for the enemy car and sets its initial position just outside the screen.
The car's vertical position is slightly randomized within the lane limits. The method attempts
to spawn the car without causing a collision, up to a maximum number of attempts.
Author: Florian Goldbach, Christian Gerhold
"""
enemy_image = random.choice(self.enemy_images)
enemy_rect = enemy_image.get_rect()
enemy_rect.centerx = self.ACTUAL_SCREEN_WIDTH if self.is_fullscreen else self.SCREEN_WIDTH
enemy_rect.centerx += int(4*self.perc_W) # Adding cars a bit outside of screen, so they drive in
attempts = 5 # Using a maximum number of attempts to avoid endless loop when screen is crowded
for _ in range(attempts):
# Slighty randomizing Y spawn position
enemy_rect.centery = random.choice(self.car_lanes_fullscreen) + random.randint(- int(2*self.perc_H), int(2*self.perc_H)) if self.is_fullscreen else random.choice(self.car_lanes_windowed)
if not self.will_collide(enemy_rect):
enemy_speed = self.ENEMY_SPEED
self.enemies.append({"image": enemy_image, "rect": enemy_rect, "speed": enemy_speed})
break
def load_canister_sound(self):
"""
Loads the canister pickup sound effect and sets its volume.
Author: Christian Gerhold
"""
self.canister_sound = pygame.mixer.Sound(resource_path("./sounds/canister.wav"))
self.canister_sound.set_volume(0.5)
def play_canister_sound(self):
"""
Plays the canister pickup sound effect on a specified audio channel.
Author: Christian Gerhold
"""
self.canister_sound.play()
pygame.mixer.Channel(5).play(self.canister_sound)
def load_bike_sound(self):
"""
Loads the bike spawn sound effect and sets its volume.
Author: Christian Gerhold
"""
# load bike sound
self.bike_sound = pygame.mixer.Sound(resource_path("./sounds/bike.wav"))
self.bike_sound.set_volume(0.1)
def play_bike_sound(self):
"""
Plays the bike spawn sound effect on a specified audio channel.
Author: Christian Gerhold
"""
self.bike_sound.play()
pygame.mixer.Channel(1).play(self.bike_sound)
def load_pedestrian_sound(self):
"""
Loads the pedestrian walking sound effect and sets its volume.
Author: Christian Gerhold.
"""
self.pedestrian_sound = pygame.mixer.Sound(resource_path("./sounds/walking.wav"))
self.pedestrian_sound.set_volume(0.2)
def play_pedestrian_sound(self):
"""
Plays the pedestrian walking sound effect on a specified audio channel.
Author: Christian Gerhold.
"""
self.pedestrian_sound.play()
pygame.mixer.Channel(1).play(self.pedestrian_sound)
def spawn_bike(self):
"""
Spawns a bike in the game.
Creates a new bike object with slightly off the right of the screen
and within the designated bike lanes. Adds the new bike to the list of bikes in the game
and plays the bike spawning sound effect.
Author: Florian Goldbach, Christian Gerhold
"""
# Also slighty randomizing Y spawn position and speed
new_bike = Bike(self.ACTUAL_SCREEN_WIDTH + int(4*self.perc_W), random.choice(self.bike_lanes_fullscreen) + random.randint(-int(0.7*self.perc_H), int(0.7*self.perc_H)), random.randint(4, 6), self.bike_animation_images)
self.bikes.append(new_bike)
# sound for spawning bike
self.play_bike_sound() # Aufruf des bike spawn sounds
def spawn_pedestrian(self):
"""
Spawns a pedestrian in the game.
Randomly selects one of the pedestrian types and creates a new pedestrian object.
The pedestrian is positioned slightly off the right of the screen with a randomized
vertical position. Adds the new pedestrian to the list of pedestrians and plays
the pedestrian sound effect.
Author: Florian Goldbach, Christian Gerhold
"""
# Randomly choose between the two types of pedestrians
chosen_pedestrian_images = random.choice([self.pedestrian1_animation_images, self.pedestrian2_animation_images])
# Also slighty randomizing Y spawn position and speed
new_pedestrian = Pedestrian(self.ACTUAL_SCREEN_WIDTH + int(4*self.perc_W), random.choice(self.side_walk_lanes) + random.randint(-int(1.5*self.perc_H), int(1.5*self.perc_H)), random.choice([3.2, 3.3, 3.5]), chosen_pedestrian_images)
self.pedestrians.append(new_pedestrian)
self.play_pedestrian_sound() # walking sound with spawning a pedestrian
def handle_collision(self, collided_with):
"""
Handles collisions in the game.
Updates the high score based on the current game time (in case game ends after this collision).
It then plays the collision sound effect, stops the game soundtrack,
and decrements the player's remaining lives. Also records the collided entity for potential future use.
Author: Florian Goldbach
"""
self.high_score = self.get_timer_string()
self.play_collision()
self.stop_soundtrack()
print("GAME OVER")
# Reduce lives
self.remaining_lives -= 1
# List of collided enemies - could be interesting for info at the end of the game - not implemented as of now
self.enemies_collided.append(collided_with)
def get_timer_string(self):
"""
Calculates and formats the elapsed game time into a string.
Computes the elapsed time since the game's start and converts it into
hours, minutes, and seconds. The time is then formatted into a string
in the format 'HH:MM:SS'.
Returns:
str: Formatted time string representing the elapsed time.
Author: Florian Goldbach
"""
elapsed_time = pygame.time.get_ticks() - self.timer_start_time
seconds = elapsed_time // 1000
minutes = seconds // 60
hours = minutes // 60
seconds %= 60
minutes %= 60
return f"{hours:02}:{minutes:02}:{seconds:02}"
def increase_difficulty(self):
"""
Increases game difficulty every minute for the first ten minutes. Then at 15 and 20 min.
Each difficulty increase, spawn times for cars, pedestrians, and bikes are reduced.
`difficulty_increase_counter` ensures each difficulty level is only applied once.
There is an opportunity to increase the difficulty more general and algorithmically.
But for now it has this customized style.
Author: Florian Goldbach
"""
elapsed_time = pygame.time.get_ticks() - self.timer_start_time
minutes = elapsed_time // 60000
if minutes >= 1 and self.difficulty_increase_counter == 0:
self.car_spawn_time -= 500
self.pedestrian_spawn_time -= 1000
self.bike_spawn_time -= 1000
self.difficulty_increase_counter +=1
if minutes >= 2 and self.difficulty_increase_counter == 1:
self.car_spawn_time -= 500
self.pedestrian_spawn_time -= 1000
self.bike_spawn_time -= 1000
self.difficulty_increase_counter +=1
if minutes >= 3 and self.difficulty_increase_counter == 2:
self.pedestrian_spawn_time -= 1000
self.bike_spawn_time -= 1000
self.difficulty_increase_counter +=1
if minutes >= 4 and self.difficulty_increase_counter == 3:
self.car_spawn_time -= 500
self.pedestrian_spawn_time -= 1000
self.bike_spawn_time -= 1000
self.difficulty_increase_counter +=1
if minutes >= 5 and self.difficulty_increase_counter == 4:
self.car_spawn_time -= 500
self.pedestrian_spawn_time -= 1000
self.bike_spawn_time -= 1000
self.difficulty_increase_counter +=1
if minutes >= 6 and self.difficulty_increase_counter == 5:
self.car_spawn_time -= 200
self.bike_spawn_time -= 500
self.difficulty_increase_counter +=1
if minutes >= 7 and self.difficulty_increase_counter == 6:
self.car_spawn_time -= 100
self.pedestrian_spawn_time -= 200
self.bike_spawn_time -= 500
self.difficulty_increase_counter +=1
if minutes >= 8 and self.difficulty_increase_counter == 7:
self.car_spawn_time -= 100
self.bike_spawn_time -= 500
self.difficulty_increase_counter +=1
if minutes >= 9 and self.difficulty_increase_counter == 8:
self.car_spawn_time -= 100
self.bike_spawn_time -= 500
self.difficulty_increase_counter +=1
if minutes >= 10 and self.difficulty_increase_counter == 9:
self.car_spawn_time -= 100
self.pedestrian_spawn_time -= 500
self.bike_spawn_time -= 500
self.difficulty_increase_counter +=1
if minutes >= 15 and self.difficulty_increase_counter == 10:
self.pedestrian_spawn_time -= 200
self.bike_spawn_time -= 500
self.difficulty_increase_counter +=1
if minutes >= 20 and self.difficulty_increase_counter == 11:
self.pedestrian_spawn_time -= 200
self.bike_spawn_time -= 500
self.difficulty_increase_counter +=1
def display_timer(self):
"""
Displays the current game timer on the screen.
Retrieves the formatted game time as a string and renders it on the screen
using the designated font and position.
Author: Florian Goldbach
"""
timer_string = self.get_timer_string()
timer_surface = self.font_timer.render(timer_string, True, self.WHITE)
self.screen.blit(timer_surface, (int(85*self.perc_W), int(7*self.perc_H)))
def display_high_score(self):
"""
Displays the high score on the screen.
Renders the high score, stored as a string, on the screen with the specified
font and at a designated position.
Author: Florian Goldbach
"""
timer_surface = self.font_high_score.render(self.high_score, True, self.BLACK)
self.screen.blit(timer_surface, (int(20*self.perc_W), int(7*self.perc_H)))
def is_game_over(self):
"""
Checks and handles the game over condition.
If the player's remaining lives are depleted, it switches the game screen to the
game over screen and updates the game state.
Author: Florian Goldbach
"""
if self.remaining_lives < 1:
self.screen = pygame.display.set_mode((self.GAME_OVER_SCREEN_WIDTH, self.GAME_OVER_SCREEN_HEIGHT), pygame.NOFRAME)
self.is_fullscreen = False
self.state = self.game_over_screen
return
def change_to_start_screen(self):
"""
Transitions the game to the start screen.
Changes the display mode to windowed (non-fullscreen) and updates the game state
to show the start screen.
Author: Florian Goldbach
"""
# Change display mode, set is_fullscreen to False and update game state
self.screen = pygame.display.set_mode((self.SCREEN_WIDTH, self.SCREEN_HEIGHT), pygame.NOFRAME)
self.is_fullscreen = False
self.state = self.start_screen
def night_day_transition(self):
"""
Manages the transition between day and night in the game.
The method tracks elapsed time to initiate a day-to-night and night-to-day transition.
It changes background images at regular intervals to create a visual transition effect.
The transition starts after a predefined 'HIGH_NOON_TIME' and proceeds in a forward or
reverse sequence, based on the elapsed time and whether the reverse transition has been triggered.
The process ensures a continuous day-night cycle in the game's background environment.
Author: Florian Goldbach
"""
# Keeping track of time
elapsed_time = pygame.time.get_ticks() - self.start_time
# After "HIGH_NOON_TIME" milliseconds of daytime driving, we start the transition
if self.HIGH_NOON_TIME <= elapsed_time and self.transition_start_time is None:
self.transition_start_time = pygame.time.get_ticks()
# We keep track of when the transition started
if self.transition_start_time:
time_since_transition_start = pygame.time.get_ticks() - self.transition_start_time
# While we are not in a reverse transition (night to day), we change the background image every TRANSITION_SPEED miliseconds
if not self.reverse_transition:
transition_index = time_since_transition_start // self.TRANSITION_SPEED # e.g. every 5 seconds: time_since_transition_start // 5000
# We only change the background image, when it is fully on screen ((SCREEN_WIDTH - BACKGROUND_WIDTH) < bg_x <= 0)
# and while we still have new transition images in our list
if transition_index < len(self.transition_images) and (self.ACTUAL_SCREEN_WIDTH - self.BACKGROUND_WIDTH) < self.bg_x <= 0:
self.current_background = self.transition_images[transition_index]
self.current_trees = self.cut_out_tree_images[transition_index]
# Once we run out of transition images, we enter the reverse_transition
elif transition_index >= len(self.transition_images):
self.reverse_transition = True
self.transition_start_time = pygame.time.get_ticks()
# We keep time of when the reverse transition started and make sure the transition_index counts reversly (e.g. 7 to 0)
if self.reverse_transition:
time_since_reverse_transition = pygame.time.get_ticks() - self.transition_start_time
transition_index = len(self.transition_images) - 1 - time_since_reverse_transition // self.TRANSITION_SPEED
# We only change the background image, when it is fully on screen ((SCREEN_WIDTH - BACKGROUND_WIDTH) < bg_x <= 0)
# and while we still have new transition images in our list
if 0 <= transition_index < len(self.transition_images) and (self.ACTUAL_SCREEN_WIDTH - self.BACKGROUND_WIDTH) < self.bg_x <= 0:
self.current_background = self.transition_images[transition_index]
self.current_trees = self.cut_out_tree_images[transition_index]
# Once we ran out of transitin images, we reset for the next loop
elif transition_index < 0:
self.start_time = pygame.time.get_ticks()
self.transition_start_time = None
self.reverse_transition = False
def update_player_position(self):
"""
Updates the player's position based on their current speed.
Adjusts the player's vertical (y-axis) and horizontal (x-axis) position
according to their current speed. It also ensures the player's car
remains within the defined screen boundaries, preventing it from moving
off-screen.
Authors: Christian Gerhold, Florian Goldbach
"""
# Movement of player based on speed (y-axis)
self.player_rect.centery += self.player_speed_y
# Check and adjust if the player car is out of the bounds:
if self.player_rect.top < self.MIN_Y:
self.player_rect.top = self.MIN_Y
elif self.player_rect.bottom > self.MAX_Y:
self.player_rect.bottom = self.MAX_Y
# Movement of player based on speed (x-axis)
self.player_rect.centerx += self.player_speed_x
# Check and adjust if the player car is out of the x-bounds:
if self.player_rect.left < self.MIN_X:
self.player_rect.left = self.MIN_X
elif self.player_rect.right > self.MAX_X:
self.player_rect.right = self.MAX_X
# Start Screen sound
def load_sound(self):
"""
Loads and sets the volume for various sound effects used in the game.
This includes sounds for the game over screen, start screen, start and quit buttons,
general game soundtrack, collision effects, car engine (vroom sound), and screams for pedestrian collisions.
Each sound is loaded from a file and its volume is set accordingly.
Author: Christian Gerhold, Florian Goldbach
"""
# load game over screen sound
self.game_over_screen_sound = pygame.mixer.Sound(resource_path("./sounds/game_over.wav"))
self.game_over_screen_sound.set_volume(1)
# load start screen sound
self.start_screen_sound = pygame.mixer.Sound(resource_path("./sounds/start_screen.wav"))
self.start_screen_sound.set_volume(0.2)
# load start button sound
self.start_button_sound = pygame.mixer.Sound(resource_path("./sounds/button_start_sound.wav"))
self.start_button_sound.set_volume(0.5)
# load quit button sound
self.quit_button_sound = pygame.mixer.Sound(resource_path("./sounds/button_quit_sound.wav"))
self.quit_button_sound.set_volume(0.5)
# load soundtrack file
self.soundtrack = pygame.mixer.Sound(resource_path("./sounds/soundtrack.wav"))
self.soundtrack.set_volume(0.3)
# load collision file
self.collision = pygame.mixer.Sound(resource_path("./sounds/collision.wav"))
self.collision.set_volume(0.5)
# load vroom file
self.vroom = pygame.mixer.Sound(resource_path("./sounds/vroom.wav"))
self.vroom.set_volume(0.5)
# load scream for pedestrian when being hit
self.scream = pygame.mixer.Sound(resource_path("./sounds/scream.wav"))
self.scream.set_volume(0.5)
# load scream for pedestrian when being hit
self.scream2 = pygame.mixer.Sound(resource_path("./sounds/scream2.mp3"))
self.scream2.set_volume(0.5)
# start game over sound
def play_game_over_screen_sound(self):
"""
Plays the game over screen sound effect on a loop.
This sound effect is played continuously when the game over screen is active.
Author: Ghristian Gerhold
"""
pygame.mixer.Channel(6).play(self.game_over_screen_sound, loops=-1) # plays forever as long as being stuck in the game over screen
# start_screen sound play
def play_soundtrack(self):
"""
Plays the game's main soundtrack on a loop.
This background music is set to play continuously throughout the game.
Author: Ghristian Gerhold
"""
pygame.mixer.Channel(5).play(self.soundtrack, loops=-1) # sound plays forever // extra channel
# stop soundtrack
def stop_soundtrack(self):