Python NPC "laufen" lassen

Hier werden alle anderen GUI-Toolkits sowie Spezial-Toolkits wie Spiele-Engines behandelt.
Antworten
Tobs
User
Beiträge: 65
Registriert: Sonntag 29. September 2013, 11:11

Ich will ein Python Spiel programmieren, in dem computergesteuerte Charaktere vorkommen programmieren. Dazu wollte ich ein Funktion schreiben, die den NPC, wenn sie ausgeführt wird, um eine bestimmte Distanz Richtung Ziel bewegt.

Der Anfang sollte ungefähr so aussehen:

def bewegen(self,Zielkoordinaten,Distanz):
...

Ich hab schon viele überlegt, habe aber noch keine Lösung gefunden.

Ich möchte, dass der NPC in gerader Linie(sofern das durch die ganzen Pixel möglich ist) Stück ür Stück(je nach angegebener Distanz) zum Ziel läuft.

Bisher habe ich es nur geschafft, das er zuerst zu der gesuchten x-, oder y-Reihe läuft und dann erst zum eigentlichen Ziel.

Könntet ihr mir helfen, oder habt ihr eine Idee wie das zu bewerkstelligen wäre?

Vielen Dank schon mal im voraus!
BlackJack

@Tobs: Das Stichwort aus der Mathematik ist Vektor. Du müsstest Dich ein wenig mit Vektorrechnung befassen. Man könnte zum Beispiel einen von der Spielfigur zum Zielpunkt erstellen und dann die Länge des Vektors auf die zurück zu legende Distanz setzen und schon kann man die Spielfigur um diese Distanz auf das Ziel zubewegen. Ein eigener Datentyp würde sich anbieten, oder falls Pygame in einer aktuellen Version verwendet wird, gibt es dort schon etwas im `math`-Modul.
Tobs
User
Beiträge: 65
Registriert: Sonntag 29. September 2013, 11:11

Danke für die schnelle Antwort!

Ich informiere mich mal über Vektoren.

Gerade habe ich versucht mein Problem über die Funktion von Geraden zu lösen(y = Steigung * x + t), t brauch ich da nicht, denke ich.

Hier mein bisheriges Programm:

-----------------------------------------------------------------------------------------------------------------------------------------------------------------

import pygame

list = []

class Castle:

def __init__(self,Ort):
self.Life = 10000
self.pos = Ort
self.image = pygame.image.load("Burg.png")

class Ritter:

def __init__(self,Ort,Richtung,Bild):
self.Life = 100
self.pos = Ort
self.speed = Richtung
self.image = Bild

def bewegen(self,Ziel):
x_diff = Ziel[0] - self.pos[0]
y_diff = Ziel[1] - self.pos[1]
if not y_diff == 0 or not x_diff ==0:
m = y_diff/x_diff
if m < 1:
self.speed = [1,y_diff/x_diff]
elif m > 1:
self.speed = [x_diff/y_diff,1]
elif m == 1:
self.speed = [1,1]
else:
if x_diff == 0 and y_diff != 0:
self.speed = [0,1]
elif y_diff == 0 and x_diff != 0:
self.speed = [1,0]
elif x_diff == 0 and y_diff == 0:
self.speed = [0,0]
self.pos[0] = self.pos[0] + self.speed[0]
self.pos[1] = self.pos[1] + self.speed[1]

pygame.init()
screen = pygame.display.set_mode([1280,720])
Clock = pygame.time.Clock()
running = True
Burg1 = Castle([1180,10])
Burg2 = Castle([10,620])
list = [Burg1,Burg2]
Ritterlist = []
Position = [640,360]
Ritterbild = pygame.image.load("Ritter.png")

while running:
Clock.tick(100)
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
if event.type == pygame.MOUSEBUTTONDOWN:
Ritter.pos = event.pos
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_SPACE:
Ritter = Ritter(Position,[0,0],Ritterbild)
Ritterlist.append(Ritter)
screen.fill([0,255,0])
for Ritter in Ritterlist:
Ritter.bewegen(Position)
screen.blit(Ritter.image,Ritter.pos)
for Castle in list:
screen.blit(Castle.image,Castle.pos)
pygame.display.flip()

