Steering path following

Hier werden alle anderen GUI-Toolkits sowie Spezial-Toolkits wie Spiele-Engines behandelt.
hell
User
Beiträge: 40
Registriert: Montag 1. Oktober 2018, 18:01

Hallo ThomasL u.a
habe mal wieder ein Problen:
versuche die Ausführungen von Fernando Bevilaqcua Understanding Steering Behavior: Path Following in pygame und python umzusetzen.
Ein Vehikel folgt den Punkten eines Pfades.
Ein Pfad ist zusammengesetzt aus Linien und Punkten. Jeder Punkt des Pfades wird als target angesehen, sodass der seek-Algorithmus von Reynolds verwendet werden kann. Wenn ein bestimmter Abstand zu einem Punkt erreicht ist, wird der nächste Punkt gesucht, der Listenindex um 1 erhöht.
Problem: Wenn der letzte Punkt des Pfades gesucht wird, soll das Vehikel dort zur Ruhe kommen, der seek_with_approach - Algorithmus verwendet werden.
Frage: wie kann ich das machen, ohne eine Fehlermeldung wegen des Listenindex zu erhalten.
Für Tips wäre ich dankbar.

Code:

import pygame
from random import randint, uniform
import math
import copy

from vec2d import vec2d


WIDTH = 1000
HEIGHT = 640
FPS = 60
WHITE = (255, 255, 255)
BLACK = (0, 0, 0)
RED = (255, 0, 0)
GREEN = (0, 255, 0)
BLUE = (0, 0, 255)
YELLOW = (255, 255, 0)
DARKGRAY = (40, 40, 40)
CYAN = (0, 255, 255)
MAGENTA = (255, 0, 255)
BG_COLOR = (122, 150, 134)
#BG_COLOR = (150, 150, 80)

MAX_SPEED = 2
MAX_FORCE = 0.05
APPROACH_RADIUS = 50


AGENT_FILENAME = '../images/dreieck0.png'

screen = pygame.display.set_mode((WIDTH, HEIGHT))
pygame.init()

class Vehicle(pygame.sprite.Sprite):
def __init__(self, screen, img_filename, init_position):
pygame.sprite.Sprite.__init__(self)
self.screen = screen
self.screen_rect = screen.get_rect()
self.base_image = pygame.image.load(img_filename).convert_alpha()
self.image = self.base_image
self.image_w, self.image_h = self.image.get_size()
self.location = vec2d(init_position)
self.velocity = vec2d(MAX_SPEED, 0)
self.direction = vec2d(0,0)
self.acceleration = vec2d(MAX_SPEED, 0)
self.desired = vec2d(0, 0)
self.current_node = 0 # Listenindex der Liste path.nodes


def apply_force(self, force):
self.acceleration += force


def follow_path(self, path):
target = vec2d(0,0)
if path != None:
path.nodes = path.get_nodes()
target = path.nodes[self.current_node]

distance = self.location.get_distance(target)
if distance <= 10:
self.current_node += 1


if self.current_node >= len(path.nodes):
self.seek_with_approach(target)
self.current_node = len(path.nodes) - 1
self.seek(target)


def seek(self, target):
self.desired = (target - self.location).normalized()* MAX_SPEED
steer = (self.desired - self.velocity)
if steer.get_length() > MAX_FORCE:
steer *= (MAX_FORCE)

self.apply_force(steer)


def seek_with_approach(self, target):
""" wenn sich Vehikel innerhalb der Entfernung
APPROACH_RADIUS befindet bremse ab """
self.desired = (target - self.location)
dist = self.desired.get_length()
self.desired.normalized()
if dist < APPROACH_RADIUS:
self.desired *= dist / APPROACH_RADIUS * MAX_SPEED
else:
self.desired *= MAX_SPEED
steer = (self.desired - self.velocity)

if steer.get_length() > MAX_FORCE:
steer *= MAX_FORCE

self.apply_force(steer)


def update(self):
if self.velocity.get_length() > 0.00001:
self.direction = self.velocity.normalized()

