Benutzer-Werkzeuge

Webseiten-Werkzeuge


Seitenleiste

ws1617:2d-jump_n_run

Verbesserungsvorschläge [17.April]

  • Ausblick hinzufügen, was kann noch gemacht werden, was muss verbessert werden?
  • Da es essentiell ist für euer Projekt würde ich an eurer Stelle noch irgendwo einen Link zu pygame hinzufügen
  • Für Leute die noch nichts mit Spieleprogrammierung zu tun hatten/haben, wäre ein Diagramm wie die verschiedenen Module miteinander kommunizieren Gold wert.
  • Man könnte noch in einem Abschnitt auf die „Physikengine“ eingehen, was wurde wie realisiert ( Schwerkraft, Ballistischer Wurf, etc.). Gerne auch mit Formeln.

Das Jump'n'Run-Projekt

Willkommen zum Wiki-Artikel über das Jump and Run Projekt von Tobias, Adel und Kilian.

Die Projekte sollten einen wissenschftlichen Aspekt beinhalten, zudem die Gruppenmitglieder interessieren, Spaß machen und einen dazu anreizen sich in seiner eigenen Freizeit mit dem Projekt zu beschäftigen und dieses weiter voranzubringen.

Deshalb haben wir uns dazu entschieden ein Spiel zu entwickeln.

Grund für die Wahl unseres Projekts

Spiele sind sehr spaßig und interessant. Zudem ist die Entwicklung eines Spiels ein äußerst kreativer Prozess, bei dem man die Programmiererfolge direkt konkret sehen kann. Außerdem werden Spiele sehr schnell sehr komplex und bieten damit die perfekte Möglichkeit sich mit der Syntax einer Programmiersprache tiefgehend vertraut zu machen und die von ihr zur Verfügung gestellten Möglichkeiten komplett auszuschöpfen.

Der Titel unseres Projekts erscheint erstaunlich banal und wirkt so als wollten wir es und besonders einfach machen. Was für ein Jump'n'Run (wie z.B. eines der alten Mario Spiele) zutreffen würde, jedoch haben wir nicht vor ein so simples Spiel zu programmieren, zumal weil da der wissenschaftliche Aspekt nahezu nicht vorhanden ist, sondern wir möchten ein Jump'n'Run programmieren, das sich echt anfühlt, was den Objekten einen Charakter gibt und Leben suggeriert. Um das zu erreichen, muss man sich erst mit der Realität auseinanderzusetzen und die dahinterstehenden Naturgesetze verstehen. Spätestens anhand des letzten Satzes sollte es klar geworden sein wo hier der Wissenschaftlich Aspekt liegt. Zudem ist ein Spiel nie perfekt. Man kann es immer weiter verbessern. Ist die Spielumgebung zufriedenstellend realistisch, kann man anfangen KIs hinzuzufügen (z.B. in Form von Gegnern). Diese KIs kann man versuchen immer intelligenter zu machen, um damit den Spielspaß zu erhöhen. Man kann die Spielwelt immer weiter ausbauen und komplexere Wechselwirkungen verschiedenster Objekte einbauen. Das bietet einem eine enorme Flexibilität und der Erfolg ist proportional zur hineingesteckten Arbeit.

Die konkretere Wahl eines 2D Jump'n'Runs ist darauf zurückzuführen, dass ein 3D SPiel für uns ein unrealistisches Ziel wäre, was selbst große Spieleentwicklungsstudios viel Arbeit kostet. (Damit wären wir drei, wovon zwei absolute Programmieranfänger sind, höchstwahrscheinlich ein kleines bisschen überfordert.)

Projektplanung

Nach kurzer Recherche sind wir auf ein schönes Python-package namens „Pygame“ gestoßen, was viele nützliche Funktionen zur Spieleentwicklung mitliefert und einem „unnötige“ Arbeit abnimmt.