-----------------------------------------------------------------------------------------------------------------------------------------------------------------

Tut mir leid wenn es etwas unübersichtlich ist.
Tobs
User
Beiträge: 65
Registriert: Sonntag 29. September 2013, 11:11

sry ich merk grad, dass er die 4 Leerzeichen vor einem untergeordneten Block immer weg hat.

Code: Alles auswählen

import pygame

list = []

class Castle:
    
    def __init__(self,Ort):
        self.Life = 10000
        self.pos = Ort
        self.image = pygame.image.load("Burg.png")
        
class Ritter:
    
    def __init__(self,Ort,Richtung,Bild):
        self.Life = 100
        self.pos = Ort
        self.speed = Richtung
        self.image = Bild
        
    def bewegen(self,Ziel):
        x_diff = Ziel[0] - self.pos[0]
        y_diff = Ziel[1] - self.pos[1]
        if not y_diff == 0 or not x_diff ==0:
            m = y_diff/x_diff
            if m < 1:
                self.speed = [1,y_diff/x_diff]
            elif m > 1:
                self.speed = [x_diff/y_diff,1]
            elif m == 1:
                self.speed = [1,1]
        else:
            if x_diff == 0 and y_diff != 0:
                self.speed = [0,1]
            elif y_diff == 0 and x_diff != 0:
                self.speed = [1,0]
            elif x_diff == 0 and y_diff == 0:
                self.speed = [0,0]
        self.pos[0] = self.pos[0] + self.speed[0]
        self.pos[1] = self.pos[1] + self.speed[1]
        
pygame.init()
screen = pygame.display.set_mode([1280,720])
Clock = pygame.time.Clock()
running = True
Burg1 = Castle([1180,10])
Burg2 = Castle([10,620])
list = [Burg1,Burg2]
Ritterlist = []
Position = [640,360]
Ritterbild = pygame.image.load("Ritter.png")

while running:
    Clock.tick(100)
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False
        if event.type == pygame.MOUSEBUTTONDOWN:
            Ritter.pos = event.pos
        if event.type == pygame.KEYDOWN:
            if event.key == pygame.K_SPACE:
                Ritter = Ritter(Position,[0,0],Ritterbild)
                Ritterlist.append(Ritter)
    screen.fill([0,255,0])
    for Ritter in Ritterlist:
        Ritter.bewegen(Position)
    screen.blit(Ritter.image,Ritter.pos)
    for Castle in list:
        screen.blit(Castle.image,Castle.pos)
    pygame.display.flip()]
Weiß nicht ob es so besser ist.
Zuletzt geändert von Anonymous am Sonntag 29. September 2013, 12:55, insgesamt 1-mal geändert.
Grund: Quelltext in Python-Code-Tags gesetzt.
Tobs
User
Beiträge: 65
Registriert: Sonntag 29. September 2013, 11:11