# Make the agent point in the correct direction.
# Since our direction vector is in screen coordinates
# (i.e. right bottom is 1, 1), and rotate() rotates
# counter-clockwise, the angle must be inverted to
# work correctly.
# self.image = pygame.transform.rotate(self.base_image,
# -180 * math.atan2(self.direction.y , self.direction.x)
# / math.pi)

self.image = pygame.transform.rotate(self.base_image, -self.direction.angle)
self.image_w, self.image_h = self.image.get_size()

# equations of motion
self.velocity += self.acceleration
# begrenze velocity auf MAX_SPEED
if self.velocity.get_length() > MAX_SPEED:
self.velocity * MAX_SPEED

self.location += self.velocity
self.acceleration *= 0.0 # resette nach jedem update

self.rect = self.image.get_rect()
self.rect.topleft = (self.location.x, self.location.y)


# Zeichne Sprite
def blitme(self):
draw_pos = self.rect.move(
[-self.image_w / 2, -self.image_h / 2])
self.screen.blit(self.image, draw_pos)



class Path(pygame.sprite.Sprite):
def __init__(self):
pygame.sprite.Sprite.__init__(self)
self.screen = screen
self.screen_rect = screen.get_rect()
self.nodes = []


# Add a point to the path
def add_node(self, x, y):
node = vec2d(x, y)
self.nodes.append(node);

def get_start(self):
return self.nodes[0]

def get_end(self):
return self.nodes[-1]

def get_nodes(self):
return self.nodes

def display(self):
node_list = []
for node in self.nodes:
node_list.append(node)

pygame.draw.lines(screen, BLACK, False, node_list, 3)


# generiere Pfad aus Liniensegmenten
def new_path():
path = Path()
path.add_node(20, HEIGHT/2)
path.add_node(randint(0, WIDTH/2), randint(20, HEIGHT))
path.add_node(randint(WIDTH/2, WIDTH), randint(20, HEIGHT))
path.add_node(WIDTH-20, HEIGHT/2)
return path

###### main ######
def main() :
pygame.init()
clock = pygame.time.Clock()

# Generiere Pfad
path = new_path()

# einen Agent erzeugen
agent = pygame.image.load(AGENT_FILENAME).convert_alpha()
agent = Vehicle(screen, AGENT_FILENAME,
(WIDTH/2, HEIGHT/3))

running = True
while running:
clock.tick(FPS)
screen.fill(BG_COLOR)
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_ESCAPE:
running = False


path.display()

agent.follow_path(path)
agent.update()
agent.blitme()

pygame.display.set_caption('Path Following')
pygame.display.flip()

if __name__ == "__main__":
main()
__deets__
User
Beiträge: 14493
Registriert: Mittwoch 14. Oktober 2015, 14:29

Du machst doch jetzt schon ein paar Monate rum mit Python. Dabei ist dir doch sicher schon aufgefallen, dass Einrueckungen in Python relevant sind. Sonst kann der Interpreter den Code nicht auswerten, weil er nicht weiss, was zu was gehoert.

Und uns geht das genauso. Ich habe keine Ahnung, was von deinem Code wo wie eingerueckt ist. Und darum kann ich den nicht verstehen.

Es gibt hier im Editor den "</>"-Button, und damit kannst du deinen Code in Code-Tags setzen. Dann ist der fuer uns auch lesbar.
hell
User
Beiträge: 40
Registriert: Montag 1. Oktober 2018, 18:01

Hallo _deets_,
habe es leider zu spät gemerkt, was soll ich jetzt machen ,Code noch mal übermitteln??
Benutzeravatar
ThomasL
User
Beiträge: 1366
Registriert: Montag 14. Mai 2018, 14:44
Wohnort: Kreis Unna NRW

ja, das wäre nett und ich schau mir deinen Code morgen an. Ich sag jetzt gute Nacht.
Ich bin Pazifist und greife niemanden an, auch nicht mit Worten.
Für alle meine Code Beispiele gilt: "There is always a better way."
https://projecteuler.net/profile/Brotherluii.png
__deets__
User
Beiträge: 14493
Registriert: Mittwoch 14. Oktober 2015, 14:29