Nachdem wir uns darauf geeinigt haben Pygame als Entwicklungshilfe zu benutzen, war es dann an der Zeit sich mit Pygames Funktionen vertraut zu machen, was sich mit der Macht des Internets nicht allzu schwer gestaltet hat. Es gibt viele hilfreiche Artikel, schöne Videos und kompakte Bücher, die es einem sehr einfach gestalten Pygames Funktionen zu lernen.

Und somit war unsere erste Aufgabe uns all dieses Wissen zu Gemüte zu führen, damit wir dieses Modul effektiv benutzen könnten. Als Startpunkt haben wir ein bereits geschriebenes template benutzt, dass als ein Skelett für unser Spiel fungierte Dabei achteten wir darauf, dass wir alles verstehen und somit potenzielle Problemquellen leichter finden und Probleme leichter lösen können.

Zu beginn ging es relativ schleppend voran, da wir erst noch mit Pygame warm werden mussten, aber mit der Zeit wurde das Arbeiten mit Pygame einfacher. (trotzdem konnten wir nicht alles erreichen, was wir und oben als Ziel gesetzt haben.)

Projektverlauf

Jedes Pygame-Projekt startet in der Regel mit einem speziellen Code-Skelett, was die Grundlegendsten Funktionen eines jeden Spiels beinhaltet. Dieses sah bei uns folgendermaßen aus:

# import necessary files
import pygame
import random


WIDTH = 360
HEIGHT = 480
FPS = 30

# define colors
WHITE = (255, 255, 255)
BLACK = (0, 0, 0)
RED = (255, 0, 0)
GREEN = (0, 255, 0)
BLUE = (0, 0, 255)

# initialize pygame and create window
pygame.init()
pygame.mixer.init()
screen = pygame.display.set_mode((WIDTH, HEIGHT))
pygame.display.set_caption("My Game")
clock = pygame.time.Clock()

all_sprites = pygame.sprite.Group()
# Game loop
running = True
while running:
    # keep loop running at the right speed
    clock.tick(FPS)
    # Process input (events)
    for event in pygame.event.get():
        # check for closing window
        if event.type == pygame.QUIT:
            running = False

    # Update
    all_sprites.update()

    # Draw / render
    screen.fill(BLACK)
    all_sprites.draw(screen)
    # *after* drawing everything, flip the display
    pygame.display.flip()

pygame.quit()

Diese Datei haben wir in eine „main.py“ sowie eine „settings.py“ aufgespaltet. Die „main.py“ enthält den Gameloop, während die „Settings.py“ alle Konstanten beinhaltet. In der main.py erstellen wir ein Game-Objekt „g“, das was jeden frame aktualisiert wird und die wichtigsten funktionen beinhaltet. Wenn man den Ordner „1.0.7 stable“ in PyCharm öffnet, kann man im „Structure“-Fenster gut nachvollziehen, wie das Spiel aufgebaut ist.

Daraufhin haben wir eine „Sprites.py“ geschrieben, welche alle Sprite-Klassen definiert.

import pygame as pg
from settings import* #importiert unsere Konstanten aus der settings.py

vec = pg.math.Vector2


