Steering Hide, Interpose

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 Forumsmitglieder
ich bin immer noch mit Reynolds Steering Algorithmen zu Gange. Im Moment versuche ich mich an Hide. Ein Vehikel versteckt hinter einem
Hindernis vor einem anderen Agenten, der über den Bildschirm wandert. Was den Algoritmus betrifft, so orientiere ich mich an Matt Buckland, Programming Game AI ...,ch 03. Das Programm "läuft", aber eben nur "läuft".
Probleme: lasse ich den Hunter wandern, so schert der sich nur bedingt um die Hindernisse, die auf seinem Weg sind. Ebenso verhält sich das
Vehikel, welches sich versteckt. Das Verhalten ist überhaupt nicht ausbalanciert, so scheint das Wanderverhalten das Hindernisvermeidungsverhalten zu überlagern. Ähnliches Probleme hatte ich schon beim Interposealgorithmus, stelle ich den Agenten Hindernisse in den Weg, so gab es beschriebene Probleme.
Wenn ich mir die Beispielprogramme von Buckland (wine) ansehe, so erblasse ich vor Neid.
Ich weiss, dass es verschiedene Methoden gibt, die Agenten zu ausgewogenerem Verhalten zu bringen, aber wie? Die C++-Programme von
Buckland sind mir eiiiiiiinige Nummern zu weit entfernt, als dass ich sie auch nur ein kleines bisschen verstehe.
Frage: gibt es eine Möglichkeit, die nicht zu komplex ist, das Verhalten der Vehikel etwas ausgewogener zu gestalten.
Und noch eine Frage: der Methode hide(hunter, obstacles) übergebe ich eine Instanz von Vehicel, wie kann ich diese Methode in update aufrufen (statt in main ), ohne Fehlermeldung (hunter nicht bekannt) ?
Für Tips wäre ich dankbar, hell.

Hier der Code:

Code: Alles auswählen

import os, sys
import pygame 
from random import randint, uniform, choice
import math

sys.path.append('..')
#from vec2d import vec2d
Vec2 = pygame.math.Vector2

WIDTH = 840
HEIGHT = 680
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)
CYAN = (0, 255, 255)
DARKGRAY = (40, 40, 40)
#BG_COLOR = (122, 150, 134)
BG_COLOR = (150, 150, 80)

FLEE_DISTANCE = 200

LOOK_AHEAD = 80
WANDER_CIRCLE_DISTANCE = 160
WANDER_CIRCLE_RADIUS = 20
CHANGE = 0.2

MAX_AVOID_FORCE = 0.3
APPROACH_RADIUS = 50
NUM_WALLS = 4
WALL_LIMIT = 40
SIZE_W = 80
SIZE_H = 80
walls = []

AGENT_FILENAME1 = '../images/bluecreep_0.png'
AGENT_FILENAME2 = '../images/old_pinkcreep_0.png'
AGENT_FILENAME3 ='../images/yellowcreep_0.png'
    
screen = pygame.display.set_mode((WIDTH, HEIGHT))

class Vehicle(pygame.sprite.Sprite):
    def __init__(self, screen, img_filename, init_position, max_speed, max_force):
        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.location = Vec2(init_position)
        self.image_w, self.image_h = self.image.get_size()
        self.rect = self.image.get_rect()
        self.rect.center = self.location
        self.location = Vec2(init_position)
        self.max_speed = max_speed
        self.max_force = max_force
        self.velocity = Vec2(self.max_speed,self.max_speed)
        self.acceleration = Vec2(0, 0)
        self.direction = Vec2(0,0)
        self.wanderangle = 0.0
       
    def apply_force(self, force):
        self.acceleration += force
    
    def seek(self, target):
        desired = (target - self.location).normalize() * self.max_speed
        steer = (desired - self.velocity)
        if steer.length() > self.max_force:
            steer.scale_to_length(self.max_force)
        
        self.apply_force(steer)
        
    #def seek_with_approach(self, target):
    #    """ wenn sich Vehikel innerhalb der Entfernung
    #        APPROACH_RADIUS befindet bremse ab """
    #    desired = (target - self.location)
    #    dist = desired.length()
    #    desired.normalize_ip()
    #    if dist < APPROACH_RADIUS:
    #        desired *= dist / APPROACH_RADIUS * self.max_speed
    #    else:
    #        desired *= self.max_speed
    #    steer = desired - self.velocity
        
    #    if steer.length() > self.max_force:
    #        steer * self.max_force
        
    #    self.apply_force(steer)
    
    def seek_with_approach(self, target):
        """ nach Bucklands arrive-Algorithmus  """
        deceleration = 5
        to_target = Vec2(0, 0)
        to_target = target - self.location
        # berechne die Entfernung zum target
        dist = to_target.length() 
        if dist > 0:
            deceleration_tweaker = 3
            #calculate the speed required to reach the target given the desired
            #deceleration
            speed = dist / (deceleration * deceleration_tweaker)
            #make sure the velocity does not exceed the max
            speed = min(speed, self.max_speed)
            #from here proceed just like Seek except we don't need to normalize
            #the ToTarget vector because we have already gone to the trouble
            #of calculating its length: dist.
            #desired = Vec2(0, 0)
            desired = to_target * speed / dist
            self.apply_force(desired - self.velocity)
        self.apply_force(Vec2(0,0))
    
        
    def evade(self,hunter):
        # berechne Entfernung zwischen pursuer und evader
        distance = hunter.location - self.location
        # -> nach: SteeringBehavior.java (Buckland)
        look_ahead = distance.length() / (self.max_speed + hunter.velocity.length())
        future_position = hunter.location + hunter.velocity * look_ahead
        # Aufruf der Methode seek_with_approach
        self.flee(future_position)
        
    def flee(self, target):
        """ Reynolds steering Algorithmus: flee 
            wenn sich Mover innerhalb der FLEE_DISTANCE
            befindet """
        dist = self.location - target
        if dist.length() < FLEE_DISTANCE:
            desired = (self.location - target).normalize() * self.max_speed
        else:
            desired = self.velocity.normalize() * self.max_speed
        steer = desired - self.velocity   
        if steer.length() > self.max_force:
            steer.scale_to_length(self.max_force)
        self.apply_force(steer)
        
    
    def get_hiding_position(self,pos_obst, radius_obst, pos_target):
        """ pos_obst: Vec2, position eines Hindernisses(Kreis) 
            radius_obst: float, Radius des Hindernisses
            pos_target: Vec2, position des Targetagenten """ 
            
        # calculate how far away the agent is to be from the chosen obstacle’s
        # bounding radius
        dist_from_boundary = 20.0
        dist_away = radius_obst + dist_from_boundary
        # calculate the heading toward the object from the target
        to_obst = Vec2(0, 0)
        to_obst = (pos_obst - pos_target).normalize()
        
        #print(to_obst) # debug
        # scale it to size and add to the obstacle's position to get
        # the hiding spot
        to_obst.scale_to_length(dist_away)
        return to_obst + pos_obst
       
    
    def hide(self, hunter, walls):
        world_record = 1000000
        dist_to_closest = world_record
        best_hiding_spot = Vec2(0,0)
        #iteriere über alle Kreise ( Hindernisse)
        #cur_obst = Vec2(0.0,0.0)
        for cur_obst in walls:
            #calculate the position of the hiding spot for this obstacle
            hiding_spot = Vec2(0,0)
            hiding_spot = self.get_hiding_position(cur_obst.location,
                                                    cur_obst.size_w,
                                                    hunter.location)
            #print(hiding_spot) # debug
            # find closest hiding_spot to the agent                                   
            dist = hiding_spot.distance_to(hunter.location)                                 
            if dist < dist_to_closest:
                dist_to_closest = dist
                best_hiding_spot = hiding_spot
        # if no suitable obstacles found then evade the target
        if  dist_to_closest == world_record:
            steer = self.evade(hunter)
            return steer
        else: #use Arrive on the hiding spot 
            steer =  self.seek_with_approach(best_hiding_spot)
            return steer
            
    def avoid_walls(self):
        steer = Vec2(0, 0)
        desired = Vec2(0, 0)
        near_wall = False
        if self.location.x < WALL_LIMIT:
            desired = Vec2(self.max_speed, self.velocity.y)
            near_wall = True
        if self.location.x > WIDTH - WALL_LIMIT:
            desired = Vec2(-self.max_speed, self.velocity.y)
            near_wall = True
        if self.location.y < WALL_LIMIT:
            desired = Vec2(self.velocity.x, self.max_speed)
            near_wall = True
        if self.location.y > HEIGHT - WALL_LIMIT:
            desired = Vec2(self.velocity.x, -self.max_speed)
            near_wall = True
        if near_wall:
            steer = (desired - self.velocity)
            if steer.length() > self.max_force:
                steer.scale_to_length(self.max_speed)
        self.apply_force(steer)
        
    def find_most_threatening(self, walls, ahead, ahead2):
        most_threatening = None
        for wall in walls:
            collision = self.line_intersect_circle(wall, ahead, ahead2)
            
            if collision and (not most_threatening  or 
                self.location.distance_to(wall.location) < 
                self.location.distance_to(most_threatening.location)):
                    most_threatening = wall
        return most_threatening
        
    def line_intersect_circle(self, wall,  ahead, ahead2 ):
        wall_center = Vec2(wall.location)
        dist1 = wall_center.distance_to(ahead)
        dist2 = wall_center.distance_to(ahead2)
        
        return (dist1 <= wall.size_w)  or (dist2 <= wall.size_w )
    
    def collision_avoidance(self,wall):
        steer = Vec2(0, 0)
        ahead = self.location + self.velocity.normalize() * LOOK_AHEAD
        ahead2 = self.location + self.velocity.normalize() * LOOK_AHEAD * 0.5
        
        most_threatening = self.find_most_threatening(walls, ahead, ahead2)
        if most_threatening:
           steer = ahead - most_threatening.location
           steer.normalize_ip() 
           #steer *=  MAX_AVOID_FORCE
           steer.scale_to_length(MAX_AVOID_FORCE)
        else:
           steer = Vec2(0, 0)
        
        self.apply_force(steer)
       
    def wander(self):
        """ Reynolds steering Algorithmus: wander
        circle_pos : Position des Mittelpunktes des Wanderkreises """
        # berechne Mittelpunkt des Wanderkreises
        circle_pos = self.velocity.normalize() * WANDER_CIRCLE_DISTANCE
        # und relativ zur Position des Vehikels
        circle_pos += self.location
        # ändere den wanderangle ein klein wenig
        self.wanderangle += uniform(-CHANGE, CHANGE)
        # berechne displacement-Vektor
        self.displacement = Vec2(WANDER_CIRCLE_RADIUS* math.cos(self.wanderangle),
                        WANDER_CIRCLE_RADIUS * math.sin(self.wanderangle))
        # berechne wanderforce 
        target = Vec2(circle_pos + self.displacement)
        # zum anzeigen des displacement-Vektors
        return self.seek(target)
        
    def update(self):
        if self.velocity.length() > 0.00001:
            self.direction = self.velocity.normalize()
        # 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)
        # wenn vec2d verwendet wird:
        #self.image = pygame.transform.rotate(self.base_image, -self.direction.angle)
        #self.image_w, self.image_h = self.image.get_size()
        self.velocity.scale_to_length(self.max_speed)
        # equations of motion
        self.velocity += self.acceleration
        # begrenze velocity auf MAX_SPEED
        if self.acceleration.length() > 0.000001:
            self.velocity.scale_to_length(self.max_speed)
        if self.velocity.length() > self.max_speed:
            self.velocity.scale_to_length(self.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)
        # wenn agent Fenstergrenzen erreicht
        if self.location.x > WIDTH:
            self.location.x = 0
        if self.location.x < 0:
            self.location.x = WIDTH
        if self.location.y > HEIGHT:
            self.location.y = 0
        if self.location.y < 0:
            self.location.y = HEIGHT
        self.rect.center = self.location
       
     # zeichne sprites
    def blitme(self):
        draw_pos = self.rect.move(
            [-self.image_w / 2, -self.image_h / 2])
        self.screen.blit(self.image, draw_pos)   
    
    def draw_vectors(self):
        """ zeichne Pfeilrepräsentanten der Vektoren
            velocity und desired """
        center = self.location + self.velocity.normalize() * WANDER_CIRCLE_DISTANCE
        
        pygame.draw.line(screen, RED,(self.location), 
                         (self.location + self.displacement), 2)
        

