MXVK Vulkan Framework 0.35.0
C++20 Vulkan rendering framework for practical 2D and 3D application development with SDL3.
Loading...
Searching...
No Matches
asteroids.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2## @file asteroids.py
3## @brief Interactive MXVK Asteroids game implemented in Python.
4## @details Demonstrates a Python @c VK_Window subclass, NumPy-backed sprite
5## rendering, keyboard input, particles, collision handling, and a fixed-step
6## arcade game loop. The game renders to a 640-by-360 logical canvas before it
7## is scaled to the current swapchain extent.
8##
9## @section asteroids_run Running the example
10## @code{.sh}
11## python3 python-examples/asteroids/asteroids.py --width 1280 --height 720 --vsync
12## @endcode
13## @section asteroids_controls Controls
14## - Left/Right: rotate the ship.
15## - Up: thrust.
16## - Space: fire; after game over, start a new game.
17## - Escape: exit.
18## @section asteroids_design Design notes
19## @c AsteroidsWindow owns gameplay state, while the @c Ship, @c Projectile,
20## @c Asteroid, and @c Particle data classes remain renderer-independent. NumPy
21## supplies the dynamic pixel buffers uploaded to the MXVK sprites each frame.
22
23import argparse
24import math
25import random
26import time
27from dataclasses import dataclass, field
28from pathlib import Path
29
30import numpy as np
31
32import mxvk_ext as mxvk
33
34
35GAME_W = 640
36GAME_H = 360
37STAR_COUNT = 800
38MAX_PROJECTILES = 50
39MAX_ASTEROIDS = 30
40MAX_PARTICLES = 40
41ASTEROID_VERTICES = 8
42PROJECTILE_SPEED = 5.0
43PROJECTILE_LIFETIME = 60
44FIRE_COOLDOWN = 5
45SHOTS_PER_BURST = 5
46FIRE_DELAY = 3
47EXPLOSION_DURATION = 90
48MIN_ASTEROID_RADIUS = 8.0
49LARGE_ASTEROID_POINTS = 20
50MEDIUM_ASTEROID_POINTS = 50
51SMALL_ASTEROID_POINTS = 100
52TARGET_FRAME_TIME = 1.0 / 60.0
53
54EXAMPLE_DIR = Path(__file__).resolve().parent
55DATA_DIR = EXAMPLE_DIR / "data"
56
57
58@dataclass
59class Ship:
60 x: float = GAME_W / 2
61 y: float = GAME_H / 2
62 vx: float = 0.0
63 vy: float = 0.0
64 angle: float = 0.0
65 lives: int = 3
66 fire_cooldown: int = 0
67 burst_count: int = 0
68 exploding: bool = False
69 explosion_timer: int = 0
70 score: int = 0
71 continuous_fire_timer: int = 0
72 overheated: bool = False
73 overheat_cooldown: int = 0
74
75
76@dataclass
78 x: float = 0.0
79 y: float = 0.0
80 vx: float = 0.0
81 vy: float = 0.0
82 lifetime: int = 0
83 active: bool = False
84
85
86@dataclass
88 x: float = 0.0
89 y: float = 0.0
90 vx: float = 0.0
91 vy: float = 0.0
92 radius: float = 0.0
93 active: bool = False
94 vertices: list[tuple[float, float]] = field(default_factory=list)
95 rotation_angle: float = 0.0
96 rotation_speed: float = 0.0
97
98
99@dataclass
101 x: float = 0.0
102 y: float = 0.0
103 vx: float = 0.0
104 vy: float = 0.0
105 lifetime: int = 0
106 active: bool = False
107
108
109@dataclass
110class Star:
111 x: float
112 y: float
113 speed: float
114 size: float
115 brightness: float
116 layer: int
117 red: float = 1.0
118 green: float = 1.0
119 blue: float = 1.0
120 twinkle_phase: float = 0.0
121 twinkle_speed: float = 1.0
122
123
125 def __init__(self, width: int, height: int, fullscreen: bool, enable_vsync: bool) -> None:
126 super().__init__("-[ SpaceRox - MXVK ]-", width, height, fullscreen, False, enable_vsync)
127 self.fallback_width = width
128 self.fallback_height = height
129 self.width = width
130 self.height = height
132 self.last_frame_time = time.monotonic()
133 self.state = "countdown"
135 self.key_left = False
136 self.key_right = False
137 self.key_thrust = False
138 self.key_fire = False
139 self.was_exploding = False
140 self.ship = Ship()
141 self.projectiles = [Projectile() for _ in range(MAX_PROJECTILES)]
142 self.asteroids = [Asteroid() for _ in range(MAX_ASTEROIDS)]
143 self.particles = [Particle() for _ in range(MAX_PARTICLES)]
144 self.stars: list[Star] = []
145 self.set_font(str(DATA_DIR / "font.ttf"), 20)
146 self.star_sprite = self.create_sprite(4, 4, str(DATA_DIR / "sprite_vert.spv"), str(DATA_DIR / "sprite_frag.spv"))
147 self.pixel = self.create_sprite(2, 2, str(DATA_DIR / "sprite_vert.spv"), str(DATA_DIR / "solid_frag.spv"))
148 self.star_sprite.update_texture(self.make_star_pixels(), 4, 4, 16)
149 self.pixel.update_texture(np.full((2, 2, 4), 255, dtype=np.uint8), 2, 2, 8)
150 self.init_game()
151
152 def close(self) -> None:
153 self.star_sprite = None
154 self.pixel = None
155 self.release()
156
157 def make_star_pixels(self) -> np.ndarray:
158 pixels = np.zeros((4, 4, 4), dtype=np.uint8)
159 for y in range(4):
160 for x in range(4):
161 distance = math.hypot((x - 1.5) / 1.5, (y - 1.5) / 1.5)
162 pixels[y, x] = (255, 255, 255, max(0, int(255 * (1.0 - distance))))
163 return pixels
164
165 def sx(self) -> float:
166 return self.width / GAME_W
167
168 def sy(self) -> float:
169 return self.height / GAME_H
170
171 def to_x(self, value: float) -> int:
172 return int(value * self.sx())
173
174 def to_y(self, value: float) -> int:
175 return int(value * self.sy())
176
177 def to_w(self, value: float) -> int:
178 return max(1, int(value * self.sx()))
179
180 def to_h(self, value: float) -> int:
181 return max(1, int(value * self.sy()))
182
183 def refresh_size(self) -> None:
184 width, height = self.swapchain_extent
185 self.width = width if width > 0 else self.fallback_width
186 self.height = height if height > 0 else self.fallback_height
187
188 def update_font_size(self) -> None:
189 font_size = max(14, min(128, int(20.0 * self.height / 480.0)))
190 if font_size != self.last_font_size:
191 self.last_font_size = font_size
192 self.set_font(str(DATA_DIR / "font.ttf"), font_size)
193 self.clear_text_queue()
194
195 def init_game(self) -> None:
196 self.ship = Ship()
197 self.projectiles = [Projectile() for _ in range(MAX_PROJECTILES)]
198 self.asteroids = [Asteroid() for _ in range(MAX_ASTEROIDS)]
199 self.particles = [Particle() for _ in range(MAX_PARTICLES)]
200 for _ in range(4):
201 x, y = self.random_safe_position()
202 self.spawn_asteroid(x, y, random.uniform(-1.0, 1.0), random.uniform(-1.0, 1.0), random.uniform(20.0, 39.0))
203 self.init_stars()
204 self.state = "countdown"
205 self.state_started = time.monotonic()
206 self.was_exploding = False
207
208 def random_safe_position(self) -> tuple[float, float]:
209 while True:
210 x, y = random.uniform(0, GAME_W), random.uniform(0, GAME_H)
211 if math.hypot(x - self.ship.x, y - self.ship.y) >= 100:
212 return x, y
213
214 def init_stars(self) -> None:
215 self.stars = []
216 for layer in range(3):
217 for _ in range(STAR_COUNT):
218 speed_range = ((0.15, 0.55), (0.4, 1.2), (0.8, 2.6))[layer]
219 size_range = ((0.8, 1.4), (1.2, 2.1), (1.8, 3.05))[layer]
220 brightness_range = ((0.25, 0.7), (0.45, 1.0), (0.65, 1.0))[layer]
221 star = Star(random.uniform(0, GAME_W), random.uniform(0, GAME_H), random.uniform(*speed_range), random.uniform(*size_range), random.uniform(*brightness_range), layer, twinkle_phase=random.uniform(0, math.tau), twinkle_speed=random.uniform(0.4 + layer * 0.4, 1.9 + layer * 1.5))
222 self.assign_star_color(star)
223 self.stars.append(star)
224
225 def assign_star_color(self, star: Star) -> None:
226 roll = random.random()
227 if roll < 0.35:
228 star.red, star.green, star.blue = random.uniform(0.85, 1.0), random.uniform(0.9, 1.0), 1.0
229 elif roll < 0.55:
230 star.red, star.green, star.blue, star.size, star.brightness = random.uniform(0.5, 0.7), random.uniform(0.75, 0.9), 1.0, star.size * 1.15, star.brightness * 1.1
231 elif roll < 0.70:
232 star.red, star.green, star.blue, star.size = 1.0, random.uniform(0.95, 1.0), random.uniform(0.6, 0.9), star.size * 1.05
233 elif roll < 0.90:
234 star.red, star.green, star.blue, star.size = 1.0, random.uniform(0.3, 0.8), random.uniform(0.2, 0.4), star.size * 1.1
235 else:
236 star.red, star.green, star.blue, star.size, star.brightness = 0.9, 0.95, 1.0, star.size * 1.5, star.brightness * 1.4
237
238 def set_color(self, red: float, green: float, blue: float, alpha: float = 1.0) -> None:
239 self.pixel.set_shader_params(red, green, blue, alpha)
240
241 def draw_rect(self, x: float, y: float, width: float, height: float) -> None:
242 self.pixel.draw_rect(self.to_x(x), self.to_y(y), self.to_w(width), self.to_h(height))
243
244 def draw_line(self, x0: float, y0: float, x1: float, y1: float) -> None:
245 x0, y0, x1, y1 = self.to_x(x0), self.to_y(y0), self.to_x(x1), self.to_y(y1)
246 dx, dy = abs(x1 - x0), -abs(y1 - y0)
247 step_x, step_y = (1 if x0 < x1 else -1), (1 if y0 < y1 else -1)
248 error = dx + dy
249 size = max(1, int(1.5 * min(self.sx(), self.sy())))
250 while True:
251 self.pixel.draw_rect(x0, y0, size, size)
252 if x0 == x1 and y0 == y1:
253 return
254 twice_error = 2 * error
255 if twice_error >= dy:
256 error += dy
257 x0 += step_x
258 if twice_error <= dx:
259 error += dx
260 y0 += step_y
261
262 def spawn_asteroid(self, x: float, y: float, vx: float, vy: float, radius: float) -> None:
263 asteroid = next((value for value in self.asteroids if not value.active), None)
264 if asteroid is None:
265 return
266 asteroid.x, asteroid.y, asteroid.vx, asteroid.vy, asteroid.radius, asteroid.active = x, y, vx, vy, radius, True
267 asteroid.rotation_angle, asteroid.rotation_speed = 0.0, random.uniform(-0.05, 0.05)
268 asteroid.vertices = []
269 for index in range(ASTEROID_VERTICES):
270 angle = index * math.tau / ASTEROID_VERTICES
271 vertex_radius = radius * random.uniform(0.7, 0.99)
272 asteroid.vertices.append((math.cos(angle) * vertex_radius, math.sin(angle) * vertex_radius))
273
274 def update_ship(self) -> None:
275 if self.ship.exploding:
276 return
277 if self.key_left:
278 self.ship.angle -= 0.15
279 if self.key_right:
280 self.ship.angle += 0.15
281 if self.key_thrust:
282 self.ship.vx += math.sin(self.ship.angle) * 0.2
283 self.ship.vy -= math.cos(self.ship.angle) * 0.2
284 speed = math.hypot(self.ship.vx, self.ship.vy)
285 if speed > 4.0:
286 self.ship.vx, self.ship.vy = self.ship.vx * 4.0 / speed, self.ship.vy * 4.0 / speed
287 self.ship.vx *= 0.98
288 self.ship.vy *= 0.98
289 self.ship.x = (self.ship.x + self.ship.vx) % GAME_W
290 self.ship.y = (self.ship.y + self.ship.vy) % GAME_H
291 self.ship.fire_cooldown = max(0, self.ship.fire_cooldown - 1)
292
293 def can_fire(self) -> bool:
294 if self.ship.overheated or self.ship.fire_cooldown > 0:
295 return False
296 if self.ship.burst_count >= SHOTS_PER_BURST:
297 self.ship.fire_cooldown, self.ship.burst_count = FIRE_COOLDOWN, 0
298 return False
299 self.ship.fire_cooldown += FIRE_DELAY
300 self.ship.burst_count += 1
301 return True
302
303 def fire_projectile(self) -> None:
304 projectile = next((value for value in self.projectiles if not value.active), None)
305 if projectile is None:
306 return
307 projectile.x = self.ship.x + math.sin(self.ship.angle) * 12
308 projectile.y = self.ship.y - math.cos(self.ship.angle) * 12
309 projectile.vx = math.sin(self.ship.angle) * PROJECTILE_SPEED
310 projectile.vy = -math.cos(self.ship.angle) * PROJECTILE_SPEED
311 projectile.lifetime, projectile.active = PROJECTILE_LIFETIME, True
312
313 def update_fire(self) -> None:
314 if self.key_fire and self.can_fire():
315 self.fire_projectile()
316 if self.key_fire and not self.ship.overheated:
317 self.ship.continuous_fire_timer += 1
318 if self.ship.continuous_fire_timer >= 180:
319 self.ship.overheated, self.ship.continuous_fire_timer, self.ship.burst_count = True, 0, 0
320 elif self.ship.overheated:
321 self.ship.overheat_cooldown += 1
322 if self.ship.overheat_cooldown >= 180:
323 self.ship.overheated, self.ship.overheat_cooldown = False, 0
324 else:
325 self.ship.continuous_fire_timer = max(0, self.ship.continuous_fire_timer - 1)
326
327 def update_projectiles(self) -> None:
328 for projectile in self.projectiles:
329 if projectile.active:
330 projectile.x += projectile.vx
331 projectile.y += projectile.vy
332 projectile.lifetime -= 1
333 if projectile.lifetime <= 0 or not (0 <= projectile.x < GAME_W and 0 <= projectile.y < GAME_H):
334 projectile.active = False
335
336 def update_asteroids(self) -> None:
337 for asteroid in self.asteroids:
338 if asteroid.active:
339 asteroid.x = (asteroid.x + asteroid.vx) % GAME_W
340 asteroid.y = (asteroid.y + asteroid.vy) % GAME_H
341 asteroid.rotation_angle += asteroid.rotation_speed
342
343 def split_asteroid(self, asteroid: Asteroid) -> None:
344 radius = asteroid.radius / 2.0
345 if radius < MIN_ASTEROID_RADIUS:
346 self.create_explosion(asteroid.x, asteroid.y)
347 self.ship.score += SMALL_ASTEROID_POINTS
348 asteroid.active = False
349 return
350 for _ in range(random.randint(2, 3)):
351 angle, speed = random.uniform(0, math.tau), random.uniform(0.5, 1.9)
352 self.spawn_asteroid(asteroid.x + math.cos(angle) * radius * 0.5, asteroid.y + math.sin(angle) * radius * 0.5, math.cos(angle) * speed + asteroid.vx * 0.3, math.sin(angle) * speed + asteroid.vy * 0.3, radius)
353 self.ship.score += LARGE_ASTEROID_POINTS if radius >= 25.0 else MEDIUM_ASTEROID_POINTS
354 self.create_explosion(asteroid.x, asteroid.y)
355 asteroid.active = False
356
357 def create_explosion(self, x: float, y: float) -> None:
358 for particle in [value for value in self.particles if not value.active][:random.randint(15, 24)]:
359 angle, speed = random.uniform(0, math.tau), random.uniform(0.5, 3.0)
360 particle.x, particle.y, particle.vx, particle.vy, particle.lifetime, particle.active = x + random.uniform(-8, 8), y + random.uniform(-8, 8), math.cos(angle) * speed, math.sin(angle) * speed, random.randint(20, 60), True
361
362 def update_particles(self) -> None:
363 for particle in self.particles:
364 if particle.active:
365 particle.x = (particle.x + particle.vx) % GAME_W
366 particle.y = (particle.y + particle.vy) % GAME_H
367 particle.vx *= 0.97
368 particle.vy *= 0.97
369 particle.lifetime -= 1
370 if particle.lifetime <= 0:
371 particle.active = False
372
373 def check_collisions(self) -> None:
375 for projectile in self.projectiles:
376 if not projectile.active:
377 continue
378 for asteroid in self.asteroids:
379 if asteroid.active and math.hypot(projectile.x - asteroid.x, projectile.y - asteroid.y) < asteroid.radius:
380 projectile.active = False
381 if asteroid.radius >= 20.0:
382 self.ship.score += LARGE_ASTEROID_POINTS
383 elif asteroid.radius >= MIN_ASTEROID_RADIUS:
384 self.ship.score += MEDIUM_ASTEROID_POINTS
385 else:
386 self.ship.score += SMALL_ASTEROID_POINTS
387 self.split_asteroid(asteroid)
388 break
389 if self.ship.exploding:
390 return
391 for asteroid in self.asteroids:
392 if asteroid.active and math.hypot(self.ship.x - asteroid.x, self.ship.y - asteroid.y) < asteroid.radius + 8:
393 self.ship.exploding, self.ship.explosion_timer, self.ship.lives = True, EXPLOSION_DURATION, self.ship.lives - 1
394 self.create_explosion(self.ship.x, self.ship.y)
395 break
396
397 def check_asteroid_collisions(self) -> None:
398 active_asteroids = [asteroid for asteroid in self.asteroids if asteroid.active]
399 for index, first in enumerate(active_asteroids):
400 for second in active_asteroids[index + 1:]:
401 difference_x, difference_y = first.x - second.x, first.y - second.y
402 distance = math.hypot(difference_x, difference_y)
403 minimum_distance = first.radius + second.radius
404 if distance <= 0.0 or distance >= minimum_distance:
405 continue
406 normal_x, normal_y = difference_x / distance, difference_y / distance
407 overlap = (minimum_distance - distance) * 0.5
408 first.x, first.y = first.x + normal_x * overlap, first.y + normal_y * overlap
409 second.x, second.y = second.x - normal_x * overlap, second.y - normal_y * overlap
410 velocity_along_normal = (first.vx - second.vx) * normal_x + (first.vy - second.vy) * normal_y
411 if velocity_along_normal > 0.0:
412 continue
413 impulse = -(1.0 + 0.8) * velocity_along_normal * 0.5
414 first.vx, first.vy = first.vx + impulse * normal_x, first.vy + impulse * normal_y
415 second.vx, second.vy = second.vx - impulse * normal_x, second.vy - impulse * normal_y
416
417 def maintain_asteroids(self) -> None:
418 while sum(value.active for value in self.asteroids) < 3:
419 edge = random.randrange(4)
420 x, y = ((-30, random.uniform(0, GAME_H)), (GAME_W + 30, random.uniform(0, GAME_H)), (random.uniform(0, GAME_W), -30), (random.uniform(0, GAME_W), GAME_H + 30))[edge]
421 self.spawn_asteroid(x, y, random.uniform(-1, 1), random.uniform(-1, 1), random.choice((random.uniform(12, 20), random.uniform(20, 30), random.uniform(30, 45))))
422
423 def update_death(self) -> None:
424 if self.ship.exploding:
425 self.ship.explosion_timer -= 1
426 self.was_exploding = True
427 if self.ship.explosion_timer <= 0:
428 self.ship.exploding = False
429 elif self.was_exploding:
430 self.was_exploding = False
431 if self.ship.lives <= 0:
432 self.state = "gameover"
433 else:
434 self.ship.x, self.ship.y, self.ship.vx, self.ship.vy, self.ship.angle = GAME_W / 2, GAME_H / 2, 0.0, 0.0, 0.0
435 self.key_left, self.key_right, self.key_thrust, self.key_fire = False, False, False, False
436 self.state, self.state_started = "countdown", time.monotonic()
437
438 def draw_stars(self, speed_multiplier: float) -> None:
439 for star in self.stars:
440 star.y += star.speed * speed_multiplier
441 if star.y >= GAME_H:
442 star.x, star.y = random.uniform(0, GAME_W), 0.0
443 star.twinkle_phase += star.twinkle_speed / 60.0
444 brightness = star.brightness * (0.7 + 0.3 * math.sin(star.twinkle_phase))
445 self.star_sprite.set_shader_params(star.red * brightness, star.green * brightness, star.blue * brightness, brightness)
446 size = max(2, int(star.size * min(self.sx(), self.sy())))
447 self.star_sprite.draw_rect(self.to_x(star.x), self.to_y(star.y), size, size)
448
449 def draw_ship(self, x: float, y: float) -> None:
450 if self.ship.exploding:
451 return
452 cosine, sine = math.cos(self.ship.angle), math.sin(self.ship.angle)
453 def rotate(local_x: float, local_y: float) -> tuple[float, float]: return x + local_x * cosine - local_y * sine, y + local_x * sine + local_y * cosine
454 nose, left, right, left_engine, right_engine, center = rotate(0, -15), rotate(-12, 8), rotate(12, 8), rotate(-8, 12), rotate(8, 12), rotate(0, 10)
455 if self.ship.overheated:
456 self.set_color(1.0, 0.0, 0.0)
457 else:
458 self.set_color(1.0, 1.0, 1.0)
459 for start, end in ((nose, left), (nose, right), (left, right), (left, left_engine), (right, right_engine), (left_engine, center), (right_engine, center)):
460 self.draw_line(*start, *end)
461 self.set_color(0.0, 1.0, 1.0)
462 cockpit_left, cockpit_right, cockpit_center = rotate(-3, -8), rotate(3, -8), rotate(0, -5)
463 self.draw_line(*cockpit_left, *cockpit_right)
464 self.draw_line(*cockpit_left, *cockpit_center)
465 self.draw_line(*cockpit_right, *cockpit_center)
466 if self.key_thrust:
467 self.set_color(1.0, 0.4, 0.0)
468 for start, end in ((left_engine, rotate(-6, 18)), (center, rotate(0, 22)), (right_engine, rotate(6, 18))):
469 self.draw_line(*start, *end)
470
471 def draw_asteroids(self) -> None:
472 for asteroid in self.asteroids:
473 if not asteroid.active:
474 continue
475 cosine, sine = math.cos(asteroid.rotation_angle), math.sin(asteroid.rotation_angle)
476 vertices = [(asteroid.x + x * cosine - y * sine, asteroid.y + x * sine + y * cosine) for x, y in asteroid.vertices]
477 self.set_color(1.0, 1.0, 1.0)
478 for index, vertex in enumerate(vertices):
479 self.draw_line(*vertex, *vertices[(index + 1) % len(vertices)])
480 inner = [(asteroid.x + (x - asteroid.x) * 0.6, asteroid.y + (y - asteroid.y) * 0.6) for x, y in vertices]
481 self.set_color(0.59, 0.59, 0.59)
482 for index, vertex in enumerate(inner):
483 self.draw_line(*vertex, *inner[(index + 1) % len(inner)])
484 self.set_color(0.39, 0.39, 0.39)
485 for index in range(0, ASTEROID_VERTICES, 2):
486 self.draw_line(*vertices[index], *inner[index])
487 self.set_color(0.71, 0.71, 0.71)
488 for index in range(0, ASTEROID_VERTICES, 2):
489 self.draw_line(*vertices[index], *inner[(index + 1) % ASTEROID_VERTICES])
490
491 def draw_particles(self) -> None:
492 for particle in self.particles:
493 if particle.active:
494 ratio = particle.lifetime / 60.0
495 self.set_color(1.0, min(1.0, ratio * 2.0), 0.0 if ratio < 0.5 else 0.3)
496 self.draw_rect(particle.x, particle.y, 2, 2)
497
498 def print_centered(self, text: str, y: int, color: mxvk.Color) -> None:
499 dimensions = self.get_text_dimensions(text)
500 width = dimensions[0] if dimensions else len(text) * self.last_font_size // 2
501 self.print_text(text, self.width // 2 - width // 2, y, color)
502
503 def draw_countdown(self, elapsed: float) -> None:
504 self.draw_stars(0.3)
505 number = 3 - int(elapsed)
506 if number > 0:
507 self.print_centered(str(number), self.height // 2, mxvk.Color(255, 255, 0))
508 else:
509 self.print_centered("LAUNCH!", self.height // 2, mxvk.Color(0, 255, 0))
510 self.print_centered("PREPARE FOR MISSION", self.height // 2 + self.last_font_size * 2, mxvk.Color(255, 255, 255))
511
512 def draw_game(self) -> None:
513 self.draw_stars(1.0)
514 self.draw_ship(self.ship.x, self.ship.y)
515 self.set_color(1.0, 0.0, 0.0)
516 for projectile in self.projectiles:
517 if projectile.active:
518 self.draw_line(projectile.x, projectile.y, projectile.x - projectile.vx * 0.5, projectile.y - projectile.vy * 0.5)
519 self.draw_asteroids()
520 self.draw_particles()
521 self.print_text(f"Score: {self.ship.score}", self.to_x(10), self.to_y(10), mxvk.Color(255, 255, 255))
522 self.print_text(f"Lives: {self.ship.lives}", self.to_x(10), self.to_y(30), mxvk.Color(255, 255, 255))
523
524 def proc(self) -> None:
525 current_time = time.monotonic()
526 frame_time = current_time - self.last_frame_time
527 if frame_time < TARGET_FRAME_TIME:
528 time.sleep(TARGET_FRAME_TIME - frame_time)
529 current_time = time.monotonic()
530 self.last_frame_time = current_time
531 self.refresh_size()
532 self.update_font_size()
533 now = current_time
534 elapsed = now - self.state_started
535 if self.state == "countdown":
536 self.draw_countdown(elapsed)
537 if elapsed >= 4.0:
538 self.state, self.state_started = "launch", now
539 elif self.state == "launch":
540 self.draw_stars(0.5)
541 launch_y = GAME_H - min(1.0, elapsed * 2.0) * (GAME_H - self.ship.y)
542 self.draw_asteroids()
543 self.draw_ship(self.ship.x, launch_y)
544 if elapsed >= 0.5:
545 self.print_centered("MISSION START!", self.height // 2, mxvk.Color(0, 255, 255))
546 if elapsed >= 1.0:
547 self.state = "playing"
548 elif self.state == "playing":
549 self.update_ship()
550 self.update_fire()
551 self.update_projectiles()
552 self.update_asteroids()
553 self.update_particles()
554 self.check_collisions()
555 self.maintain_asteroids()
556 self.update_death()
557 self.draw_game()
558 else:
559 self.draw_stars(0.2)
560 self.print_centered("GAME OVER", self.to_y(130), mxvk.Color(255, 0, 0))
561 self.print_centered(f"Final Score: {self.ship.score}", self.to_y(165), mxvk.Color(255, 255, 255))
562 self.print_centered("Press SPACE to begin", self.to_y(195), mxvk.Color(255, 255, 0))
563
564 def event(self, event: mxvk.Event) -> None:
565 if event.key_down:
566 if event.key == mxvk.KEY_ESCAPE:
567 self.request_exit()
568 elif self.state == "gameover" and event.key == mxvk.key_code("Space"):
569 self.init_game()
570 elif event.key == mxvk.key_code("Left"):
571 self.key_left = True
572 elif event.key == mxvk.key_code("Right"):
573 self.key_right = True
574 elif event.key == mxvk.key_code("Up"):
575 self.key_thrust = True
576 elif event.key == mxvk.key_code("Space"):
577 self.key_fire = True
578 elif event.key_up:
579 if event.key == mxvk.key_code("Left"):
580 self.key_left = False
581 elif event.key == mxvk.key_code("Right"):
582 self.key_right = False
583 elif event.key == mxvk.key_code("Up"):
584 self.key_thrust = False
585 elif event.key == mxvk.key_code("Space"):
586 self.key_fire = False
587
588
589def parse_args() -> argparse.Namespace:
590 parser = argparse.ArgumentParser(description="MXVK Python SpaceRox/Asteroids example")
591 parser.add_argument("--width", type=int, default=1280)
592 parser.add_argument("--height", type=int, default=720)
593 parser.add_argument("--fullscreen", action="store_true")
594 parser.add_argument("--vsync", action="store_true")
595 return parser.parse_args()
596
597
598def main() -> None:
599 args = parse_args()
600 window = AsteroidsWindow(args.width, args.height, args.fullscreen, args.vsync)
601 try:
602 window.loop()
603 finally:
604 window.close()
605
606
607if __name__ == "__main__":
608 main()
np.ndarray make_star_pixels(self)
Definition asteroids.py:157
None __init__(self, int width, int height, bool fullscreen, bool enable_vsync)
Definition asteroids.py:125
int to_x(self, float value)
Definition asteroids.py:171
None event(self, mxvk.Event event)
Definition asteroids.py:564
None draw_rect(self, float x, float y, float width, float height)
Definition asteroids.py:241
int to_w(self, float value)
Definition asteroids.py:177
int to_h(self, float value)
Definition asteroids.py:180
None print_centered(self, str text, int y, mxvk.Color color)
Definition asteroids.py:498
None draw_stars(self, float speed_multiplier)
Definition asteroids.py:438
None update_projectiles(self)
Definition asteroids.py:327
None spawn_asteroid(self, float x, float y, float vx, float vy, float radius)
Definition asteroids.py:262
None create_explosion(self, float x, float y)
Definition asteroids.py:357
None split_asteroid(self, Asteroid asteroid)
Definition asteroids.py:343
None draw_ship(self, float x, float y)
Definition asteroids.py:449
None draw_line(self, float x0, float y0, float x1, float y1)
Definition asteroids.py:244
None maintain_asteroids(self)
Definition asteroids.py:417
None draw_countdown(self, float elapsed)
Definition asteroids.py:503
None assign_star_color(self, Star star)
Definition asteroids.py:225
tuple[float, float] random_safe_position(self)
Definition asteroids.py:208
int to_y(self, float value)
Definition asteroids.py:174
None set_color(self, float red, float green, float blue, float alpha=1.0)
Definition asteroids.py:238
None check_asteroid_collisions(self)
Definition asteroids.py:397
Main Vulkan window wrapper for MXVK.
Definition mxvk.hpp:39
VkExtent2D swapchain_extent
Definition mxvk.hpp:616
void release()
Release Vulkan and SDL resources.
Definition mxvk.cpp:247
virtual void proc()
Execute one processing/update step.
Definition mxvk.cpp:1278
None main()
Definition asteroids.py:598
argparse.Namespace parse_args()
Definition asteroids.py:589
Definition main.py:1