class Player(pg.sprite.Sprite):
 # defines player
    def __init__(self, game):
  # macht Instanzen dieser Klasse zu sprites
        pg.sprite.Sprite.__init__(self)
  # hier definieren wir erstmal die grundlegendsten Attribute des Spielers
        self.game = game
        self.image = pg.Surface((30, 40))
        self.image.fill(YELLOW)
        self.rect = self.image.get_rect()
        self.pos = vec( self.rect.width / 2, HEIGHT - 40 )
        self.rect.center = self.pos
        self.vel = vec(0, 0)
        self.acc = vec(0, 0)

    def jump(self):
     # Jump only if standing on ground
        self.rect.x += 1
        hitsPlat = pg.sprite.spritecollide(self, self.game.platforms, False)
        hitsWall = pg.sprite.spritecollide(self, self.game.walls, False)
        self.rect.x -= 1
        if hitsPlat and self.vel.y == 0:
            self.vel.y = -PLAYER_JUMP

    def update(self):
     # wird jeden frame aufgerufen
        self.acc = vec(0, PLAYER_GRAVITY)
        keys = pg.key.get_pressed()
        if keys[pg.K_LEFT] or keys[pg.K_a]:
            self.acc.x = -PLAYER_ACC
        if keys[pg.K_RIGHT] or keys[pg.K_d]:
            self.acc.x = PLAYER_ACC
        if keys[pg.K_UP] or keys[pg.K_w]:
            self.jump()
        if keys[pg.K_SPACE]:
            self.jump()


      # Reibung
        self.acc.x += self.vel.x * PLAYER_FRICTION
      # Geschwindigkeitsgleichungen
        self.vel += self.acc
        self.pos += self.vel + 0.5 * self.acc
      # Definition der Position
        self.rect.midbottom = self.pos
        
# defines platform
class Platform(pg.sprite.Sprite):
    def __init__(self, x, y, w, h):
        pg.sprite.Sprite.__init__(self)
        self.image = pg.Surface((w, h))
        self.image.fill(GREEN)
        self.rect = self.image.get_rect()
        self.rect.x = x
        self.rect.y = y

Diese sprites wurden in der main.py instanziiert und in verschiedene Spritegruppen getan. (Pygame beinhaltet eine Funktion, die es ermöglicht sprites in Sprite-Gruppen zu organisieren, was die Arbeit erheblich erleichtert.) Zu diesem Zeitpunkt hatten wir ein funktionierendes Spielfenster, einen Spieler, der sich bewegen konnte und Platformen, auf denen der Spieler laufen konnte.

Ab diesem Punkt fing der wahre Spaß erst an (oder Frust, je nach dem). Die Platformen haben nur von oben den Spieler davon abgehalten hindurchzugleiten und als wir versucht haben die Platformen sich so verhalten zu lassen, dass der Spieler sie von keiner Seite durchdringen kann, sind Spieler-Platform-Interaktionen entstanden, die nicht gewollt waren. Dieses Problem hat uns viel Konpfschmerz bereitet.

Zuerst wollten wir das Problem lösen, indem wir zu den Platformen, Wände als weitere Klasse hinzugen. Die Platformen sollten den Spieler weder von oben noch von unten durchlassen und die Wände sollten von rechts und links nicht zu durchdringen sein. Das hat das Problem aber nicht richtig gelöst. Es führte bloß zu neuen Problemen und hat die Kollisionsabfragen in der main.py unnötig verkompliziert. Natürlich haben wir eine Lösung für dieses Problem gefunden.

Beim Programmieren gibt es immer eine Lösung zu einem Problem. Was wir uns vorstellen können, können wir auch programmieren. Wir haben einfach den Wald for lauter Bäumen nicht gesehen. Nach einigem Rumgeteste, haben wir etwas erhalten was die Wall-Klasse überflüssig machte und den code wesentlich vereinfachte.

def update(self): # Game Loop - Update
self.all_sprites.update()
hits_platform = pg.sprite.spritecollide(self.player, self.platforms, False)

for hits in hits_platform:
    # check if player hits a platform - only when moving left
    if self.player.vel.x > 0 :
        if hits and self.player.rect.right -10 <= hits.rect.left:
            self.player.pos.x = hits.rect.left - self.player.width / 2
            self.player.vel.x = 0
            self.player.vel.y = 0
            self.mov_left_allowed = False

    # check if player hits a platform - only when moving right
    if  self.player.vel.x < 0:
        if hits and self.player.rect.left +10 >= hits.rect.right:
            self.player.pos.x = hits.rect.right + self.player.width / 2
            self.player.vel.x = 0
            self.player.vel.y = 0

    # check if player hits a platform - only when falling
    if self.player.vel.y > 0:
        if hits:
            self.player.pos.y = hits.rect.top
            self.player.vel.y = 0

    # check if player hits a platform - only when jumping
    if self.player.vel.y < 0:
        if hits:
            self.player.pos.y = hits.rect.bottom + self.player.heigth
            self.player.vel.y = 0