class Wall():
    def __init__(self, location_x, location_y, SIZE_W, SIZE_H):
        self.x = location_x
        self.y = location_y
        self.location = Vec2(self.x, self.y)
        self.size_w = SIZE_W
        self.size_h = SIZE_H
        
    def display(self):
        """ Hindernis werden  als
            Kreis dargestellt  """
        pygame.draw.circle(screen, CYAN, (int(self.x),int(self.y)), int(self.size_w), 2)

############### Funktionen ###############

def calculate_non_overlapping_circles():
    protection = 0
    while len(walls) < 500:
        # zufällige Positonen generieren
        location_x = randint(SIZE_W, WIDTH - SIZE_W)
        location_y = randint(SIZE_W, HEIGHT - SIZE_W)
        wall = Wall(location_x, location_y, SIZE_W, SIZE_H)
    
        overlapping = False
        # teste alle positionen
        for i in range(len(walls)):
            wall = Wall(location_x, location_y, SIZE_W, SIZE_H)
            other = Wall(location_x, location_y, SIZE_W, SIZE_H)
            other = walls[i]
            
            # überschneiden sich die Kreise
            distance = ball_intersect_ball(wall.x, wall.y, other.x, other.y)
            if (distance < wall.size_w*2 + other.size_w*2):
                overlapping = True
        # falls sich Kreise nicht überschneiden
        # füge diese Position zur Liste hinzu
        if not overlapping:
            walls.append(wall)       
            return walls
            
        protection += 1
        if protection > 1000:
            break
    
    
def ball_intersect_ball(x1, y1, x2, y2):
    """ x1, y1 : Position eines Kreises
        x2, y2 : Position eines zweiten Kreises
         """
    # find distance between the two objects
    x_dist = x1-x2  # distance horiz
    y_dist = y1-y2  # distance vert
    distance = math.sqrt((x_dist*x_dist) + (y_dist*y_dist))  # diagonal distance
    return distance

################################# main ################################
def main() :
    pygame.init()
    clock = pygame.time.Clock()
    
    # erzeuge Hindernisse
    location_x =WIDTH/2
    location_y = HEIGHT/2
    for i in range(NUM_WALLS):
        # Aufruf der Funktion
        calculate_non_overlapping_circles()
    
    # Vehikel hinzufügen
    hunter = pygame.image.load(AGENT_FILENAME2).convert_alpha() 
    hunter = Vehicle(screen, AGENT_FILENAME2,
                  (randint(0, WIDTH),randint(0, HEIGHT)), 1.5, 0.5)
    agent = pygame.image.load(AGENT_FILENAME3).convert_alpha() 
    agent = Vehicle(screen, AGENT_FILENAME3,
                  (randint(0, WIDTH),randint(0, HEIGHT)), 1.5, 0.5)
                 
    #agent_b = pygame.image.load(AGENT_FILENAME1).convert_alpha()
    #agent_b = Vehicle(screen, AGENT_FILENAME1,
    #             (randint(0, WIDTH),randint(0, HEIGHT)),1, 0.5)
                        
    
    running = True
    while running:
        clock.tick(FPS)
        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
        
        screen.fill(BG_COLOR)
        for wall in walls:
            wall.display()
            agent.collision_avoidance(wall)
            hunter.collision_avoidance(wall)
        
        # Update and redraw all agents
        agent.update()
        agent.hide(hunter, walls)
        agent.blitme()
        
        
        hunter.update()
         
        hunter.avoid_walls()
        #hunter.wander()
        hunter.blitme()
        
        pygame.display.set_caption('Autonome Agenten: Hide')
        pygame.display.flip()

if __name__ == "__main__":
main()
Benutzeravatar
ThomasL
User
Beiträge: 1366
Registriert: Montag 14. Mai 2018, 14:44
Wohnort: Kreis Unna NRW

Die Thematik interessiert mich, kann ich mir aber erst heute Abend anschauen.
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
Benutzeravatar
__blackjack__
User
Beiträge: 13101
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@hell: Ein paar Anmerkungen zum Quelltext:

Die Importe könnte man mal aufräumen. Zwei werden nicht verwendet (`os`, `random.choice`) und im Grunde `sys` auch nicht wenn man das übergeordnete Verzeichnis nicht mehr `sys.path` hinzufügt. Was wohl nicht mehr nötig ist wenn Du `pygame.math.Vector2` verwendest. Statt auszukommentieren hättest Du den alten Import auch gleich entfernen können.

