27from dataclasses
import dataclass, field
28from pathlib
import Path
32import mxvk_ext
as mxvk
43PROJECTILE_LIFETIME = 60
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
54EXAMPLE_DIR = Path(__file__).resolve().parent
55DATA_DIR = EXAMPLE_DIR /
"data"
66 fire_cooldown: int = 0
68 exploding: bool =
False
69 explosion_timer: int = 0
71 continuous_fire_timer: int = 0
72 overheated: bool =
False
73 overheat_cooldown: int = 0
94 vertices: list[tuple[float, float]] = field(default_factory=list)
95 rotation_angle: float = 0.0
96 rotation_speed: float = 0.0
120 twinkle_phase: float = 0.0
121 twinkle_speed: float = 1.0
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)
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"))
149 self.
pixel.update_texture(np.full((2, 2, 4), 255, dtype=np.uint8), 2, 2, 8)
158 pixels = np.zeros((4, 4, 4), dtype=np.uint8)
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))))
165 def sx(self) -> float:
166 return self.
width / GAME_W
168 def sy(self) -> float:
169 return self.
height / GAME_H
171 def to_x(self, value: float) -> int:
172 return int(value * self.
sx())
174 def to_y(self, value: float) -> int:
175 return int(value * self.
sy())
177 def to_w(self, value: float) -> int:
178 return max(1, int(value * self.
sx()))
180 def to_h(self, value: float) -> int:
181 return max(1, int(value * self.
sy()))
189 font_size = max(14, min(128, int(20.0 * self.
height / 480.0)))
192 self.set_font(str(DATA_DIR /
"font.ttf"), font_size)
193 self.clear_text_queue()
202 self.
spawn_asteroid(x, y, random.uniform(-1.0, 1.0), random.uniform(-1.0, 1.0), random.uniform(20.0, 39.0))
204 self.
state =
"countdown"
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:
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))
223 self.
stars.append(star)
226 roll = random.random()
228 star.red, star.green, star.blue = random.uniform(0.85, 1.0), random.uniform(0.9, 1.0), 1.0
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
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
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
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
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)
241 def draw_rect(self, x: float, y: float, width: float, height: float) ->
None:
244 def draw_line(self, x0: float, y0: float, x1: float, y1: float) ->
None:
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)
249 size = max(1, int(1.5 * min(self.
sx(), self.
sy())))
252 if x0 == x1
and y0 == y1:
254 twice_error = 2 * error
255 if twice_error >= dy:
258 if twice_error <= dx:
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)
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))
275 if self.
ship.exploding:
278 self.
ship.angle -= 0.15
280 self.
ship.angle += 0.15
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)
286 self.
ship.vx, self.
ship.vy = self.
ship.vx * 4.0 / speed, self.
ship.vy * 4.0 / speed
291 self.
ship.fire_cooldown = max(0, self.
ship.fire_cooldown - 1)
294 if self.
ship.overheated
or self.
ship.fire_cooldown > 0:
296 if self.
ship.burst_count >= SHOTS_PER_BURST:
297 self.
ship.fire_cooldown, self.
ship.burst_count = FIRE_COOLDOWN, 0
299 self.
ship.fire_cooldown += FIRE_DELAY
300 self.
ship.burst_count += 1
304 projectile = next((value
for value
in self.
projectiles if not value.active),
None)
305 if projectile
is None:
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
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
325 self.
ship.continuous_fire_timer = max(0, self.
ship.continuous_fire_timer - 1)
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
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
344 radius = asteroid.radius / 2.0
345 if radius < MIN_ASTEROID_RADIUS:
347 self.
ship.score += SMALL_ASTEROID_POINTS
348 asteroid.active =
False
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
355 asteroid.active =
False
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
365 particle.x = (particle.x + particle.vx) % GAME_W
366 particle.y = (particle.y + particle.vy) % GAME_H
369 particle.lifetime -= 1
370 if particle.lifetime <= 0:
371 particle.active =
False
376 if not projectile.active:
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
386 self.
ship.score += SMALL_ASTEROID_POINTS
389 if self.
ship.exploding:
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
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:
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:
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
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))))
424 if self.
ship.exploding:
425 self.
ship.explosion_timer -= 1
427 if self.
ship.explosion_timer <= 0:
428 self.
ship.exploding =
False
431 if self.
ship.lives <= 0:
432 self.
state =
"gameover"
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
439 for star
in self.
stars:
440 star.y += star.speed * speed_multiplier
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())))
450 if self.
ship.exploding:
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:
459 for start, end
in ((nose, left), (nose, right), (left, right), (left, left_engine), (right, right_engine), (left_engine, center), (right_engine, center)):
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)
468 for start, end
in ((left_engine, rotate(-6, 18)), (center, rotate(0, 22)), (right_engine, rotate(6, 18))):
473 if not asteroid.active:
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]
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]
482 for index, vertex
in enumerate(inner):
483 self.
draw_line(*vertex, *inner[(index + 1) % len(inner)])
485 for index
in range(0, ASTEROID_VERTICES, 2):
486 self.
draw_line(*vertices[index], *inner[index])
488 for index
in range(0, ASTEROID_VERTICES, 2):
489 self.
draw_line(*vertices[index], *inner[(index + 1) % ASTEROID_VERTICES])
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)
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)
505 number = 3 - int(elapsed)
517 if projectile.active:
518 self.
draw_line(projectile.x, projectile.y, projectile.x - projectile.vx * 0.5, projectile.y - projectile.vy * 0.5)
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))
525 current_time = time.monotonic()
527 if frame_time < TARGET_FRAME_TIME:
528 time.sleep(TARGET_FRAME_TIME - frame_time)
529 current_time = time.monotonic()
535 if self.
state ==
"countdown":
539 elif self.
state ==
"launch":
541 launch_y = GAME_H - min(1.0, elapsed * 2.0) * (GAME_H - self.
ship.y)
547 self.
state =
"playing"
548 elif self.
state ==
"playing":
561 self.
print_centered(f
"Final Score: {self.ship.score}", self.
to_y(165), mxvk.Color(255, 255, 255))
564 def event(self, event: mxvk.Event) ->
None:
566 if event.key == mxvk.KEY_ESCAPE:
568 elif self.
state ==
"gameover" and event.key == mxvk.key_code(
"Space"):
570 elif event.key == mxvk.key_code(
"Left"):
572 elif event.key == mxvk.key_code(
"Right"):
574 elif event.key == mxvk.key_code(
"Up"):
576 elif event.key == mxvk.key_code(
"Space"):
579 if event.key == mxvk.key_code(
"Left"):
581 elif event.key == mxvk.key_code(
"Right"):
583 elif event.key == mxvk.key_code(
"Up"):
585 elif event.key == mxvk.key_code(
"Space"):
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()
600 window =
AsteroidsWindow(args.width, args.height, args.fullscreen, args.vsync)
607if __name__ ==
"__main__":
None check_collisions(self)
None update_particles(self)
np.ndarray make_star_pixels(self)
None update_asteroids(self)
None draw_particles(self)
None __init__(self, int width, int height, bool fullscreen, bool enable_vsync)
None update_font_size(self)
int to_x(self, float value)
None event(self, mxvk.Event event)
None draw_rect(self, float x, float y, float width, float height)
int to_w(self, float value)
int to_h(self, float value)
None print_centered(self, str text, int y, mxvk.Color color)
None draw_stars(self, float speed_multiplier)
None update_projectiles(self)
None spawn_asteroid(self, float x, float y, float vx, float vy, float radius)
None create_explosion(self, float x, float y)
None draw_asteroids(self)
None split_asteroid(self, Asteroid asteroid)
None draw_ship(self, float x, float y)
None draw_line(self, float x0, float y0, float x1, float y1)
None maintain_asteroids(self)
None draw_countdown(self, float elapsed)
None assign_star_color(self, Star star)
tuple[float, float] random_safe_position(self)
None fire_projectile(self)
int to_y(self, float value)
None set_color(self, float red, float green, float blue, float alpha=1.0)
None check_asteroid_collisions(self)
Main Vulkan window wrapper for MXVK.
VkExtent2D swapchain_extent
void release()
Release Vulkan and SDL resources.
virtual void proc()
Execute one processing/update step.
argparse.Namespace parse_args()