Zu diesem Zeitpunkt haben wir auch eine levels.py erstellt, welche die Parameter aller Objekt-Instanzen der verschiedenen Klassen beinhalten sollte. Die levels.py war dafür da den Überblick in der main.py nicht zu verlieren und um die größerwerdende Anzahl verschiedener Objektinstanzen einfach und logisch sortieren zu können. Das Erstellen dieser Datei wird von einem Level-editor übernommen, den Tobias in C# geschrieben hat. (Der mit jeder neuen Sprite-Klasse immer komplizierter wurde.) Wenn Du dir die level.py anschaust, wirst du sehen, weshalb wir das nicht von hand gemacht haben. (Und wir haben gerade einmal 3 Levels)

levels.py http://i.imgur.com/CN1k43P.png

Der MathesisJump!LevelEditor

Der Level-Editor ist ein zusätzliches Tool, um Levels in Form einer Levels.py-Datei auf einer graphischen Oberfläche zu generieren. Ich habe den Level-Editor inder Programmiersprache C# geschrieben, da ich mit dieser schon einige Jahre Erfahrung habe. Außerdem ist die von Microsoft gestellte Oberfläche zum erstellen von UIs sehr leicht zu bedienen, so dass auch kompliziertere Elemente leicht zu implementieren sind, wie zum Beispiel der Drag-and-Drop-Vorgang von UI-Elementen.

Wir hatten sehr früh in unserem Projekt festgestellt, dass das Erstellen von Levels schnell sehr unübersichtlich wird, da Python keine Oberfläche bietet, auf der man leicht verschiedene Elemente (zum Beispiel Rechtecke) in einem Fenster anordnen kann, und diesen verschiedene Eigenschaften zuweisen. Die ersten Versionen des Level-Editors waren noch recht umständlich zu bedienen, beispielweise waren ursprünglich Platformen nicht nachträglich bewegbar, oder der „Undo“-Button konnte nur eine Aktion rückgängig machen. Andernfalls hätte man alle vorigen Level komplett neu erstellen müssen. (Eine „Zwischenspeichern“-Funktion gibt es übrigens zur Zeit immer noch nicht.) Nach und nach hat auch das Hauptprojekt Gestalt angenommen, so wurden Gegner intelligenter und haben begonnen ihrerseits mit ~Projektilen~ Schneebällen um sich zu werfen. All diese Features durften im Level-Editor natürlich nicht fehlen, zwischendurch wurde eine Option eingefügt, mit der ein Hintergrundbild angezeigt werden kann, damit man im Vorfeld ein Level planen kann (zum Beispiel in GIMP) und dann im Level-Editor nur noch die Rechtecke an die Richtigen Positionen ziehen muss.

Wie bediene ich den Level-Editor?

Wenn das Programm gestartet wird, öffnet sich das Haupt-Fenster. Hier sieht man eine schwarze Oberfläche mit einem grauen Raster darauf, und eine Titelleiste, in der sich eine Farb- und Hotkey-Legende befindet, sowie Buttons zum Importieren eines Hintergrundbildes, Rückgängigmachen des letzen Arbeitsschritts, Speichern, Exportieren und Schließen des Fensters. Wenn sich der Cursor über der Arbeitsfläche befindet und einer der Hotkeys (Q, W, .., Z) gedrückt wird, öffnet sich ein Dialog zum erstellen des jeweiligen Objekts. In diesem „New-Object-Dialog“ kann die Position, an dem das Objekt erstellt wird eingesehen werden und Eigenschaften, wie etwa der Größe des Objekts verändert werden. Für unterschiedliche Objekte werden unterschiedliche Buttons ausgeblendet, schließlich besitzt beispielsweise eine Plattform keine Eigenschaft, die bestimmt, in welche Richtung sie wirft, eine Plattform kann keine Projektile werfen.