``Vec2 = pygame.math.Vector2`` würde man auch idiomatischer als Import schreiben ``from pygame.math import Vector2 as Vec2``.

An zwei Stellen ist nicht 4 Leerzeichen pro Ebene eingerückt.

`SIZE_W` und `SIZE_H` ist unschön benannt. Das `W` und das `H` steht ja wohl für `WIDTH` und `HEIGHT`, warum wird der wichtige Teil durch einen Buchstaben abgekürzt? Und dann wäre es sinnvoller in den Namen zu packen von *was* das die Grösse ist, statt einfach nur `SIZE` – was im Grunde komplett weg kann. Also `WALL_WIDTH` und `WALL_HEIGHT`. Wenn man sich dann aber anschaut das beide Werte gleich sind und wie die im Code verwendet werden, ist das gar nicht die Breite/Höhe sondern ein Radius. *Da* soll mal einer drauf kommen, das `SIZE_W` eigentlich ein Radius ist!

`AGENT_FILENAME1` bis `AGENT_FILENAME3` sollten besser benannt werden, oder keine Einzelnamen sein, sondern beispielsweise eine Liste. Nummerierte Namen sind ein „code smell“

`walls` und `screen` haben auf Modulebene nichts zu suchen. Das erzeugen von `screen` vor dem Aufruf von `pygame.init()` dürfte auch gar nicht möglich sein. Jedenfalls nicht plattformübergreifend.

Wenn man `walls` und `screen` nicht mehr global hat, dann funktionieren `Vehicle.collision_avoidance()`, `Vehicle.draw_vectors()`, `Wall.display()`, und `calculate_non_overlapping_circles()` nicht mehr, weil die jeweils auf eine der beiden zugreifen. Funktionen und Methoden sollten alles was sie ausser Konstanten benötigen als Argument(e) übergeben bekommen.

`Vehicle.collision_avoidance()` ist komisch. Das bekommt ein Hindernis als Argument übergeben (`wall`) – benutzt das dann aber überhaupt gar nicht, sondern geht alle globalen Wände (`walls`) durch. Wird aber in der Hauptfunktion für jede Wand aufgerufen. Das heisst je mehr Wände desto öfter wird diese Methode aufgerufen und desto höher ist die Beschleunigung pro Frame/Zeiteinheit. Soll dass so? Und falls ja, kann man das doch auch einfach durch multiplikation von `steer` mit der Länge von `walls` erreichen statt da einmal pro `walls`-Element den immer gleichen Wert zu berechnen und zu addieren‽

`Vehicle.draw_vectors()` kann einfach auf `self.screen` zurückgreifen wenn das globale `screen` weg ist.

`Wall.display()` kann man `screen` als Argument übergeben.

`calculate_non_overlapping_circles()` ist ähnlich schräg wie `Vehicle.collision_avoidance()`. In der Funktion gibt es eine Schleife die so aussieht als wenn mit ihr 500 Wände erzeugt werden sollten. Die Schleife wird aber nach dem erstellen *einer* Wand *abgebrochen* und die Funktion wird dann von aussen `NUM_WALL`\s mal aufgerufen. Sollte `NUM_WALL`\s grösser als 500 sein, würde das durch die Schleifenbedingung in der Funktion auch einfach begrenzt. Man sollte sich da für eine Schleife zur Steuerung wie viele Wände denn nun erzeugt werden entscheiden. Vorzugsweise die *in* der Funktion, denn dann kann die Funktion tatsächlich die Liste mit Wänden erstellen und zurückgeben, statt eine globale oder übergebene Liste zu verändern.

`location_x` und `location_y` in `main()` werden definiert aber nicht verwendet.

Beim erstellen der `Vehicle`-Objekte wird der Name für das Objekt in der Zeile davor immer an an Bild-Objekt gebunden das nirgends verwendet wird‽

In der Hauptschleife den Fenstertitel immer wieder auf den selben Wert zu setzen, macht keinen Sinn.

In `calculate_non_overlapping_circles()` ist ein „anti pattern“ zu sehen: ``for i in range(len(sequence)):`` nur um `i` dann als Index in `sequence` zu verwenden. In Python kann man direkt über die Elemente von Sequenztypen wie Listen iterieren, ohne den Umweg über einen Laufindex. Die ersten drei Zeilen in der Schleife sind unsinnig. `wall` wurde genau *so* vor der Schleife bereits erzeugt, das braucht man in der Schleife nicht in jedem Durchlauf noch einmal machen. `other` wird dann auch noch mal so definiert, aber gar nicht verwendet, weil der Name gleich in der nächsten Schleife an ein ganz anderes Objekt gebunden wird. Und zwar an das an Index `i` was wie gesagt schon ohne Index im Schleifenkopf passieren sollte.

Der Name `ball_intersect_ball()` ist falsch. Die Funktion berechnet einfach nur den Abstand zwischen zwei Punkten. Was man mit `Vector2` auch etwas kürzer erledigen könnte. Und das wäre auch ein Kandidat für eine Methode von `Wall`. Und darauf aufbauend könnte man dort auch gleiche eine `overlaps()`-Testmethode definieren.

Man sollte redundante Attribute vermeiden weil dann die Gefahr besteht das die Werte auseinanderdriften. `Wall` sollte beispielsweise nicht `location` und dann noch mal `x` und `y` als Attribute haben. Wenn man beides haben möchte/braucht, hat Python `property()`, also ”berechnete Attribute”.

Die Benennung zum Zeichnen von Objekten ist uneinheitlich – mal `blitme()`, mal `display()`, und das `sprites`-Modul verwendet `draw()`.

`Vehicle` erbt von `pygame.sprites.Sprite` – dieser Umstand wird dann aber nirgends verwendet. Dann kann man das auch sein lassen.

Die Objekte haben zu viele, teils auch wieder redundate Attribute. `screen` kann man einfach loswerden wenn man das bei den beiden Methoden die Zeichnen als Argument übergibt, wie auch schon bei `Wall` um das globale `screen` los zu werden.

Die Informationen von `self.image_w` und `self.image_h` stecken auch schon in `self.rect` drin.

`self.location` wird zweimal mit dem gleichen Wert initialisiert.

`self.rect` sollte das `Rect` enthalten an dem das Bild geblittet wird, nicht eines das zum blitten erst noch verschoben wird. Sonst kann man das so mit dem `pygame.sprite`-Modul nicht verwenden.

Du verwendest die Eigenschaften rund um `Rect`\s nicht ausgiebig genug. Einem `Surface.get_rect()` kann man beispielsweise Bezugskoordinaten mitgeben, beispielsweise ``self.image.get_rect(center=self.location)`` ergibt ein `Rect` in der Grösse des Bildes aber mit Koordinaten die `self.location` als Mittelpunkt haben. Zum Testen von Grenzen kann man nicht nur `x` und `y` verwenden, sondern auch `top`, `bottom`, `left`, und `right`. Und wenn man das benutzt um mit vier Tests dafür zu sorgen, dass das in einem bestimmten Bereich bleibt, hat man die `Rect.clamp()`-Methode unnötigerweise selbst noch mal nachprogrammiert.

`self.direction` ist redundant, weil das aus `self.velocity` berechnet werden kann. Es wird auch nur in der `update()`-Methode verwendet, also muss das nicht einmal ein Property sein, sondern dort einfach nur eine lokale Variable.

`max_force`, `acceleration`, und `wanderangle` gehören nicht wirklich als Attribute auf `Vehicle`. Das sind Zustände die zu der/den Bewegungsstrategien gehören, die nicht alle in die gleiche Klasse gehören. Die Grundlagen von einem Fahrzeug, also dessen Zustand, sollte man von den verschiedenen Algorithmen sauber trennen. Dann hat man da auch nicht immer mehr und mehr Methoden von denen jeder Agent nur seine Teilmenge benötigt. Das geht schon so ein bisschen in Richtung Gottklasse.

``self.apply_force(Vector2(0, 0))`` macht eher keinen Sinn.

In `evade()` stehen kommentare die so nicht stimmen und auch der Name `distance` ist falsch, denn das ist gar nicht die Entfernung zwischen den beiden beteiligten Fahrzeugen. Und nach einem Kommentar ``# Aufruf der Methode seek_with_approach`` sollte auch tatsächlich der Aufruf der Methode `seek_with_approach()` folgen und nicht `flee()`. Wobei so ein Kommentar auch total überflüssig ist, denn man sieht ja welche Methode aufgerufen wird auch ohne einen Kommentar.