Ja, der Bearbeitungszeitraum ist abgelaufen. Musst du nochmal Posten.
hell
User
Beiträge: 40
Registriert: Montag 1. Oktober 2018, 18:01

_deets_ ,
ich habe keine Ahnung wie das geht ?
__deets__
User
Beiträge: 14493
Registriert: Mittwoch 14. Oktober 2015, 14:29

Deinen Code einkopieren, markieren, und den </>-Button drücken.
hell
User
Beiträge: 40
Registriert: Montag 1. Oktober 2018, 18:01

danke _deets_ für deine Geduld,
Hier also nochmal der Code:

Code: Alles auswählen

import pygame 
from random import randint, uniform 
import math
import copy

from vec2d import vec2d


WIDTH = 1000
HEIGHT = 640
FPS = 60
WHITE = (255, 255, 255)
BLACK = (0, 0, 0)
RED = (255, 0, 0)
GREEN = (0, 255, 0)
BLUE = (0, 0, 255)
YELLOW = (255, 255, 0)
DARKGRAY = (40, 40, 40)
CYAN = (0, 255, 255)
MAGENTA = (255, 0, 255)
BG_COLOR = (122, 150, 134)
#BG_COLOR = (150, 150, 80)

MAX_SPEED = 2
MAX_FORCE = 0.05
APPROACH_RADIUS = 50


AGENT_FILENAME = '../images/dreieck0.png'

screen = pygame.display.set_mode((WIDTH, HEIGHT))
pygame.init()

class Vehicle(pygame.sprite.Sprite):
    def __init__(self, screen, img_filename, init_position):
        pygame.sprite.Sprite.__init__(self)
        self.screen = screen
        self.screen_rect = screen.get_rect()
        self.base_image = pygame.image.load(img_filename).convert_alpha()
        self.image = self.base_image
        self.image_w, self.image_h = self.image.get_size()
        self.location = vec2d(init_position)
        self.velocity = vec2d(MAX_SPEED, 0)
        self.direction = vec2d(0,0)
        self.acceleration = vec2d(MAX_SPEED, 0)
        self.desired = vec2d(0, 0)
        self.current_node = 0 # Listenindex der Liste path.nodes
        
        
    def apply_force(self, force):
        self.acceleration += force    
       
     
    def follow_path(self, path):
        target = vec2d(0,0)
        if path != None:
            path.nodes = path.get_nodes()
            target = path.nodes[self.current_node]
            
            distance = self.location.get_distance(target)
            if distance <= 10:
                self.current_node += 1
            
    
                if self.current_node >= len(path.nodes):
                    self.seek_with_approach(target)    
                    self.current_node = len(path.nodes) - 1
        self.seek(target) 
                    
            
    def seek(self, target):
        self.desired = (target - self.location).normalized()* MAX_SPEED
        steer = (self.desired - self.velocity)
        if steer.get_length() > MAX_FORCE:
            steer *= (MAX_FORCE)
        
        self.apply_force(steer)
        
    
    def seek_with_approach(self, target):
        """ wenn sich Vehikel innerhalb der Entfernung
            APPROACH_RADIUS befindet bremse ab """
        self.desired = (target - self.location)
        dist = self.desired.get_length()
        self.desired.normalized()
        if dist < APPROACH_RADIUS:
            self.desired *= dist / APPROACH_RADIUS * MAX_SPEED
        else:
            self.desired *= MAX_SPEED
        steer = (self.desired - self.velocity)
        
        if steer.get_length() > MAX_FORCE:
            steer *= MAX_FORCE
        
        self.apply_force(steer)
        
      
    def update(self):
        if self.velocity.get_length() > 0.00001:
            self.direction = self.velocity.normalized()
        
        # Make the agent point in the correct direction.
        # Since our direction vector is in screen coordinates
        # (i.e. right bottom is 1, 1), and rotate() rotates
        # counter-clockwise, the angle must be inverted to
        # work correctly.
        # self.image = pygame.transform.rotate(self.base_image, 
        #             -180 * math.atan2(self.direction.y , self.direction.x) 
        #             / math.pi)
      
        self.image = pygame.transform.rotate(self.base_image, -self.direction.angle)
        self.image_w, self.image_h = self.image.get_size()
        
        # equations of motion
        self.velocity += self.acceleration
        # begrenze velocity auf MAX_SPEED
        if self.velocity.get_length() > MAX_SPEED:
            self.velocity * MAX_SPEED
        
        self.location += self.velocity
        self.acceleration *= 0.0 # resette nach jedem update
        
        self.rect = self.image.get_rect()
        self.rect.topleft = (self.location.x, self.location.y)
        
    
    # Zeichne Sprite
    def blitme(self):
        draw_pos = self.rect.move(
            [-self.image_w / 2, -self.image_h / 2])
        self.screen.blit(self.image, draw_pos)
        

        