Nein, sry ich bin zu blöd um den Code richtig einzufügen :-(
BlackJack

@Tobs: Die Namensgebung weicht vom Style Guide for Python Code ab. Insbesondere Namen die mit Grossbuchstaben beginnen sollte man nicht für etwas anderes als Klassen verwenden. (Ausgenommen Konstanten die konventionell komplett in Grossbuchstaben benannt werden.)

Dann hätte man auch nicht das Problem dass man sich Klassennamen mit anderen Objekten überschreibt wie das zum Beispiel mit `Castle` und `Ritter` passiert.

Du solltest Dich vielleicht auch auf eine Sprache bei der Benennung festlegen. Englisch würde sich anbieten. Die gleichen Sachen mal `pos` und mal `Ort` oder mal `Burg` und mal `Castle` oder mal `image` und mal `Bild` zu nennen ist nicht übersichtlich.

Der Name `speed` hat eine völlig andere Bedeutung als `Richtung`.

Ebenfalls vermeiden sollte man das verdecken von eingebauten Namen wie `list`. Wenn Du da eine Liste dran bindest, kannst Du nicht mehr auf die `list()`-Funktion zugreifen. Und Leser erwarten bei dem Namen `list` die eingebaute Funktion und nicht eine Liste mit Objekten darin.

Die konkrete Datenstruktur sollte auch nicht im Namen stehen. Wenn man dann später aus der `Ritterlist` zum Beispiel eine `pygame.sprite.Group` machen möchte, muss man überall den Namen ändern oder man hat einen irreführenden Namen im Quelltext stehen. Englisch für die Namensgebung hätte hier den Vorteil, dass sich die Mehrzahlform von Hauptwörtern fast immer von der Einzahl unterscheidet. Deshalb kann man Container-Objekte einfach in der Mehrzahl benennen. Beispiel: ``for knight in knights:``. Da hätte man bei `ritter` ein Problem.

Die Namensgebung von Objekten und Klassen führt beim `Ritter` zu einem ziemlichen Durcheinander. Solange nicht mindestens ein Ritter in der Liste existiert, ist dieser Name innerhalb der ``while``-Schleife an die *Klasse* gebunden. Wenn man also eine Maustaste betätigt wird auf der *Klasse* ein `pos`-Attribut gesetzt. Sowie man `Ritter`-Objekte in der Liste hat, ist der Name am Anfang jeder Schleife an das letzte `Ritter`-Objekt aus der Liste gebunden und die Maustaste führt dann dazu das auf *diesem* Objekt das `pos`-Attribut neu gebunden wird. Das ist sehr verwirrend und das setzen des Attributs auf der Klasse würde ich als Programmfehler bezeichnen. Wenn der Code vom Hauptprogramm nicht auf Modulebene sondern in einer Funktion stehen würde, währe dieser Fehler aufgefallen, weil dann das setzen eines Attributs auf einem noch nicht existierenden lokalen Namen zu einer Ausnahme geführt hätte. Ausserdem kann maximal ein Ritter erstellt werden, weil danach der Name `Ritter` nicht mehr an die Klasse gebunden ist.

Die Positionen für Burgen und Ritter sollte man aus der Spielfeldgrösse berechnen. Wenn man die ändert muss man sich dann nicht um die anderen Positionen kümmern.

Anstelle der Positionen würde ich mit `pygame.rect.Rect`-Objekten arbeiten. Die haben viele nützliche Attribute und Methoden. Den Mittelpunkt von einem `Surface` kann man so zum Beispiel sehr einfach bestimmen ohne dass man selber etwas ausrechnen muss: ``new_knight_position = screen.get_rect().center``

Der nächste Schritt wäre dann die `Sprite` und Gruppenklassen aus `pygame.sprite` zu verwenden, die ein Bild und ein `Rect` verwenden.
Tobs
User
Beiträge: 65
Registriert: Sonntag 29. September 2013, 11:11

Wow, ihr seit ja alle ziemliche Profis hier.

Ich hab mal gehört, dass man Variablen mit kleinbuchstaben und Klassen mit großbuchstaben beginnen soll.

Mit Vektoren bin ich leider nicht viel weiter gekommen, wikipedia war nicht wirklich eine Hilfe :-(

In Zukunft passe ich meine Programmierstil an diese Regeln an, wenn ich hier Code poste.

Danke, dass du dir die Zeit genommen hast so eine ausführliche Antwort zu schreiben.

Wenn du dich mit Vektoren auskennst:

Könntest du mir ein Beispielprogramm schreiben:

In dem man Start und Ziel eingibt,
und dann z.B. ein roter Kreis von einem zum anderen Punkt läuft,
damit ich mir den Code anschauen kann und bei meinem Programm weiterkomme?

Danke für eure Antworten wie immer im voraus!!!
Benutzeravatar
Madmartigan
User
Beiträge: 200
Registriert: Donnerstag 18. Juli 2013, 07:59
Wohnort: Berlin

Tobs hat geschrieben:[...]
Könntest du mir ein Beispielprogramm schreiben:
[...]
Was nützt dir ein "Beispielprogramm", wenn dir die Kenntnisse zur grundlegenden Funktionsweise zu fehlen scheinen?
Schau einfach mal im Netz, Wikipedia ist nicht die einzige Ressource.

http://www.youtube.com/watch?v=jzOa-6AU0z4

Ansonsten erinnere ich immer wieder gern an die üblichen Verdächtigen: Mathematik-Lehrbücher aus der Schule. Da ist Lineare Algebra und speziell Vektorrechnung umfassend behandelt.

Nur Lesen, Lernen, Probieren, Scheitern und wieder Probieren hilft, Beispielprogramme anderer Leute eher selten. ;-)
Benutzeravatar
/me
User
Beiträge: 3554
Registriert: Donnerstag 25. Juni 2009, 14:40
Wohnort: Bonn

Madmartigan hat geschrieben:Ansonsten erinnere ich immer wieder gern an die üblichen Verdächtigen: Mathematik-Lehrbücher aus der Schule.
Als ich vor langen Jahren auf dem C=64 Sechsecke zeichnen wollte war ich sehr froh darüber, die trigonometrischen Funktionen noch halbwegs im Kopf zu haben. In die alten Mathebücher habe ich, wenn ich mich recht erinnere, auch geschaut.
Tobs
User
Beiträge: 65
Registriert: Sonntag 29. September 2013, 11:11

Danke für eure Tipps.

In Mathe hatten wir sowas bis jetzt noch nicht,
aber in dem Video wurde das ganze sehr ausführlich erklärt,
und ich glaube ich hab es jetzt endlich verstanden.

Gibt es hier im Forum so etwas wie ein Antwortniveau?
Wenn ja würde ich euch gerne ein positive Feedback geben.

Danke für eure schnellen, und ausführlichen Antworten!
Benutzeravatar
/me
User
Beiträge: 3554
Registriert: Donnerstag 25. Juni 2009, 14:40
Wohnort: Bonn

Tobs hat geschrieben:Gibt es hier im Forum so etwas wie ein Antwortniveau?
Nein. Die Regulars erkennen das Niveau ohnehin schnell genug. :D
Tobs
User
Beiträge: 65
Registriert: Sonntag 29. September 2013, 11:11

Es scheint soweit zu klappen bis auf einen Fehler:

Bei bestimmten Zielkoordinaten(z.B.[0,0]) versucht er durch 0 zu teilen, was natürlich nicht geht.

Ich hab schon versucht es mit "if" zu umgehen, aber es kommt immer wieder und ich hab keine Ahnung warum :-(

Code: Alles auswählen

# -*- coding: cp1252 -*-


import pygame
import easygui


class Ball:
    
    def __init__(self,pos):
        self.pos = pos
        self.int_pos = pos
        
    def bewegen(self,target,distance):
        #Berechnen des Vektors und dessen Betrags
        vektor = [target[0] - self.pos[0] , target[1] - self.pos[1]]
        b_vektor = [(vektor[0]**2)**0.5 , (vektor[1]**2)**0.5]
        #Falls keine der gegebenen Koordinaten erreicht ist
        if not b_vektor[0] == 0 or not b_vektor[1] == 0:
            #Reduzieren des Vektors
            if b_vektor[0] > b_vektor[1]:
                vektor = [vektor[0] / b_vektor[0] , vektor[1] / b_vektor[1]]
            elif b_vektor[1] > B_vektor[0]:
                vektor = [vektor[0] / b_vektor[1] , vektor[1] / b_vektor[1]]
            elif b_vektor[0] == B_vektor[1]:
                vektor = [vektor[0] / b_vektor[0] , vektor[1] / b_vektor[1]]
            #Addieren des Vektors zur Position je nach der Variable distance
            self.pos[0] = self.pos[0] + distance * vektor[0]
            self.pos[1] = self.pos[1] + distance * vektor[1]
        #Falls eine Zielkoordinate schon erreicht ist
        elif vektor[0] == 0 or vektor[1] == 0:
            if vektor[0] == 0:
                vektor = [0 , vektor[1] / b_vektor[1]]
            elif vektor[1] == 0:
                vektor = [vektor[0] / b_vektor[0] , 0]
        
        self.int_pos = [int(self.pos[0]) , int(self.pos[1])]
        

screen = pygame.display.set_mode([640,480])
clock = pygame.time.Clock()

this_ball = Ball([320,240])
finish = []
finish.append(int(easygui.enterbox("Gib die x-Koordinate des Ziels ein.")))
finish.append(int(easygui.enterbox("Gib die y-Koordinate des Ziels ein.")))
step_distance = int(easygui.enterbox("Gib ein wie schnell sich der Ball bewegen soll."))

running = True

while running:
    clock.tick(100)
    this_ball.bewegen(finish,step_distance)
    screen.fill([0,0,0])
    pygame.draw.circle(screen,[255,0,0],this_ball.int_pos, 25, 0)
    pygame.display.flip()]
Danke im voraus
Zuletzt geändert von Anonymous am Montag 30. September 2013, 16:40, insgesamt 1-mal geändert.
Grund: Quelltext in Python-Code-Tags gesetzt.
Sirius3
User
Beiträge: 17711
Registriert: Sonntag 21. Oktober 2012, 17:20

@Tobs: was soll denn ein b_vektor sein?
Um eine gewisse Distanz pro Schritt zurückzulegen zu können, mußt Du den Vektor vektor auf 1 normieren, das heißt also, die x- und y-Komponente durch die Länge (wurzel(x²+y²)) des Vektors teilen. Wenn Du das Ziel erreicht hast, ist die Länge 0, dann brauchst Du auch nicht mehr teilen.
Benutzeravatar
Madmartigan
User
Beiträge: 200
Registriert: Donnerstag 18. Juli 2013, 07:59
Wohnort: Berlin

Du kannst vor der Normierung auf eine mögliche Distanz von 0 prüfen, damit vermeidest du in der nächsten Iteration die Division.

Eleganter wäre ggf. die Variante, ein Epsilon zu definieren, welches das Abbruchkriterium für deine Division darstellt. Unterschreitet der Betrag des Vektors vom Startpunkt zum Endpunkt das Epsilon, dann brichst du vor der Division ab und setzt die Position des NPCs auf den Zielpunkt. Damit es dort dann keinen Snapping-Effekt gibt, muss das Epsilon entsprechend klein gewählt sein.
Zuletzt geändert von Madmartigan am Montag 30. September 2013, 20:45, insgesamt 2-mal geändert.
Benutzeravatar
Hyperion
Moderator
Beiträge: 7478
Registriert: Freitag 4. August 2006, 14:56
Wohnort: Hamburg
Kontaktdaten:

BlackJack hat geschrieben:Da hätte man bei `ritter` ein Problem.
Ach i wo... ``for ritter in rittersleut`` :mrgreen:
encoding_kapiert = all(verstehen(lesen(info)) for info in (Leonidas Folien, Blog, Folien & Text inkl. Python3, utf-8 everywhere))
assert encoding_kapiert
Tobs
User
Beiträge: 65
Registriert: Sonntag 29. September 2013, 11:11

@Hyperion:
Danke für den Hinweis :D
@Sirius3:
b_vektor enthält die Beträge von vektor(weil sich ja sonst beim dividieren das Vorzeichen umdrehen würde)
Danke für den Tipp!!! :o
EyDu
User
Beiträge: 4881
Registriert: Donnerstag 20. Juli 2006, 23:06
Wohnort: Berlin

Hyperion hat geschrieben:
BlackJack hat geschrieben:Da hätte man bei `ritter` ein Problem.
Ach i wo... ``for ritter in rittersleut`` :mrgreen:
:mrgreen:
Tobs hat geschrieben:b_vektor enthält die Beträge von vektor(weil sich ja sonst beim dividieren das Vorzeichen umdrehen würde)
Der Betrag ist ein eindimensionaler Wert und entspricht der Länge des Vektors. Dein "Betrag" hat jedoch zwei Elemente. Bei der Gelegenheit solltest du dir mal Anschauen, wass für ein Ergebnis du bei ``vektor = [vektor[0] / b_vektor[0] , vektor[1] / b_vektor[1]]`` herausbekommst. Das ist nämlich relativ langweilig. Und natürlich stellt sich die Frage: Warum hast du ein if/elif/elif-Konstrukt, wenn iin allen drei Fällen das selbe getan wird? ;-)