`Vehicle` könnte ein Property `speed` vertragen.

Die Begrenzung auf `max_force` findet grundsätzlich auf Werten statt mit denen danach `apply_force()` aufgerufen wird – es macht also Sinn diese Begrenzung nicht immer wieder in den Code zu schreiben, sondern in `apply_force()` die Begrenzung anzuwenden.

`get_hiding_position()` ist eine Funktion und keine Methode. Also entweder aus der Klasse als Funktion heraus ziehen oder explizit als `staticmethod()` dekorieren.

Die `obst`-Namen sind lustig – ich bekomme glatt Hunger auf einen Apfel. Ich rate einfach mal das hier `obstacle` gemeint ist. Also auch `obstacle` geschrieben werden sollte. Wobei man hier auch statt zwei Argumenten für das Hinderniss auch überlegen könnte ein `Wall`-Objekt zu übergeben.

Bei `hide()` weist Du nicht vorhandene Rückgabewerte von Aufrufen an Namen zu und hast dort auch ``return``\s die beim Aufrufer überhaupt nicht benutzt werden. Die Methode lässt sich auch deutlich kompakter ausdrücken.

In `avoid_walls()` ist `near_wall` redundant. Das ist an der entscheidenden Stelle immer genau dann wahr, wenn `desired` nicht der Nullvektor ist. Also kann man auch darauf testen.

In `wander` wird plötzlich noch das Attribut `displacement` eingeführt. So etwas sollte ausserhalb der `__init__()` nicht passieren. Und auch dieses Attribut gehört eigentlich nicht auf `Vehicle`.

Bogenmass in Grad umrechnen kann man mit `math.degrees()`. Man kann mit `Vector2` aber auch mit `angle_to()` arbeiten.

Einmal völlig ungetestet überarbeitet:

Code: Alles auswählen

#!/usr/bin/env python3
import math
from random import randint, uniform

import pygame
from pygame.math import Vector2

WIDTH = 840
HEIGHT = 680
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)
CYAN = (0, 255, 255)
DARKGRAY = (40, 40, 40)
BG_COLOR = (150, 150, 80)

FLEE_DISTANCE = 200

LOOK_AHEAD = 80
WANDER_CIRCLE_DISTANCE = 160
WANDER_CIRCLE_RADIUS = 20
CHANGE = 0.2

MAX_AVOID_FORCE = 0.3
APPROACH_RADIUS = 50
NUM_WALLS = 4
WALL_LIMIT = 40
WALL_RADIUS = 80
# 
# TODO Bessere einzelnamen oder in eine Liste stecken.
# 
AGENT_FILENAME1 = '../images/bluecreep_0.png'
AGENT_FILENAME2 = '../images/old_pinkcreep_0.png'
AGENT_FILENAME3 = '../images/yellowcreep_0.png'


def get_hiding_position(obstacle, agent):
    # 
    # Calculate the heading toward the obstacle from the agent.
    # 
    agent_to_obstacle = (obstacle.location - agent.location).normalize()
    # 
    # Scale it to size and add to the obstacle's position to get
    # the hiding spot.
    # 
    agent_to_obstacle.scale_to_length(obstacle.radius + 20)
    return agent_to_obstacle + obstacle.location
    

def line_intersects_obstacle(obstacle, start_point, end_point):
    return (
        obstacle.location.distance_to(start_point) <= obstacle.radius
        or obstacle.location.distance_to(end_point) <= obstacle.radius
    )


class Wall:
    
    def __init__(self, x, y, radius):
        self.location = Vector2(x, y)
        self.radius = radius
    
    def distance_to(self, other):
        return self.location.distance_to(other.location)
    
    def overlaps(self, other):
        return self.distance_to(other) < 2 * (self.radius + other.radius)
    
    def draw(self, screen):
        """Hindernisse werden als Kreis dargestellt."""
        pygame.draw.circle(
            screen,
            CYAN,
            (int(self.location.x), int(self.location.y)),
            self.radius,
            2,
        )


class Vehicle:
    
    def __init__(self, image_filename, init_position, max_speed, max_force):
        self.base_image = pygame.image.load(image_filename).convert_alpha()
        self.image = self.base_image
        self.location = Vector2(init_position)
        self.rect = self.image.get_rect(center=self.location)
        self.max_speed = max_speed
        self.velocity = Vector2(self.max_speed, self.max_speed)
        #
        # TODO Attribute ab hier gehören eigentlich nicht mehr allgemein zu
        #   einem Fahrzeug, sondern zu den Strategien/Algorithmen um ein
        #   Fahrzeug zu steuern, und das sollte sauber ausgelagert werden.
        #   
        #   Durch Vererbung oder besser Komposition, zum Beispiel mit dem
        #   „Strategie“-Entwurfsmuster.
        # 
        self.max_force = max_force
        self.acceleration = Vector2()
        self.wander_angle = 0
        self.displacement = Vector2()
    
    @property
    def speed(self):
        return self.velocity.length()
    
    @property
    def direction(self):
        return self.velocity.normalize()
    
    def apply_force(self, force):
        if force.length() > self.max_force:
            force.scale_to_length(self.max_force)
        self.acceleration += force
    
    def seek(self, target):
        desired = (target - self.location).normalize() * self.max_speed
        self.apply_force(desired - self.velocity)
        
    def seek_with_approach_simple(self, target):
        """Wenn sich Vehikel innerhalb der Entfernung APPROACH_RADIUS
        befindet bremse ab.
        """
        desired = target - self.location
        distance = desired.length()
        desired.normalize_ip()
        if distance < APPROACH_RADIUS:
            desired *= distance / APPROACH_RADIUS * self.max_speed
        else:
            desired *= self.max_speed
        self.apply_force(desired - self.velocity)
    
    def seek_with_approach(self, target):
        """Nach Bucklands arrive-Algorithmus."""
        deceleration = 5
        to_target = target - self.location
        distance = to_target.length() 
        if distance > 0:
            deceleration_tweaker = 3
            #
            # Calculate the speed required to reach the target given the desired
            # deceleration
            speed = distance / (deceleration * deceleration_tweaker)
            # 
            # Make sure the velocity does not exceed the max.
            # 
            speed = min(speed, self.max_speed)
            # 
            # From here proceed just like Seek except we don't need to normalize
            # the `to_target` vector because we have already gone to the trouble
            # of calculating its length: distance.
            # 
            desired = to_target * speed / distance
            self.apply_force(desired - self.velocity)
        
    def flee(self, target):
        """Reynolds steering Algorithmus: flee 
        wenn sich Mover innerhalb der FLEE_DISTANCE befindet
        """
        if self.location.distance_to(target) < FLEE_DISTANCE:
            desired = self.location - target
        else:
            desired = self.velocity
        self.apply_force(desired.normalize() * self.max_speed - self.velocity)

    def evade(self, hunter):
        # berechne Entfernung zwischen pursuer und evader
        distance = hunter.location.distance_to(self.location)
        # -> nach: SteeringBehavior.java (Buckland)
        look_ahead = distance / (self.max_speed + hunter.speed)
        future_position = hunter.location + hunter.velocity * look_ahead
        self.flee(future_position)
        
    def hide(self, hunter, walls):
        candidates = (get_hiding_position(wall, hunter) for wall in walls)
        hiding_spot = min(
            candidates, default=None, key=hunter.location.distance_to
        )
        if hiding_spot is None:
            self.evade(hunter)
        else:
            self.seek_with_approach(hiding_spot)
            
    def avoid_walls(self):
        if self.location.x < WALL_LIMIT:
            desired = Vector2(self.max_speed, self.velocity.y)
        elif self.location.x > WIDTH - WALL_LIMIT:
            desired = Vector2(-self.max_speed, self.velocity.y)
        elif self.location.y < WALL_LIMIT:
            desired = Vector2(self.velocity.x, self.max_speed)
        elif self.location.y > HEIGHT - WALL_LIMIT:
            desired = Vector2(self.velocity.x, -self.max_speed)
        else:
            desired = Vector2()
        
        if desired:
            self.apply_force(desired - self.velocity)

    def collision_avoidance(self, walls):
        start_point = self.location + self.direction * LOOK_AHEAD
        end_point = self.location + self.direction * LOOK_AHEAD * 0.5
        
        threatening_walls = filter(
            lambda wall: line_intersects_obstacle(wall, start_point, end_point),
            walls
        )
        most_threatening_wall = min(
            threatening_walls, default=None, ke=self.location.distance_to
        )
        if most_threatening_wall is not None:
            steer = start_point - most_threatening_wall.location
            steer.normalize_ip() 
            steer.scale_to_length(MAX_AVOID_FORCE)
            # 
            # TODO Ist die Multiplikation hier wirklich so gewollt?
            # 
            self.apply_force(steer * len(walls))
       
    def wander(self):
        """Reynolds steering Algorithmus: wander"""
        self.wander_angle += uniform(-CHANGE, CHANGE)
        self.displacement = WANDER_CIRCLE_RADIUS * Vector2(
            math.cos(self.wander_angle), math.sin(self.wander_angle)
        )
        self.seek(
            self.direction * WANDER_CIRCLE_DISTANCE
            + self.location
            + self.displacement
        )
        
    def update(self):
        if self.speed > 0.00001:
            self.image = pygame.transform.rotate(
                self.base_image, self.direction.angle_to(Vector2(1, 0))
            )
        
        self.velocity.scale_to_length(self.max_speed)
        self.velocity += self.acceleration
        # begrenze velocity auf MAX_SPEED
        if self.acceleration.length() > 0.000001:
            self.velocity.scale_to_length(self.max_speed)
        if self.speed > self.max_speed:
            self.velocity.scale_to_length(self.max_speed)     
        self.location += self.velocity
        self.acceleration *= 0  # Resette nach jedem update.
        
        self.rect = (
            self.image.get_rect(center=self.location).clamp(0, 0, WIDTH, HEIGHT)
        )
    
    def draw_vectors(self, screen):
        """Zeichne Pfeilrepräsentanten der Vektoren velocity und desired."""
        pygame.draw.line(
            screen, RED, self.location, (self.location + self.displacement), 2
        )
       
    def draw(self, screen):
        screen.blit(self.image, self.rect)   
        

