@Daniel Schreiber: Ich habe es jetzt auch mal ausprobiert und die erste Kleinigkeit, die mir aber deutlich aufgefallen ist: man kann das nicht mit der Maus beenden in dem man das X in der Fensterleiste klickt.
Ein Weg das zu Beschleunigen ist den Regen hinter dem HUD nicht zu berechnen. Das verdeckt ja einen guten Teil des Bildschirms. Alternativ würde ich das graue Rechteck einfach nicht zeichnen, damit man den Regen an der Stelle auch sehen kann.
`pygame.init()` und `pygame.quit()` haben auf Modulebene nichts zu suchen. Man sollte jedes Modul importieren können, ohne das mehr passiert als das der Modulinhalt (Konstanten, Funktionen, Klassen) definiert wird. `pygame.init()` versucht Hardware zu initialisieren.
Und `pygame.quit()` am Ende möchte man vielleich auch in einen ``finally:``-Zweig schreiben.
Der Name `Slider` ist irreführend. Darunter verstehen alle GUI-Rahmenwerke die ich kenne einen Schieberegler der in aller Regel mehr als nur zwei Positionen hat. Die vorliegende Klasse hätte eher den Namen `Switch` verdient.
`stateToChange` könnte auch einfach nur `state` heissen. Das Zustand veränderbar ist, steckt in „Zustand“ im Grunde schon drin.
Ich würde auch versuchen die Abhängigkeit von dem `Test`-Objekt zu minimieren. In Deinem Entwurf muss ja wirklich jedes Objekt dieses Exemplar kennen, und über das Test-Exemplar kommt man auch an jedes andere Objekt heran. Das ist für meinen Geschmack alles etwas zu stark aneinander gekoppelt. Aus den `draw()`-Methoden kann man es beispielsweise ganz einfach heraus bekommen, in dem man das `Surface` auf dem gezeichnet werden soll als Argument übergibt. Aus `Raindrop` und `Rain` kann man es dann schon rausnehmen, weil deren `update()`-Methode es nicht verwenden.
Wenn man die beiden Farben von dem Schalter-Objekt in einer Liste speichert, lässt sich das schalten kompakter ausdrücken.
Statt sich in `mKlicked` ein Flag zu merken ob mit der Maus geklickt wurde oder nicht, könnte man sich dort entweder `None` oder die Position der Maus beim Klick merken. Und auch den `update()`-Methoden könnte man Argumente mitgeben, damit die Objekte nicht alle das komplette `Test`-Exemplar kennen müssen. Wenn man bei `Hud.update()` die Klickposition übergibt und die dort an `Slider.update()` weiterreicht, dann braucht auch der `Slider` das `Test`-Objekt gar nicht mehr zu kennen.
Nun braucht man nur noch die Verbindung zwischen `Slider` und `Rain` über eine Rückruffunktion realisieren und die in `Test` verbinden, so das `Hud` gar nichts davon wissen muss, und schon muss auch `Hud` das `Test`-Objekt nicht mehr kennen.
Kommentare gehören über den Code den sie kommentieren. An mindestens einer Stelle hast Du das umgekehrt gemacht.
`Raindrop` hat ein paar unbenutze Attribute und `self.particles` wird in der `__init__()` zweimal definiert.
Die Konstante `MTOP` braucht einen besseren Namen. Was soll der aktuelle Name denn bedeuten?
Literale Zeichenketten sind nicht dazu da um Code auszukommentieren oder generell Kommentare zu schreiben.
Bei den Partikeln ist noch ein zweiter Fehler bei den Zuständigkeiten. Diesmal nicht zwischen Objekten, sondern zwischen Methoden: Die `draw()`-Methode sollte wirklich nur zeichnen und nicht auch gleichzeitig noch Partikelpositionen aktualisieren. Das gehört in die `update()`-Methode.
Wobei ich die Listenrepräsentation eines Partikels für unschön halte. Das gehört IMHO auch in eine Klasse.
Das der Inhalt von `particles` mit ``del`` gelöscht wird ist eine überflüssige Aktion. Das passiert kurz bevor das `Raindrop`-Exemplar insgesamt zur Speicherbereinigung freigegeben wird, weil es aus der Liste mit den Regentropfen entfernt wird.
Ein weiterer Fehler befindet sich in `gameloop()`. Eine `pygame.error`-Ausnahme wird mit einem `pygame.quit()` behandelt und danach geht aber alles weiter wie vorher, man kann aber keine Pygame-Funktionalität mehr verwenden wenn man `pygame.quit()` aufgerufen hat.
Der `pygame.quit()`-Aufruf in der `input()`-Methode sollte auch nicht sein, denn danach wird das am Programmende ja noch einmal aufgerufen. Einmal am Programmende reicht.
Ich lande dann ungefähr hier:
Code: Alles auswählen
#!/usr/bin/env python3
import random
import pygame
SIZE = (1024, 786) # Set to `None` for fullscreen.
MTOP = 50 # TODO Needs a better name.
FPS = 60
WHITE = (255, 255, 255)
BLACK = (0, 0, 0)
BLUE = (0, 0, 255)
LIGHTGREY = (160, 160, 160)
class Switch:
COLORS = [BLACK, BLUE]
def __init__(self, position, state=False, callback=lambda state: None):
self.state = state
self.callback = callback
self.rect = pygame.Rect(position, (50, 20))
@property
def color(self):
return self.COLORS[self.state]
def update(self, click_position):
if click_position and self.rect.collidepoint(click_position):
self.state = not self.state
self.callback(self.state)
def draw(self, screen):
pygame.draw.rect(screen, self.color, self.rect)
class Hud:
def __init__(self, width, height):
self.width = width
self.height = height
self.rain_switch = Switch((120, 500))
def update(self, click_position):
self.rain_switch.update(click_position)
def draw(self, screen):
# pygame.draw.rect(screen, LIGHTGREY, [0, 0, self.width, self.height])
self.rain_switch.draw(screen)
class Particle:
def __init__(self, position, velocity):
self.position = list(position)
self.velocity = velocity
def update(self):
self.position[0] += self.velocity[0]
self.position[1] += self.velocity[1]
def draw(self, screen):
pygame.draw.line(screen, BLUE, self.position, self.position, 1)
class Raindrop:
def __init__(self, x, y, max_y):
self.x = x
self.y = y
self.max_y = max_y
self.particles = []
# Tropfendurchmesser in mm * 2 = m/s
# 1m = 100pxl
diameter = 4 # eigentlich mm, wäre zu klein
self.velocity = (
random.uniform(-0.2, 0.2) / FPS * MTOP,
(diameter * 2 + random.random()) / FPS * MTOP
)
self.particles = []
def update(self):
self.y += self.velocity[1]
self.x += self.velocity[0]
if self.y >= self.max_y:
for particle in self.particles:
particle.update()
self.particles.append(
Particle(
(self.x, self.max_y),
(
random.uniform(-50, 50) / FPS,
random.uniform(-50, 0) / FPS)
)
)
if self.y > self.max_y + 140:
return False
return True
def draw(self, screen):
pygame.draw.line(
screen, BLUE, [self.x, self.y], [self.x, self.y + 4], 1
)
for particle in self.particles:
particle.draw(screen)
class Rain:
def __init__(self, size):
self.width, self.height = size
self.raindrops = []
self.raining = False
def switch(self, state):
self.raining = state
def update(self):
if self.raining:
for _ in range(3):
self.raindrops.append(
Raindrop(random.randint(0, self.width), 0, self.height)
)
raindrops = []
for raindrop in self.raindrops:
if raindrop.update():
raindrops.append(raindrop)
self.raindrops = raindrops
def draw(self, screen):
for raindrop in self.raindrops:
raindrop.draw(screen)
class Simulation:
def __init__(self, screen):
self.screen = screen
self.running = True
self.click_position = None
self.clock = pygame.time.Clock()
self.rain = Rain(self.screen.get_size())
self.hud = Hud(300, self.screen.get_height())
self.hud.rain_switch.callback = self.rain.switch
def input(self):
for event in pygame.event.get():
if (
event.type == pygame.KEYDOWN and event.key == pygame.K_ESCAPE
or event.type == pygame.QUIT
):
self.running = False
elif event.type == pygame.MOUSEBUTTONUP:
self.click_position = pygame.mouse.get_pos()
def update(self):
self.rain.update()
self.hud.update(self.click_position)
self.click_position = None
def draw(self):
self.screen.fill(WHITE)
self.rain.draw(self.screen)
self.hud.draw(self.screen)
pygame.display.flip()
def run(self):
while self.running:
self.clock.tick(FPS)
self.input()
self.update()
self.draw()
def main():
pygame.init()
try:
if SIZE:
screen = pygame.display.set_mode(SIZE)
else:
screen = pygame.display.set_mode((0, 0), pygame.FULLSCREEN)
pygame.display.set_caption('Rain')
simulation = Simulation(screen)
simulation.run()
finally:
pygame.quit()
if __name__ == '__main__':
main()