Korrekt für den Betrag ist:

Code: Alles auswählen

length = (vector[0]**2 + vector[1]**2)**0.5
Das Leben ist wie ein Tennisball.
BlackJack

@Tobs: An der Stelle könntest Du vielleicht auch mal eine kleine Pause vom Spiel nehmen und als Nebenprojekt den Vektor als Datentyp implementieren. Das eignet sich nämlich ganz gut um sich mal mit Klassen, Werttypen, und einigen von den ”magischen” Methoden zu beschäftigen. So dass man zum Beispiel mit Vektoren rechnen kann, oder den Betrag mit ``abs(vector)`` bestimmen kann, und so weiter.
Tobs
User
Beiträge: 65
Registriert: Sonntag 29. September 2013, 11:11

Danke, es funktioniert :D

Könntet ihr bitte mir noch schnell sagen, wie man in pygame einfache Textnachrichten einblendet?

Im voraus schonmal Danke!
Tobs
User
Beiträge: 65
Registriert: Sonntag 29. September 2013, 11:11

Okay, ich hab rausgefunden, dass ich vergessen habe pygame zu initialisieren :roll:

Trotzdem Danke :D

Hier ist der fertige Code:

Code: Alles auswählen

# -*- coding: cp1252 -*-


import pygame


class Ball:
    
    def __init__(self,pos):
        self.pos = pos
        self.int_pos = pos
        
    def bewegen(self,target,distance):
        vektor = [target[0] - self.pos[0] , target[1] - self.pos[1]]
        vektor_distance = (vektor[0]**2+vektor[1]**2)**0.5
        if not vektor_distance == 0:
            vektor[0] = vektor[0] / vektor_distance
            vektor[1] = vektor[1] / vektor_distance
        if distance > vektor_distance:
            distance = vektor_distance
        self.pos[0] = self.pos[0] + vektor[0] * distance
        self.pos[1] = self.pos[1] + vektor[1] * distance
        
        self.int_pos = [int(self.pos[0]) , int(self.pos[1])]