def calculate_non_overlapping_circles(num_walls):
    walls = list()
    for _ in range(num_walls):
        # 
        # Maximale Versuche pro Hinderniss begrenzen.
        # 
        for _ in range(1000):
            wall = Wall(
                randint(WALL_RADIUS, WIDTH - WALL_RADIUS),
                randint(WALL_RADIUS, HEIGHT - WALL_RADIUS),
                WALL_RADIUS,
            )
            if not any(map(wall.overlaps, walls)):
                walls.append(wall)
                break
    
    return walls


def main():
    pygame.init()
    screen = pygame.display.set_mode((WIDTH, HEIGHT))
    pygame.display.set_caption('Autonome Agenten: Hide')
    clock = pygame.time.Clock()
    
    walls = calculate_non_overlapping_circles(NUM_WALLS)
    
    hunter = Vehicle(
        AGENT_FILENAME2,
        (randint(0, WIDTH), randint(0, HEIGHT)),
        1.5,
        0.5,
    )
    agent = Vehicle(
        AGENT_FILENAME3,
        (randint(0, WIDTH), randint(0, HEIGHT)),
        1.5,
        0.5,
    )
    # agent_b = Vehicle(
    #     AGENT_FILENAME1,
    #     (randint(0, WIDTH), randint(0, HEIGHT)),
    #     1,
    #     0.5,
    # )
    
    running = True
    while running:
        clock.tick(FPS)
        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
        
        screen.fill(BG_COLOR)
        for wall in walls:
            wall.draw(screen)
        
        agent.collision_avoidance(walls)
        hunter.collision_avoidance(walls)
        
        agent.update()
        agent.hide(hunter, walls)
        agent.draw(screen)

        hunter.update()
        hunter.avoid_walls()
        # hunter.wander()
        hunter.draw(screen)
        
        pygame.display.flip()


if __name__ == '__main__':
    main()
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
hell
User
Beiträge: 40
Registriert: Montag 1. Oktober 2018, 18:01

danke _blackjack_ für deine ausführlichen Anmerkungen zu Code:
ich werde mir das erst einmal ansehen müssen und alles, was du geschrieben hast zu verstehen versuchen. Gut ,dass du mit allem redundanten Code aufgeräumt hast.
Und nun habe ich Fragen über Fragen.
Ich finde es gut, dass du mir einen eleganten Quellcode präsentierst bloss hast du jetzt Sprachelemente eingeführt, die ich (noch ) nicht verstehe:
- lamda-Funktion
-statische Funktion
- ich verstehe, dass die betreffende Methode eigentlich bloss eine Hilfsfunktion ist, ( wo gehört die dann hin und wie wird sie aufgerufen ?)
- property
- filter
- Vererbung ( ein wenig)
- Komposition

Soviel erstmal nach dem ersten überfliegen.
Zur Vehicle Klasse: was meinst du mit "Strategie-Entwurfsmuster" ? Eine Klasse Steering_Strategies ?
Im Original ist das ähnlich getrennt angelegt wie du es forderst (bloss dass ich die Komplexität des C++-Programmes garnicht verstehe):
Vehicle-Klasse / Steering-Klasse /BaseGameEntity-Klasse

Beim Ausführen des Codes:
dein überarbeiteter Code beendet sich mit Fehlermeldung in Methode Vehicle.collision_avoidance( ) in der Zeile mit lamda wall ...
type error: sequenz is expected
Was die Fehlermeldung betrifft, so verstehe ich nur "Bahnhof"; es ist halt so, du spielst in einer der Bundesligaklassen und ich in der Kreisklasse ( wenn überhaupt )
grüsse hell
hell
User
Beiträge: 40
Registriert: Montag 1. Oktober 2018, 18:01

Hallo _blackjack_,
ich habe das Gefühl, mich etwas missverständlich ausgedrückt zu haben und ich will noch einmal formulieren, worum es mir geht.
Bei der Durchsicht meines Quellcodes wurde dir schnell klar, dass ich nur ein Minimum an Python/ Pygame Sprachkonstrukten beherrsche.
Ich versuche verschiedene Quellen zu autonomen Agenten, Shiffman Nature of Code,
Understanding Steering Behavior von F. Bevilacqua, Matt Buckland Game AI, um die wesentlichsten zu nennen, auf python/pygame zu übertragen. Ich bin mir sehr wohl bewusst, dass das nicht eins zu eins geht, da es sich um verschiedene Sprachen handelt.
Daher bin ich sehr froh, dass es Enthusiasten wie euch hier im Forum gibt, die einem Tips und Anregungen geben und ihre Freizeit dafür hergeben. Andererseits habe ich schon im vorausgegangen thread formuliert, dass ich einige Sprachkonstrukte schlicht und einfach nicht beherrsche und wahrscheinlich auch in nächster Zukunft nicht beherrschen werde. Es muss doch möglich sein, auf fortgeschrittenen Konzepte 'didaktisch' zu verzichten und stattdessen sich auf - für mich einsichtigere - Probleme des Codes zu konzentrieren. Z.B ist mir sofort klar, dass einige Attribute der Vehicle-Klasse zum Steeringverhalten gehören und nicht Eigenschaften des Vehikels sind.
Oder es wird sofort klar, nachdem du die Funktion get_hiding_position() u.a. vor die Klasse Vehicle gestellt hast, sie für diese sichtbar sind (stimmt das?), zumindest sind sie keine Methoden der Klasse. Ich hoffe, du verstehst, was ich sagen will. Einige deiner Veränderungen kann ich gut verstehen andere andere sind für dich einfach und elegantes Python, für mich vielleicht eine ( unnötige ?? ) Verkomplizierung. Glaube nicht, dass ich es einfach haben will und nichts lernen will. Verstehen möchte ich es aber schon.
grüsse hell
__deets__
User
Beiträge: 14533
Registriert: Mittwoch 14. Oktober 2015, 14:29

Das sind keine fortgeschrittenen Konzepte. Klar kann man auf ein property verzichten, aber wenn dein Verständnis daran scheitert, ob irgendwo ein paar Klammern stehen oder nicht (das ist zumindest zum lesen wie hier der Unterschied), dann sehe ich da schwarz für das Verständnis von den Dingen, um die es dir eigentlich geht.