class Path(pygame.sprite.Sprite):
    def __init__(self):
        pygame.sprite.Sprite.__init__(self)
        self.screen = screen
        self.screen_rect = screen.get_rect()
        self.nodes = []
        
        
    # Add a point to the path
    def add_node(self, x, y): 
        node = vec2d(x, y)
        self.nodes.append(node);
 
    def get_start(self):
        return self.nodes[0]
      
    def get_end(self):
        return self.nodes[-1]
    
    def get_nodes(self):
        return self.nodes
        
    def display(self):
        node_list = []
        for node in self.nodes:
            node_list.append(node)
        
        pygame.draw.lines(screen, BLACK, False, node_list, 3)
            

# generiere Pfad aus Liniensegmenten
def new_path():
    path = Path()    
    path.add_node(20, HEIGHT/2)
    path.add_node(randint(0, WIDTH/2), randint(20, HEIGHT))
    path.add_node(randint(WIDTH/2, WIDTH), randint(20, HEIGHT))
    path.add_node(WIDTH-20, HEIGHT/2)
    return path

######  main ######   
def main() :
    pygame.init()
    clock = pygame.time.Clock()
    
    # Generiere Pfad
    path = new_path()
    
    # einen Agent erzeugen
    agent = pygame.image.load(AGENT_FILENAME).convert_alpha()
    agent = Vehicle(screen, AGENT_FILENAME,
                   (WIDTH/2, HEIGHT/3))
    
    running = True
    while running:
        clock.tick(FPS)
        screen.fill(BG_COLOR)
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                running = False
            if event.type == pygame.KEYDOWN:
                if event.key == pygame.K_ESCAPE:
                    running = False
                
        
        path.display()
        
        agent.follow_path(path)
        agent.update()
        agent.blitme()
        
        pygame.display.set_caption('Path Following')
        pygame.display.flip()

if __name__ == "__main__":
    main()
__deets__
User
Beiträge: 14493
Registriert: Mittwoch 14. Oktober 2015, 14:29

Naja, wenn du zb 10 Knoten hast. Dann gehen deren Indizes von 0 bis 9. Und wenn du den letzten sonderbehandeln willst, dann musst du ja schon bei 8 etwas anderes machen. Und jetzt schau dir mal genau an, was der Wert ist, mit dem du stattdessen vergleichst. Und mit welchem Operator.

Aber selbst wenn du das Problem gelöst hast, kommt dein nächstes. Du musst ja nicht nur EINMMAL sonder so lange seek_with_approach aufrufen, bis du da bist.

Es gibt viele Wege das zu lösen. Ein etwas einfacherer wäre, ein Attribut self.seek_function zu haben, dem du zuerst self.seek und dann beim erreichen des letzten Knotens self.approach zuweist. Und in follow_path als letztes immer self.seek_function aufrufst. Um den Wechsel der Strategie wirklich mit zu machen.
hell
User
Beiträge: 40
Registriert: Montag 1. Oktober 2018, 18:01