pygame.init()
screen = pygame.display.set_mode([640,480])
clock = pygame.time.Clock()
this_ball = Ball([320,240])
step_distance = int(raw_input("Geschwindigkeit in 100Pixel/Sekunde: "))
message1_str = "Klick mit der Maus irgendwo hin!"
message2_str = "Du kannst die Maus auch gedrückt halten!"
gedrueckt = False
running = True

while running:
    clock.tick(100)
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False
        if event.type == pygame.MOUSEBUTTONDOWN:
            gedrueckt = True
            finish = event.pos
        if event.type == pygame.MOUSEBUTTONUP:
            gedrueckt = False
        if event.type == pygame.MOUSEMOTION:
            if gedrueckt:
                finish = event.pos
    try:
        this_ball.bewegen(finish,step_distance)
    except:
        pass
    message1_font = pygame.font.Font(None, 26)
    message1_surf = message1_font.render(message1_str, 1, (255, 255, 255))
    message2_font = pygame.font.Font(None, 26)
    message2_surf = message2_font.render(message2_str, 1, (255, 255, 255))
    screen.fill([0,0,0])
    screen.blit(message1_surf, [10, 10])
    screen.blit(message2_surf, [10, 50])
    pygame.draw.circle(screen,[255,255,0],this_ball.int_pos, 25, 0)
    pygame.display.flip()
Danke für eure Hilfe! :mrgreen:
Antworten