Und keiner hier weiß, wo genau DEINE Grenze ist. Zu verlangen Code geliefert zu bekommen der dich präzise abholt, ist etwas viel des Guten. Wieviele iterationen sollen da deiner Meinung nach ins Land gehen, bis das passt?

Es gibt keinen Grund, warum du dir die verwanden Funktionen und Idiome nicht erarbeiten kannst.

Es ist toll, das du ein Thema hast, das dich antreibt. Fehlt vielen. Aber dann nutz den Antrieb & lern, was zu lernen ist. Inklusive fragen, was die Dinge tun. Statt zu lamentieren.
Benutzeravatar
__blackjack__
User
Beiträge: 13101
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@hell: Ein ``lambda``-Ausdruck ist einfach eine Funktion ohne Namen. Man hätte da auch einfach vorher eine lokale Funktion definieren können, und die dann übergeben.

Mit statische Funktion meinst Du wohl statische Methode. Das ist eine Methode die kein `self` bekommt, also eigentlich eine Funktion, und damit das mit dem fehlenden `self` funktioniert, muss man so eine Methode mit `staticmethod()` dekorieren. Manchmal hat man Funktionen die wirklich nur im Zusammenhang mit einer bestimmten Klasse Sinn machen, oder Methoden die tatsächlich Methoden sein müssen, zum Beispiel weil sie Teil einer API sind, die aber das Objekt gar nicht brauchen. Dafür ist `staticmethod()`. Ansonsten sollte man Funktionen ausserhalb von Klassen definieren.

Die beiden Funktionen die ich da heraus gezogen habe, könnte man, wenn man sie unbedingt als Methoden haben möchte, eventuell auf `Wall` definieren. Oder in einer gemeinsamen Basisklasse von der `Vehicle` und `Wall` ableiten, wenn man sicherstellt, dass `Vehicle` auch einen `radius` hat. Beispielsweise als Property das den Radius aus dem `rect`-Attribut ableitet. Dann könnte man auch Versteckpositionen hinter `Vehicle`-Objekten berechnen. Zum Beispiel wenn man dafür sorgen möchte, dass ein Fahrzeug gegenüber einem Verfolger immer hinter einem Schutzfahrzeug Deckung sucht.

Properties sind berechnete Attribue, also etwas was man wie ein Attribut abfragen und setzen kann, was aber hinter den Kulissen Code ausführt der das Ergebnis liefert, oder etwas mit dem zugewiesenen Wert macht. `Vehicle` hat ja in meiner Überarbeitung beispielsweise `speed` und `direction` als Attribute, deren Werte aus dem Geschwindigkeitsvector berechnet wird. Man könnte zum Beispiel mit ``print(hunter.speed)`` in der Hauptschleife immer die aktuelle Geschwindigkeit ausgeben. So etwas wie ``hunter.speed = 42`` geht nicht, weil das Property nur den ”getter”-Anteil implementiert. Wenn man das auch haben möchte, könnte man folgende Methode ergänzen nach der vorhandenen `speed()`-Methode:

Code: Alles auswählen

    @speed.setter
    def speed(self, value):
        self.velocity = self.direction * value
Benutzt hast Du so etwas schon bei der `Rect`-Klasse. Die speichert den Punkt für die obere, linke Ecke und Breite und höhe, hat aber auch Attribute für alle anderen Eckpunkte, Seiten, und die Mitte, die man sowohl abfragen, als auch setzen kann:

Code: Alles auswählen

In [113]: r = pygame.Rect((0, 0), (10, 10))

In [114]: r
Out[114]: <rect(0, 0, 10, 10)>

In [115]: r.center
Out[115]: (5, 5)

In [116]: r.center = (20, 20)

In [117]: r
Out[117]: <rect(15, 15, 10, 10)>
`filter()` ist eine Funktion die eine Testfunktion und ein iterierbares Objekt nimmt, und ein iterierbares Objekt liefert das nur die Elemente durch lässt, die den Test bestehen. Ein Beispiel, mit ``lambda``-Ausdruck, das alle geraden Zahlen aus einem `range()`-Objekt liefert:

Code: Alles auswählen

In [118]: list(filter(lambda x: x % 2 == 0, range(30)))
Out[118]: [0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28]
Für Vererbung und Komposition gibt's hoffentlich genug Quellen. Komposition ist meistens flexibler. Für Vererbung würde sich wie gesagt eine Basisklasse für `Wall` und `Vehicle` anbieten, die beispielsweise die `distance_to()`-Methode bereit stellt und das `location`-Attribut initialisiert. Eine leere `draw()`-Methode könnte man da auch anlegen, zu dokumentationszwecken, das diese Objekte diese Methode haben müssen, weil anderer Code erwartet die damit auf ein `Surface` zeichnen zu können. Das ist in Python aber nicht zwingend erforderlich – da reicht es das zu dokumentieren. (Im Grunde muss man nicht einmal das. :-))

„Strategie“-Entwurfsmuster meint, dass man die Strategie wie etwas gelöst wird als Argument übergeben kann. Strategie hat hier übrigens eine Doppelbedeutung. Die Strategien sind hier ja tatsächlich die Strategien der Agenten. Man kann das Entwurfsmuster aber auch in Fällen anwenden wo das in der Problemdomäne nicht unbedingt als Strategie bezeichnet wird.

Damit ist keine `SteeringStrategies`-Klasse gemeint, die all die Steuermethoden enthält, sondern ein Objekt pro Strategie. Wobei Objekt hier auch nicht zwingend Klasse meint – das kann in Python auch einfach nur eine Funktion sein, oder eine Methode. Und diesen Wert übergibt man dann `Vehicle` beim erstellen und `Vehicle` ruft die Funktion, oder eine bestimmte Methode auf dem Objekt, mit sich selbst und anderen nötigen Informationen über die Welt (Hindernisse, andere Agenten, …) auf. Die Funktion oder Methode berechnet dann wie das Fahrzeug bewegt wird und kümmert sich auch um Zustand den die Strategie selbst über Aufrufe hinweg benötigt.

Für eine Klasse würde sprechen, dass man da neben der `update()`-Methode ebenfalls eine `draw()`-Methode definieren kann, die dann beispielsweise Daten wie Abstände, Richtungen, usw. einzeichnen kann. Eine `Flee`-Strategie könnte beispielsweise den Radius um das Fahrzeug einzeichnen innerhalb dessen die Flucht ausgelöst wird.

Man kann dann auch Strategien schreiben, die ihrerseits wieder Strategien als Argumente bekommen. Die `Hide`-Strategie verwendet ja beispielsweise je nach Verfügbarkeit eines Verstecks eine von zwei anderen Strategien.

Sinn würde auch eine Strategie machen die einfach verschiedene Strategien der Reihe nach anwendet, denn von den Aussenwänden weg bleiben und Kollisionen mit Hindernissen vermeiden sind Strategien die Du da anscheinend immer im Mix haben möchtest.

Interessant wäre vielleicht auch eine Strategie die zwischen zwei anderen Strategien wählt, je nach dem ob sich der andere Agent auf einen zu oder weg bewegt.

Die `key`-Funktion geht so an der Stelle nicht wo Du die Ausnahme bekommst. Ich bin ziemlich sicher das ich auch noch andere Stellen kaputt gemacht habe. Ich habe das wegen der Bilder nicht getestet. Am einfachsten ist es dort statt `self.location.distance_to` dem `Vehicle` eine passende `distance_to()`-Methode zu spendieren und die als Schlüssel zu übergeben. Dann hätten `Wall` und `Vehicle` ein `location`-Attribut und eine `distance_to()`-Methode und man könnte das wie weiter oben schon geschrieben in eine gemeinsame Basisklasse heraus ziehen.
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
hell
User
Beiträge: 40
Registriert: Montag 1. Oktober 2018, 18:01

Hallo _deets_
ich finde, du machst es dir ein wenig einfach mit deiner Antwort. Jedenfalls hat der ursprüngliche Code - trotz der Mängel und Redundanzen, schlechter Code jedenfalls - funktioniert, auch ohne Lamdafunktion, Properties, elegantem Pythoncode etc. Ich bin mir sicher, dass, wenn du so einen Code siehst , sehr genau einschätzen kannst, wo ich stehe, und auch in der Lage bist deine Verbesserungsvorschläge, Tipps dieser Einschätzung nach formulieren kannst. Übrigens verlange ich gar nicht, dass ihr mir hier fertigen Code liefert, aber wenn ihr mir ihn liefert, so möchte ich ihn auch verstehen können. Mehr möchte ich gar nicht. Z.B. die Aussagen von _blackjack_ zur Vehicleklasse leuchten mir sofort ein, sie holen mich genau da ab, wo ich stehe und was ich beherrsche und führen mich weiter zum ursprünglichen Problem hin. Sie sind didaktisch sinnvoll, da sie mich weiter bringen ( übrigens bin ich kein Lehrer) .
grüsse hell
hell
User
Beiträge: 40
Registriert: Montag 1. Oktober 2018, 18:01