hallo _deets_ ,
was du schreibst ist something beyond my scope, ich verstehe nicht wirklich etwas.
Das member seek_function 'verstehe' ich als boolesche Variable, mit der die beiden seek-Strategien eingeschaltet werden ??? Aber wie?
__deets__
User
Beiträge: 14493
Registriert: Mittwoch 14. Oktober 2015, 14:29

Dein jetztiger Code hat halt mehrere Probleme. Das erst ist der Index-basierte Zugriff. Hast du das verstanden? Warum du einen Index-Fehler bekommst?

Und der zweite ist, wie du zwischen zwei Verhaltensweisen/Strategien umschaltest. Im Moment tust du das *NICHT*. Du rufst genau *EINMAL* seek_with_approach auf. Danach nicht mehr. Und du musst es aber der Natur deines Problems nach aufrufen, bis die jeweils eigene Abbruchbedingung erfuellt ist.

Wie du dieses Umschalten erledigst ist deine Sache. Ich habe keine boolsche variable gemeint. Sondern so etwas hier (keine Klasse, aber mit self... geht das genauso):

Code: Alles auswählen

def strategy_a(i):
       print("a", i)

def strategy_b(i):
      print("b", i)


current_strategy = strategy_a

for i in range(100):
      if i > 90:
          current_strategy = strategy_b
      current_strategy(i)
Du koenntest auch immer mit eine if-Abfrage entscheiden, was jetzt aufgerufen werden soll, aber ich wuerde das nicht so machen, weil man mit dem Ansatz den ich gewaehlt habe schoene weitere Dinge machen kann, zb sowas hier:

Code: Alles auswählen

def strategy_c(i):
       print("c", i)

def strategy_a(i):
       print("a", i)
       if i > 90:
            return strategy_b
       elif relative_mondfeuchte() > 20:
             return strategy_c
       else:
            return strategy_a

def strategy_b(i):
      print("b", i)

current_strategy = strategy_a

for i in range(100):
      current_strategy(i)
Womit du den Strategien selbst die Steuerung ueberlaesst, und nicht den umliegenden Code anfassen musst.
hell
User
Beiträge: 40
Registriert: Montag 1. Oktober 2018, 18:01

Danke _deets_ für deine ausführliche Antwort,
die Codebeispiele muss ich mir genauer ansehen, das brauch eine Weile.
Mal sehen: Indexfehler deshalb, weil über Methode follow_path current_node weiter inkrementiert wird, bis zum Abbruch:
__deets__
User
Beiträge: 14493
Registriert: Mittwoch 14. Oktober 2015, 14:29

Ja, und das liegt an dieser Zeile:

if self.current_node >= len(path.nodes):

Wenn path.nodes 10 Elemente lang ist, was ist der Wert, den current_node dann haben muss, damit die Bedingung zutrifft?
hell
User
Beiträge: 40
Registriert: Montag 1. Oktober 2018, 18:01

hallo _deets_ ,
das ist unfair, du bringst mich ganz schön ins schleudern mit deinen Beispielen.
Wenn ich mir das Ergebnis von Beispiel eins anschaue , verstehe ich was du meinst. Aber gleich tun sich neue Fragen auf: z.B wie kannst
du diese Zuweisung machen: current_strategy = strategy_a ??
Offenbar wird der Rumpf der Funktion startegy_a(i) an die Variable current_strategy übergeben, die später in der Schleife als Funktion
auftaucht. Oder nicht??
Frage: wo und unter welcher Überschrift kann ich das nachlesen?
Und verzeihe mir meine Doofheit!
__deets__
User
Beiträge: 14493
Registriert: Mittwoch 14. Oktober 2015, 14:29

Sozusagen. Der “Rumpf” ist offiziell ein Code-Objekt. Aber Bezeichnung hin oder her: du kannst das an beliebige Namen binden. “def foo()....” erzeugt erstmal ein Code-Objekt und bindet es dann automatisch an den Namen foo. Du kannst es aber noch an tausend andere Namen binden. Oder in eine Datenstruktur stecken. Das ist Python ganz egal.