Wenn man auf „Create“ klickt, wird das Objekt im Hauptfenster auf der Arbeitsfläche angezeigt. Wenn man mit der Position unzufrieden ist, kann es noch an die richtige Stelle verschoben werden.

Sobald man ein Level vollendet hat, kann man auf „Save as Level##“ klicken, wobei statt ## die Nummer des Levels angezeigt wird. Dann wird dieses im Arbeitsspeicher gesichert, und man kann mit dem nächsten Level fortfahren. Wenn alle Level erstellt wurden, klickt man zunächst noch einmal auf „Save as Level##“, um auch das letzte Level zu speichern, dann drückt man auf „Export“, um eine levels.py-Datei exportieren zu lassen. Diese enthält alle erstellten Level.

Wie funktioniert der Level-Editor?

Ich werde diese Sektion eher kurz halten, und nur die Grundkonzepte erklären.

Zuerst vorneweg: Der Level-Editor selbst kann kein Python, ich habe ihm nicht „das Programmieren beigebracht“. Alles was der Level-Editor tut, ist nach einem bestimmten Schema die Position von UI-Elementen zu ermitteln, eine Transformation der Koordinaten (von „Thickness-Objekten“ zu X-Y-Koordinaten, mit dem Ursprung oben-links) zu konvertiren. Dies erreiche ich, indem ich je Objekt-Typ eine Liste für je das Rechteck in der Nutzeroberfläche, und für jede zusätzliche Eigenschaft je eine Liste führe. Beim Drücken auf den „Speichern“-Button wird für jedes Objekt die passenden Eingenschaften dazusortiert, und in einem langen String (einer je Objekt-Typ) abgelegt. Der Export-Button verknüpft diese Strings und schreibt diese zusammen mit einigen konstanten Zeichenketten (zum Beispiel dem Datei-Header) in die Datei levels.py an einen vom Benutzer gewählten Ort auf der Festplatte (oder Speichermedium).

Weiterer Projektverlauf

Zu jedem einfachen Jump 'n' Run gehören natürlich auch Gegner und Stacheln, die den Spieler in einen alternativen Zustand des Lebens versetzen können. Die Gegner waren eine weitere sprite-Klasse, die in der update-Methode der main.py, anders mit dem Spieler interagiert. Die „Enemy“-Klasse ist bei uns eine abstrakte Klasse, von der die Klassen „Enemy_stupid“, „Enemy_thrower“ und „Spikes“ erben. Die Stacheln müssen nicht wie eine Platform funktionieren, weil der Spieler sowieso in einen alternativen Bewusstseinszustand versetzt wird, sobald er diese berührt.

Als nächstes haben wir einen Lebensbalken und eine maximale Lebensanzahl hinzugefügt und eine „Dispenser“-Klasse hinzugefügt, die wie eine Platform funtioniert, aber periodisch Projektile abschießt.

Die ganzen Kollisionsabfragen in der update methode der des Game-Objekts haben wir geschickt gestaltet. Wir überprufen mit Pygame ob der Spieler mit einer Sprite-Gruppe oder eine bestimme Sprite-Gruppe mit einer anderen Sprite-Gruppe kollidiert. (pygame.sprite.spritecollide() oder pygame.sprite.groupcollide())

Das vereinfacht das Hinzufügen weiterer Sprite-Klassen, da wir die Instanzen dieser Klassen nur zur entsprechenden sprite-Gruppe hinzufügen müssen und alle Kollisionsabfragen durch die Gruppe automatisch auch für diese Instanz gelten.