hallo _blackjack_
danke für deine ausführliche Antworten.
Zum Thema strategies habe ich im Forum etwas von hyperion gefunden, muss ich verdauen.
Zur Methode collision_avoidance():
ahead und ahead2 sind die Fühler, die definieren, wieweit das Vehikel sieht. Und der Funktion (in meinem Code) line_intersects_circle(xxx) übergebe ich dann die beiden Vektoren. Die Ausdrücke start_point , end_point verwirren mich ein wenig. Wenn ich das recht verstehe, macht die
Funktion line_intersects_obstacle(xxx) das gleiche wie in meinem Code.
Dein Kommentar unter TODO bezieht sich wohl auf den nachfolgenden Code? Der Methode apply_force( xxx) übergibst du steer * len(walls), wie kommst du darauf?
Ich verstehe nicht wirklich, wie das bei dir funktioniert.
gruss hell
Benutzeravatar
__blackjack__
User
Beiträge: 13101
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@hell: Dich verwirren `start_point` und `end_point` bei einer Funktion `line_intersects_circle()`? Mich haben `ahead` und `ahead2` ”verwirrt”. Also nicht wirklich verwirrt, aber ich musste erst drüber nachdenken was das bedeuten mag, und fand `start_point` und `end_point` deutlich verständlicher. Ich meine die Funktion braucht doch eine Linie und einen Kreis. Der Kreis ist das `Wall`-Objekt mit `location` = Mittelpunkt und `radius` = Radius (bei Dir ursprünglich noch `size_w`) des Kreises. Braucht's noch eine Linie – und da ist jetzt nicht klar warum ich die Namen `start_point` und `end_point` gewählt habe? Das bedeuten die Werte doch, oder habe ich das falsch verstanden? Ich dachte das wäre Start- und Endpunkt der Linie in Fahrtrichtung des Fahrzeugs, quasi Richtung und Reichweite eines Abstandssensors, wenn man echte Hardware verwenden würde. Wobei ich die wohl falsch herum benannt habe wenn man vom Fahrzeug aus schaut. Insofern vielleicht doch ein bisschen verwirrend. :-)

Meine `line_intersects_obstacle()` macht das gleiche, ich habe da nur die Argumente für Mittelpunkt und Radius zu einem für ein Hindernis zusammengefasst, weil `Wall`-Objekte ja im Grunde mit `location` und `radius` genau diese beiden Eigenschaften eines Kreises in einem Objekt vereinen. Ich hatte auch kurz überlegt `start_point` und `end_point` zu einem `Line`-Objekt zusammen zu fassen und das Argument dann `line` zu nennen. Also als Code dann:

Code: Alles auswählen

Line = collections.namedtuple('Line', 'start end')

def line_intersects_obstacle(obstacle, line):
    return (
        obstacle.location.distance_to(line.start) <= obstacle.radius
        or obstacle.location.distance_to(line.end) <= obstacle.radius
    )
Wobei streng genommen Name der Funktion und Inhalt nicht ganz zusammenpassen, denn eine Linie kann einen Kreis auch dann schneiden, wenn beide Endpunkte ausserhalb des Kreises liegen. Im schlimmsten Fall weil die Linie länger als der Durchmesser des Kreises ist und das Fahrzeug in dem Fall Hindernisse, die wirklich direkt in Fahrtrichtung vor dem Fahrzeug liegen, komplett übersehen kann.

Kommentare die nicht hinter Code in der gleichen Zeile stehen, beziehen sich in Python immer auf den darauf folgenden Code.

Auf ``steer * len(walls)`` komme ich weil Du das effektiv auch machst im Originalcode. Schauen wir uns das Original und den Code der das aufruft mal an:

Code: Alles auswählen

    def collision_avoidance(self,wall):
        steer = Vec2(0, 0)
        ahead = self.location + self.velocity.normalize() * LOOK_AHEAD
        ahead2 = self.location + self.velocity.normalize() * LOOK_AHEAD * 0.5
        
        most_threatening = self.find_most_threatening(walls, ahead, ahead2)
        if most_threatening:
           steer = ahead - most_threatening.location
           steer.normalize_ip() 
           #steer *=  MAX_AVOID_FORCE
           steer.scale_to_length(MAX_AVOID_FORCE)
        else:
           steer = Vec2(0, 0)
        
        self.apply_force(steer)

# ...

def main():
    # ...
    
        for wall in walls:
            wall.display()
            agent.collision_avoidance(wall)
            hunter.collision_avoidance(wall)
Die `collision_avoidance()` wird für jeden `wall` einmal aufgerufen und ermittelt bei jedem Aufruf den gleichen `steer`-Wert und wendet den an. Das `wall`-Argument wird gar nicht verwendet. Statt die Methode `n`-mal – mit `n = len(walls)` — aufzurufen und den gleichen `steer`-Wert anzuwenden, kann man die auch *einmal* aufrufen und einmal den n-fachen `steer`-Wert anwenden.

Jetzt könnte man sagen, moment mal, die Methode wird ja immer mit einem anderen `wall`-Argument aufgerufen – nur wird das halt in der Methode überhaupt gar nicht verwendet, hat also gar keinen Einfluss darauf was die Methode macht.

Deinen Code kann man so ändern, und er macht immer noch genau das gleiche:

Code: Alles auswählen

    def collision_avoidance(self):
        steer = Vec2(0, 0)
        ahead = self.location + self.velocity.normalize() * LOOK_AHEAD
        ahead2 = self.location + self.velocity.normalize() * LOOK_AHEAD * 0.5
        
        most_threatening = self.find_most_threatening(walls, ahead, ahead2)
        if most_threatening:
           steer = ahead - most_threatening.location
           steer.normalize_ip() 
           #steer *=  MAX_AVOID_FORCE
           steer.scale_to_length(MAX_AVOID_FORCE)
        else:
           steer = Vec2(0, 0)
        
        self.apply_force(steer * len(walls))

# ...

def main():
    # ...
    
        for wall in walls:
            wall.display()
        
        agent.collision_avoidance()
        hunter.collision_avoidance()
So komme ich auf ``steer * len(walls)``. Und weil mir das sehr komisch vorkommt das die Anzahl der Hindernisse diesen Effekt haben soll, habe ich den TODO-Kommentar da rein geschrieben, damit Du da noch mal drüber nachdenkst ob das wirklich so sein soll, denn effektiv ist das in Deinem Code auch schon so.
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
hell
User
Beiträge: 40
Registriert: Montag 1. Oktober 2018, 18:01

hallo _blackjack_
ahead und ahead2 sind zwei Vektoren ,welche als Sensoren fungieren , im Grunde genommen der velocity-Vektor skaliert mit MAX_SEE_AHEAD,
gleiche Richtung ahead2 halbe Länge.
collision_avoidance benötigt das wall Argument nicht und ebenso gehört der Aufruf nicht in for , da hast du recht.
Warum aber der Code trotzdem noch das gleiche machen soll, wenn er so bereinigt wurde, will nicht in meinen Kopf rein.
gruss hell
Benutzeravatar
__blackjack__
User
Beiträge: 13101
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@hell: Na angenommen es gibt 10 Hindernisse, dann ruft Dein Code `collision_avoidance()` 10 mal auf und bei jedem Aufruf wird der gleiche `steer`-Wert ermittelt und mit `apply_force()` angewendet. Also effektiv ``steer * 10``. Wenn man `collision_avoidance()` nur noch einmal aufruft, dann wird `apply_force()` nur noch einmal aufgerufen mit `steer`. Man muss das also mit 10 multiplizieren wenn man den gleichen Effekt haben will.
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
Benutzeravatar
__blackjack__
User
Beiträge: 13101
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