Und ich weiß nicht, wo einem sowas erklärt wird. Ich hab’s irgendwann verinnerlicht. Woher kann ich dir nicht sagen.
Benutzeravatar
__blackjack__
User
Beiträge: 13004
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@hell: Funktionen sind wie alles was man in Python an einen Namen binden kann Objekte. Also Werte. Damit kann man alles machen was man auch mit anderen Objekten machen kann – an andere Namen binden, als Argumente bei Aufrufen übergeben, in Datenstrukturen wie Listen oder Wörterbücher stecken, …

Es dürfte dafür keine gesonderte Überschrift geben, denn mehr gibt es dazu ja eigentlich nicht zu sagen. Es gibt einen Haufen Funktionen die Funktionen als Argumente erwarten (`map()`, `filter()`, …) und auch welche die Funktionen als Rückgabewerte haben (`functools`-Modul, `operator`-Modul, …) und im `operator`-Modul gibt es für jeden Operator den die Python-Syntax kennt, eine Funktion, falls man so etwas irgendwo zum Übergeben an andere Funktionen braucht.

Und ``lambda``-Ausdrücke sind in dem Zusammenhang noch interessant, wenn man eine einfache Funktion braucht, für die man nicht extra eine Funktion mit Namen definieren möchte.
“Most people find the concept of programming obvious, but the doing impossible.” — Alan J. Perlis
hell
User
Beiträge: 40
Registriert: Montag 1. Oktober 2018, 18:01

hallo _deets_ und _blackjack_
danke für eure Antworten .
Was mich halt ziemlich verblüfft hat, war, dass im Beispiel von _deets_
nur der Name der Funktion zugewiesen wurde ( und nicht der gesamte 'Funktionsausdruck ' : strategy_a(i) ) ,
und wie ihr sagt damit an einen anderen Namen gebunden wurde.
Mit der Zuweisung: current_strategy = strategy_a wird also eine Referenz
auf strategy_a(i) erstellt?
__deets__
User
Beiträge: 14493
Registriert: Mittwoch 14. Oktober 2015, 14:29

Nein, NICHT auf strategy_a(i). Woher soll denn das i kommen in dem Ausdruck "current_strategy = strategy_a"? Sowas kann man auch machen, das sieht dann aber anders aus.

Alles was da passiert ist, dass das callable, das du gerade unter dem Namen strategy_a finden kannst an einen anderen Namen gebunden wird. Du kannst auch

Code: Alles auswählen

foo = bar = pille = palle = furz = egal = das_objekt_formerly_known_as_strategy_a = strategy_a
machen. Dann hast du halt 7 anderen Namen, unter denen es zu finden ist.
hell
User
Beiträge: 40
Registriert: Montag 1. Oktober 2018, 18:01

ok _deets_ , ein letztes mal möchte ich deine wertvolle Zeit in Anspruch nehmen und poste diesen (nicht funktionierenden) Code zum Ausgangsproblem. Dabei habe ich dein Beispiel eins als Vorlage verwendet.

Code: Alles auswählen

def follow_path(self, path):
        
        target = vec2d(0,0)
        path.nodes = path.get_nodes()
        
            
        self.seek_function = self.seek
        
        for i in range(len(path.nodes)):
            target = path.nodes[i]
            distance = self.location.get_distance(target)
            
            if i >= len(path.nodes)-1:
                print(i)
                self.seek_function = self.seek_with_approach
            self.seek_function(i)

In meiner ersten Version wurde mit: distance <= 10: curren_node += 1 von node zu node iteriert. range iteriert ebenfalls über die Nodeliste,
ich habe keine Ahnung, wie das alles funktioniert.
An dieser Stelle will ich mich bei bei dir bedanken für deine Hilfe und Geduld, tolles Forum.
__deets__
User
Beiträge: 14493
Registriert: Mittwoch 14. Oktober 2015, 14:29

Wann wird denn follow_path aufgerufen? Was macht diese Funktion konzeptionell? Wie oft wird sie aufgerufen im Verhaeltnis dazu wieviele Wegepunkte du hast?
Antworten