Ich habe vor einiger Zeit jemandem auf Discord mit Godot geholfen, und wollte das Endprodukt einmal mit euch Godot-Begeisterten teilen.
Es geht darum zu berechnen wie ein 2D-Node mit anderen Nodes kollidiert und dann weiterfliegt:
Ich habe 2 Versionen gemacht: Eine bei der die Linie insgesamt immer dieselbe Länge hat und ein bei der die Linie n-mal an anderen Nodes abprallt.
Der SceneTree ist relativ simpel, es ist eine Main Scene mit dem Pool-Node und eine Szene für die Objekte mit denen die Linie kollidieren soll welche dann mehrfach in die Hauptszene instanziiert wurden.
Der einzige Code liegt bei dem Pool-Node.
Gucken wir uns einmal die Variante an, bei denen die Länge der Linie immer gleich bleibt:
- extends Node2D # Erbt von der Klasse Node2D
- export(Vector2) var cast_vec = Vector2(0, -1) # Normalisierter Vektor, in welche Richtung die Linie als nächstes fliegen soll
- export(float) var cast_length = 600.0 # Länge, welche die Linie am Ende haben soll
- onready var Ray_Cast = get_node("RayCast2D") # Referenz zu dem instanziierten RayCast2D-Node
- var draw_lines = [] # Array in welchem wir die berechneten Linien abspeichern werden
- func _ready():
- _generate_path() # Das Node ist im SceneTree ---> Linien zum ersten mal generieren
- func _physics_process(_delta):
- _check_input() # Physikalischer Prozess um Input des Spielers abzufragen (Richtung der Ursprungslinie)
- func _check_input():
- if Input.is_action_pressed("ui_left"): # Spieler drückt die Taste Links runter (Festgelegt in den Project-Settings)
- var new_vec = cast_vec.rotated(deg2rad(-1.0)) # Vektor wird um 1° nach links gedreht. Godot rechnet mit radians, wodurch deg2rad() verwendet werden muss
- var new_angle = rad2deg(new_vec.angle()) # Der Winkel wird nochmal in Grad abgespeichert um ihn für Überprüfungen verwenden zu können
- if new_angle < 0.0: # Wenn der Winkel kleiner als 0.0 ist, ist die Linie außerhalb des gewünschten Bereichs und wird deshalb
- cast_vec = new_vec auf -1, 0 zurückgesetzt
- else:
- cast_vec = Vector2(-1, 0)
- _generate_path() # Richtung hat sich geändert ---> Es wird ein neuer Pfad benötigt
- if Input.is_action_pressed("ui_right"): # Spieler drückt die Taste Links runter (Festgelegt in den Project-Settings)
- var new_vec = cast_vec.rotated(deg2rad(1.0)) # Vektor wird um 1° nach rechts gedreht. Godot rechnet mit radians, wodurch deg2rad() verwendet werden muss
- var new_angle = rad2deg(new_vec.angle()) # Der Winkel wird nochmal in Grad abgespeichert um ihn für Überprüfungen verwenden zu können
- if new_angle < 0.0: # Wenn der Winkel kleiner als 0.0 ist, ist die Linie außerhalb des gewünschten Bereichs und wird deshalb
- cast_vec = new_vec auf 1, 0 zurückgesetzt
- else:
- cast_vec = Vector2(1, 0)
- _generate_path() # Richtung hat sich geändert ---> Es wird ein neuer Pfad benötigt
- func _generate_path(): # Funktion durch welche die Linien generiert werden
- draw_lines.clear() # Die vorher generierten Linien werden aus dem Array entfernt
- Ray_Cast.set_global_position(global_position) # Das RayCast2D wird auf die globale Position dieses Nodes gesetzt. Es wird sich nämlich bewegen
- Ray_Cast.clear_exceptions() # Dieses RayCast2D hat keine Kollisions-Ausnahmen mehr
- var curr_vec = cast_vec # Ein neuer Vector2D wird festgelegt, mit welchem später gerechnet wird.
- var last_collider = null # Wird eine Referenz zu dem Node beinhalten, mit welchem als letztes kollidiert wurde.
- var curr_length = 0.0 # Die momentane Länge der Linie ist 0
- while round(curr_length) < cast_length: # Während die momentane Länge noch nicht der gewünschten Länge entspricht, wird der Prozess wiederholt
- Ray_Cast.set_cast_to(curr_vec * cast_length) # Das RayCast2D zeigt in die Richtung curr_vec mal dem Skalar cast_length. Keine Kollision = Länge 600
- Ray_Cast.force_raycast_update() # Das RayCast2D wird gezwungen sofort zu updaten, wodurch nicht bis zum nächsten Frame gewartet werden muss
- var start = Ray_Cast.global_position # Start-Position der Linie bei der globalen Position des RayCast2D
- var end = Vector2.ZERO # End-Position muss noch bestimmt werden --> Initialisierung auf Vektor der Länge 0 (Enum für Vector2(0, 0))
- if Ray_Cast.is_colliding(): # Ist wahr wenn das RayCast2D kollidiert
- var collider = Ray_Cast.get_collider() # Das Node mit welchem kollidiert wurde wird gespeichert
- Ray_Cast.add_exception(collider) # Das Node wird als Ausnahme hinzugefügt (Keine doppelten Kollisionen hintereinander am selben Node)
- if last_collider != null: # Es wurde vorher schon einmal mit einem anderen Node kollidiert
- Ray_Cast.remove_exception(last_collider) # Die Ausnahme wird wieder gelöscht, damit wieder mit dem Node kollidiert werden kann
- last_collider = collider # last_collider wird geupdated
- var point = Ray_Cast.get_collision_point() # Punkt der Kollision wird bestimmt (Dieser Punkt ist in globalem Raum)
- Ray_Cast.set_global_position(point) # Die Position des Ray_Cast wird zu diesem Punkt gesetzt, damit es wieder in eine andere Richtung zeigen kann
- end = point # End-Position der Linie bei Punkt der Kollision
- else:
- end = Ray_Cast.global_position + Ray_Cast.get_cast_to() # Keine Kollision ---> End-Position bei Position des RayCast2D + dem Vektor wohin es zeigt
- var to_vec = end - start # Verbindungsvektor von Start bis zum Ende
- var new_vec = to_vec # Verbindungsvektor wird nochmal abgespeichert, damit er verändert werden kann
- if curr_length + to_vec.length() > cast_length: # Die Länge würde die maximale Länge überschreiten
- new_vec = curr_vec * (cast_length - curr_length) # Der neue Vektor wird zurechtgestutzt, damit er perfekt auf die gewollte Länge kommt
- draw_lines.push_back([start, start + new_vec]) # Start-Punkt und End-Punkt der Linie wird im Array gespeichert
- curr_length += new_vec.length() # Die insgesamte Länge aller Linien wird aktualisiert
- if Ray_Cast.is_colliding(): # Es wird nochmal abgefragt ob das RayCast kollidert (Dieser Code darf erst hier ausgeführt werden)
- if !Ray_Cast.get_collision_normal() == Vector2.ZERO: # Fix für Fehler in Vector2.bounce() bei Vector.ZERO
- curr_vec = curr_vec.bounce(Ray_Cast.get_collision_normal()) # Vector2.bounce berechnet den resultierenden Vektor anhand des Normalen-Vektors der Kollision
- update() # die _draw() Funktion wird aufgerufen
- func _draw(): # Funktion um etwas auf den Bildschirm zu zeichnen
- for args in draw_lines: # Loop durch das Array mit den gespeicherten Linien
- draw_line(to_local(args[0]), to_local(args[1]), Color.pink, 2) # Es wird eine Linie gezeichnet, dafür müssen die globalen Koordinaten aber in Lokale umgewandelt werden
Da bei der anderen Version das meiste ähnlich ist, werde ich da nur den Code posten:
- extends Node2D
- export(Vector2) var cast_vec = Vector2(0, -1)
- export(int) var cast_length = 100
- export(int) var cast_bounces = 500
- onready var Ray_Cast = get_node("RayCast2D")
- var draw_lines = []
- func _ready():
- _generate_path()
- func _physics_process(_delta):
- _check_input()
- update()
- func _check_input():
- if Input.is_action_pressed("ui_left"):
- var new_vec = cast_vec.rotated(deg2rad(-1.0))
- var new_angle = rad2deg(new_vec.angle())
- if new_angle < 0.0:
- cast_vec = new_vec
- else:
- cast_vec = Vector2(-1, 0)
- _generate_path()
- if Input.is_action_pressed("ui_right"):
- var new_vec = cast_vec.rotated(deg2rad(1.0))
- var new_angle = rad2deg(new_vec.angle())
- if new_angle < 0.0:
- cast_vec = new_vec
- else:
- cast_vec = Vector2(1, 0)
- _generate_path()
- func _generate_path():
- draw_lines.clear()
- Ray_Cast.set_global_position(global_position)
- Ray_Cast.clear_exceptions()
- var curr_vec = cast_vec
- var last_collider = null
- for i in cast_bounces:
- Ray_Cast.set_cast_to(curr_vec * cast_length)
- Ray_Cast.force_raycast_update()
- if Ray_Cast.is_colliding():
- var collider = Ray_Cast.get_collider()
- Ray_Cast.add_exception(collider)
- if last_collider != null:
- Ray_Cast.remove_exception(last_collider)
- last_collider = collider
- var point = Ray_Cast.get_collision_point()
- draw_lines.push_back([Ray_Cast.global_position, point])
- Ray_Cast.set_global_position(point)
- if !Ray_Cast.get_collision_normal() == Vector2.ZERO:
- curr_vec = curr_vec.bounce(Ray_Cast.get_collision_normal())
- else:
- draw_lines.push_back([Ray_Cast.global_position, Ray_Cast.global_position + Ray_Cast.get_cast_to()])
- break
- func _draw():
- for args in draw_lines:
- draw_line(to_local(args[0]), to_local(args[1]), Color.pink, 2)
Vielleicht bringt es ja irgendjemandem etwas.