Und bezüglich `ahead` und `ahead2` – ja genau, und die beschreiben doch die Linie, im Sinne von das sind die Endpunkte der Linie, die im Namen `line_intersect_circle()` vorkommt, den *Du* ja vergeben hast, nicht ich. Oder welche Linie ist damit gemeint‽
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
hell
User
Beiträge: 40
Registriert: Montag 1. Oktober 2018, 18:01

hallo _blackjack_
ich weiss jetzt nicht, ob wir aneinander vorbeireden,
ahead und ahead2 sind Vektoren, keine Linien. Sie zeigen beide in die gleiche Richtung wie der Geschwindigkeitsvektor des Vehikels.
Sie haben fixe Längen. In line_intersect_circle teste ich, ob Vektor ahead oder der halb solange Vektor ahead2 sich mit dem Kreis schneiden.

zu Problem 2: Verstehe ich dich recht, dass , wenn ich alle 10 Hindernisse abfrage, die steering-force-Vektor akkumuliert, wie du sagst.
Wenn ich Shiffman, Nature of Code recht verstanden habe, so wird das Problem damit aus der Welt geschafft, dass mit jedem update der acceleration-Vektor auf 0 gesetzt wird mit self.acceleration *= 0 in update()

hell
Benutzeravatar
__blackjack__
User
Beiträge: 13101
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@hell: `ahead` und `ahead2` sind keine Linien sondern Punkte (Ortsvektoren) aber *beide zusammen* bilden doch die Endpunkte einer Linie in Fahrrichtung vor dem Fahrzeug. Die Methode heisst ja `line_intersect_circle()` und Punkte können sich ja auch gar nicht mit einem Kreis schneiden, sondern nur im Kreis, ausserhalb des Kreises, oder auf dem Kreis liegen. Die Methode testet ja auch nur ob mindestens einer der beiden Punkte im Kreis liegt, also ist der „intersect“-Teil des Funktionsnamens falsch. Oder eben die Implementierung. Die ist jedenfalls problematisch weil sie nicht verhindert das bei ungünstig gewählten Hindernisgrössen und/oder LOOK_AHEAD-Wert es passieren kann, dass das Hindernis genau zwischen den beiden Punkten ist, also voll im Weg des Fahrzeugs, es aber nicht erkannt wird, weil der eine Punkt aus Sicht des Fahrzeugs vor dem Hindernis liegt, und der andere hinter dem Hindernis, und damit keiner *im* Hindernis.

Klar setzt `update()` die Beschleunigung zurück, aber wieso sollte das verhindern, dass sich die Werte mit jedem Aufruf akkumulieren? Zwischen den 10 Aufrufen wird `update()` ja nicht aufgerufen. Würde an der Stelle im Code ja auch gar keinen Sinn machen.
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
hell
User
Beiträge: 40
Registriert: Montag 1. Oktober 2018, 18:01

hi _blackjack_
exakt, und deshalb ist die collision_avoidance auch ziemlich armselig und müsste überarbeitet werden z.B mit einer Boundingbox um die
Vehikel mit einer dynamischen Länge, abhängig von der Geschwindigkeit , wie bei Buckland. Was mir aber sofort neue Probleme einbringt ( Thema lokales Koordinatensystem vs globales Koordinatensystem: ???????). Einfacher wären Fühler zur Seite. Oder die Steuerung des Vehikels zusätzlich mit seitlichen Vektoren, usw.
Was mich immer noch beschäftigt ist das zweite Problem:
-welche Relevanz hat das Problem und wie wirkt es sich auf die Steuerung der Vehikel aus.
-wenn apply_force(steer * len(walls)) läuft, warum nicht stattdessen apply_force(steer / len(walls)) ????
Ehrlich gesagt, ich habe keinen Schimmer.
hell
hell
User
Beiträge: 40
Registriert: Montag 1. Oktober 2018, 18:01

hallo _blackjack_,
vielleicht kannst du so nett sein und noch *einmal* antworten.
Du hast recht, du hast so ziemlich alles wichtige zerschlagen und da ich deinen Code in hide und collision_avoidance
(noch) nicht verstehe, so blieb mir keine andere Wahl diese wieder zu ändern unter Beibehaltung deiner Codestruktur.
1. Funktion get_hiding_position -> Compiler meckert: Attributerror oder so, d.h. er kann z.B.obstacle.location nicht finden
(ist eigentlich auch klar ): also habe ich die Funktion wieder so geändert -> die Argumente nur Platzhalter

Code: Alles auswählen

def get_hiding_position(obstacle, radius, agent):
    # 
    # Calculate the heading toward the obstacle from the agent.
    # 
    agent_to_obstacle = (obstacle - agent).normalize()
    # 
    # Scale it to size and add to the obstacle's position to get
    # the hiding spot.
    # 
    agent_to_obstacle.scale_to_length(radius + 20.0)
    return agent_to_obstacle + agent
und dann in hide die Funktion so aufgerufen:

Code: Alles auswählen

hiding_spot = get_hiding_position(wall.location, 
                                              wall.radius,
                                              hunter.location)
Allerdings funktioniert hiding-Verhalten nicht so richtig. Ich vermute es liegt an ff. Codezeile:

Code: Alles auswählen

dist = hiding_spot.distance_to(hunter.location) 
Hier rufst du, wenn ich das recht sehe , die Methode distance_to der Wallklasse auf. Das ist für mich (und für den Compiler vielleicht auch ) recht verwirrend, da sie den gleichen Namen hat wie die der Vektorklasse Vector2.

2.Diese Methode hat allerdings wiederum Auswirkungen auf die Funktion calculate_non_overlapping_circles. Hier rufst du ganz offensichtlich mit ff Code:

Code: Alles auswählen

if not any(map(wall.overlaps, walls)):
ebenfalls die Wall-Methode distance_to auf. Und ab hier habe ich dann aufgegeben.

3. Die Methode collision_avoidance habe ich ebenfalls wieder rückgängig gemacht, da auch hier der Compiler gemeckert hat und ( wie gesagt, da ich *noch* deinen Code verstehe ) die ursprünglich Form hergestellt. Was dann funktionierte.

Um deine Code zu testen, benötigst du die Bilder auf Eli Benderskys Webseite hier:Bild

Im übrigen möchte ich die noch einmal ganz herzlich danken für deine Geduld mit mir, mit dem bereinigte Code von dir sind so einige unsinnige Dinge aus der Welt.
Eine Frage habe ich noch: ich verwende den Editor geany, hauptsächlich, weil ich die Programm von da heraus problemlos starten kann, allerdings hat geany ziemlich Probleme mit der Einrückung, glaube ich, sie sind nämlich von Datei zu Datei optisch ganz verschieden breit. Ich habe mich schon einmal an atom versucht, jedoch will der die Programme nicht starten und findet auch python 3 nicht.
Was kannst du mir empfehlen?
gruss hell
__deets__
User
Beiträge: 14533
Registriert: Mittwoch 14. Oktober 2015, 14:29

Die get_hiding_position Funktion ist Unfug. Sie tut nicht, was ihr eigener Kommentar beschreibt. Das Folgeverhalten kann dann wohl kaum erfolgreich ausfallen.

Der normalisierte Vektor geht in die falsche Richtung. Er zeigt WEG vom Agenten. Er muss aber ZU dem Agenten zeigen, damit man ihn korrekt skaliert auf die Position des HINDRENISSES addieren kann. Denn man will sich ja in der Nähe der Wand verstecken. Stattdessen addierst du ihn auf die Position des Agenten, der sich damit in der konsequenz ein bisschen auf das Hindernis zu bewegt, aber nicht versteckt.

Dieser Post ist frei von fortgeschrittenen Python-Konzepten & enthält nur grundlegende Vektormathematik.
hell
User
Beiträge: 40
Registriert: Montag 1. Oktober 2018, 18:01

danke _deets_ ,
habe Fehler gefunden. Die Funktion muss so lauten:

Code: Alles auswählen

def get_hiding_position(obstacle, radius, target): 
    # Calculate the heading toward the obstacle from the agent.
    # 
    agent_to_obstacle = (obstacle - target).normalize()
    # 
    # Scale it to size and add to the obstacle's position to get
    # the hiding spot.
    # 
    agent_to_obstacle.scale_to_length(radius + 20.0)
    return agent_to_obstacle + obstacle
wobei target für den hunter steht.
gruss hell
__deets__
User
Beiträge: 14533
Registriert: Mittwoch 14. Oktober 2015, 14:29

Wenn man sich VOR dem target verstecken soll, ja. Dann klingt das sinnvoll.
Antworten