Das hinzufügen von Hintergrundbildern und nicht animierten Charakteren war relativ einfach. Aber die Reihenfolge in der die Objekte auf den Bildschirm gemalt werden war noch nicht korrekt, denn selbst, wenn man für eine bestimmte Sprite-Klasse kein Bild spezifitiert, wir es dennoch als schwarzes Viereck angezeigt. Deshalb müssen alle Platformen zuerst gemalt werden, diese werden dann vom importierten Hintergrundbild übermalt und erst dann wird der Spieler, die Gegner und die Schneebälle gezeichnet.

    def draw(self):
        # Game Loop - draw
        if self.playing:
            self.platforms.draw(self.screen)
            self.platforms.draw(self.screen)
            self.ice_platforms.draw(self.screen)
            self.dispenser.draw(self.screen)
            self.spikes.draw(self.screen)
            
            self.backgrounds.draw(self.screen)
            
            self.enemies.draw(self.screen)
            self.enemies_stupid.draw(self.screen)
            self.enemies_thrower.draw(self.screen)
            self.all_projectiles.draw(self.screen)
            self.player_sprite.draw(self.screen)
            self.draw_hit_points_bar(self.screen, 5, 5, self.player.hit_points)
            
            # *after* drawing everything, flip the display
            pg.display.flip()

Aber aus irgendeinem Grund wurden die Objekte auf dem Screen nicht gelöscht, wenn man in ein neues Level eintritt.

Die Gruppenarbeit hat uns sehr viel Spaß gemacht und uns sehr viel über die Programmiersprache „Python“ und das Programmieren im Allgemeinen beigebracht (zumindest zwei von uns). Wir sind nicht so weit gekommen, wie wir gewollt hätten. Hofstadters Gesetzt, eines der grundlegendsten Gesetzt der Spieleentwicklung hat sich bei uns bewahrheitet.

„Es dauert immer länger als du glaubst, selbsts wenn du Hofstadters Gesetz beachtest.“

Hofstadters Gesetz: https://en.wikipedia.org/wiki/Hofstadter%27s_law

Wir würden es keine Gruppe raten, unser konkretes Projekt weiterzuführen. Pygame ist ein nettes Modul, um bestimmte aufgaben zu vereinfachen, aber zur Spieleentwicklung gibt es weitaus bessere Optionen. Eine Game-Engine vereinfacht einem die Arbeit immens, erspart einem eine menge Arbeit und unnötige Bugs, die man als Programmieranfänger mur sehr schwer lösen kann (Zum Beispiel haben Graphiken, die wir für den Player-Sprite geladen haben, falsch angezeigt und Teile des Bildes sind verschoben. Mit einer .gif-Datei hat es aus irgendeinem Grund funktionert.). Wir haben vieles durch die Benutzung von Pygame gelernt, aber würden es niemals wieder für die Entwicklung komplexer Spiele benutzen.(ein einfacher 2D platformer zählt hier bereits zu den komplexeren Spielen für uns). Die Game-Engine „Godot“ ist eine sehr viel bessere Alternative für Spieleentwickler.

Literatur, Videos und verwendete Software

Literatur und Videos:

Diese YouTube-Playlist war die nützlichste Informationsquelle: https://www.youtube.com/watch?v=VO8rTszcW4s&list=PLsk-HSGFjnaH5yghzu7PcOzm9NhsW0Urw

Das Buch „Making Games with Python and Pygame“ und Teile vieler verschiedener PDF-Dateien, die übers Internet verstreut sind, waren auch hilfreich beim Lernen von Pygame und Spieleprogrammierung im Allgemeinen.

Software: Tobias: Visual Studio (IDE)

Kilian: Atom(IDE), PyCharm(IDE), Adobe Photoshop CC (Bildbearbeitung)

Inzwischen hat Kilian von Photoshop zu Krita gewechselt (ein 100% freies Programm).

„Preiset den heiligen St. IGNUcius, auch bekannt unter dem Namen „Richard Stallman“!“

Adel: Geany(IDE)

Eine aktuelle Version (06.04.2017) unseres Projekts haben wir bei Hightail hochgeladen:

* Das Projekt

ws1617/2d-jump_n_run.txt · Zuletzt geändert: 2017/04/17 20:05 von arik