CreateCreateBearbeiten der Skripte in vscode oder anderen Editoren statt in Godot direkt.
Notiz: Den internen Editor auszuprobieren lohnt sich durchaus, da er sehr nette Features hat.
(Beispiel an vscode hier schauen für andere)
Editor→Editor Settings...→Text Editor/External
Exec Path: [vscode path] (kann in appdata liegen)
Exec Flags:
{project} --goto {file}:{line}:{col}
Use External Editor: TrueScriptDebugDebug with External EditorUm das Projekt schnell neu zu laden
Editor→Editor Settings...→Shortcuts
reloadReload Current Project→ ALT+RALT+R drücken)FileSystemnew Folderscenes nennenscenesCreate New → Scene...Set as Main Scene
Legt die Größe des Spielfensters fest
Project →Project Settings...→General
Display→WindowViewport Width: 1280Viewport Heigth: 720ScenePongGame) RechtsklickAdd Child Nodesprite suchen und
Sprite2D hinzufügen
Background
Background auswählenInspectorTexture Linksklick →
GradientTexture2D
Gradient klickenWidth: 1280Height: 720BackgroundNode2D→Transform→Position
x: 1280/2y: 720/2/scenes/playerCreate new→Scene...
Root Type: CharacterBody2DScene Name: paddleCollisionShape2DShape: RectangleShape2DSize von Shape auf (30,120)Sprite2DGradientTexture2DInspector unten bei
Node:
Script: New Script.../scenes/player/paddle.gdCharacterBody2DSkript Inhalt:
class_name Padddle
extends CharacterBody2D
/scenes/player/paddle.tscn via drag&drop in
Szene ziehen
PaddleATransformx: 30y: 720/2 (360)PaddleBTransformx: 1280-30 (1250)y: 720/2 (360)Code unten erklärt
class_name Paddle
extends CharacterBody2D
#-- Die Geschwindigkeit des Paddles. Kann man dank @export pro Instanz einstellen.
## speed of paddle movement
@export var speed: float = 400.0
#-- Normalerweise würde man die Größe direkt auslesen aus CollisionShape2D oder Sprite2D
#-- Hier zu vereinfachung von späteren Code hardcoded
## size of paddle (used for ball calculation later)
const size: float = 120.0
#-- Kann man sich vorstellen wie ein Joystick
#-- Nach unten -1 und nach oben 1
## movement direction: -1.0 down and 1.0 up
var movement: float = 0.0
func _physics_process(delta):
# If Movement is not zero
if movement != 0.0:
velocity.x = 0
#-- Hier wird zur Sicherheit die movement Variable clamped (min -1 und max 1)
#-- falls die variable falsch von außen gesetzt wird.
#-- das wird noch mit _speed_ multipliziert für die richtige Geschwindigkeit.
# set vertical velocity and clamp movement in case it's set incorrectly
velocity.y = clampf(movement, -1.0, 1.0) * speed
#-- move_and_collide hat einen Rückgabewert bei Kollision; wird hier verworfen
#-- Wir nutzen hier nicht move_and_slide, da wir an der Wand stehen bleiben wollen.
# move until hitting an obstacle
move_and_collide(velocity * delta)
Project→Project Settings...→Input Map
PaddleAUp: WPaddleADown: SPaddleBUp: ↑PaddleBDown: ↓Human Controller erstellen und einrichten
neues Skript
/scenes/player/human_controller.gd anlegen
class_name HumanController
extends Node
@export var action_up: String = "PaddleAUp"
@export var action_down: String = "PaddleADown"
@onready var paddle: Paddle = get_parent()
func _physics_process(_delta):
paddle.movement = Input.get_axis(action_up, action_down)
Die exportierten Action Strings geben die Möglichkeit die beiden Spieler abzubilden ohne zwei Skripte zu schreiben.
StaticBody2D hinzufügen und
Walls benennen
CollisionShape2D hinzufügenRectangleShape2D definieren(1280, 20)(1280/2, -10)CollisionShape2D duplizieren mit CTRL+D(1280/2, 730)PongGame node eine
Area2D hinzufügen
GoalLeftCollisionShape2D hinzufügen(20, 720+40)(-10, 720/2)Area2D mit der
CollisionShape2D Gruppieren
GoalRight(1280+10, 720/2)CharacterBody2D unter
PongGame erstellen
Node Gruppe "ball" hinzufügen
Sprite2D node hinzufügenGradientTexture2D(40, 40)CollisionShape2D hinzufügen
RectangleShape2D(40, 40)Code erklärung unten
class_name PongBall
extends CharacterBody2D
const BOUNCE_ANGLE: float = 45
#-- Signal bei Paddle-Ball Kollision worauf Effekte oder KIs reagieren können
signal ball_collision(collision: KinematicCollision2D)
#-- Start Geschwindigkeit geben (primär zum testen am Anfang)
func _ready():
velocity = Vector2(300, 0)
func _physics_process(delta):
#-- Solange gerade aus bewegen bis eine Kollision auftritt
var collision: KinematicCollision2D = move_and_collide(velocity * delta)
#-- Unterscheidung zwischen Paddle und anderem damit die Paddle auch Zielen können
if collision:
# check if collided with a paddle
var collider: Node = collision.get_collider()
if collider.is_in_group("paddles"):
# make special bounce to make aiming possible
_handle_paddle_collision(collision)
ball_collision.emit(collision)
else:
# bounce normally
velocity = velocity.bounce(collision.get_normal())
func _handle_paddle_collision(collision: KinematicCollision2D) -> void:
#-- extra Fehlerbehandlung falls die Gruppe "Paddle" fälschlich gesetzt wird
if collision.get_collider() is not Paddle:
push_warning("Collision with non-Paddle object: %s" % collision.get_collider().name)
return
#-- Lustige Mathe um das Bounce verhalten ähnlich zu Breakout zu machen
#-- Nur bedingt das originale PONG verhalten, aber macht es interessanter
#-- Großteil des Codes kann weggelassen werden ohne dieses Sonderverhalten
# if the ball hits a paddle, adjust the angle based on where it hit
var paddle: Paddle = collision.get_collider()
var paddle_center: Vector2 = paddle.global_position
var hit_position: Vector2 = collision.get_position()
# calculate how far up or down the paddle the ball collided
var diff = hit_position.y - paddle_center.y
diff = diff / (paddle.size / 2.0)
var magnitude: float = velocity.length()
velocity = collision.get_normal() * (magnitude + abs(diff) * 100.0)
#-- Der Vektor wird in eine Richtung rotiert um den gesteuerten Bounce zu machen
if velocity.x > 0:
# when going left
velocity = velocity.rotated(deg_to_rad(BOUNCE_ANGLE * diff))
else:
# when going right
velocity = velocity.rotated(deg_to_rad(-BOUNCE_ANGLE * diff))
paddles zu den Paddles hinzufügen/scenes/player/paddle.tscnpaddles hinzufügenPongGame Szene
aktualisiert werden
class_name PongGame
extends Node2D
@export var ball: PongBall
@export var paddle_left: Paddle
@export var paddle_right: Paddle
@export var label_a: Label
@export var label_b: Label
var score_a: int = 0
var score_b: int = 0
signal kickoff
signal goal(goal_on_right: bool)
func _ready():
reset_ball(true)
#-- Diese beiden Funktionen werden auf Tore reagieren
#-- Noch sind die nicht verbunden und werden nicht aufgerufen
func _on_goal_left_body_entered(body: Node2D) -> void:
if body.is_in_group("ball"):
score_b += 1
label_b.text = str("%02d" % score_b)
reset_ball(true)
print("Goal for Player B! Score: ", score_b)
goal.emit(true)
func _on_goal_right_body_entered(body: Node2D) -> void:
if body.is_in_group("ball"):
score_a += 1
label_a.text = str("%02d" % score_a)
reset_ball(false)
print("Goal for Player A! Score: ", score_a)
goal.emit(false)
func reset_paddle_pos() -> void:
paddle_left.position.y = 720 / 2.0
paddle_right.position.y = 720 / 2.0
#-- Ball wird in die Mitte gesetzt mit leichten offset von position und Richtung
#-- Damit ist das Spiel ein bisschen interessanter
func reset_ball(towards_left: bool = true) -> void:
ball.position = Vector2(1280.0/2.0, 720.0/2.0) + Vector2.UP * (randf() - 0.5) * 100
ball.velocity = Vector2.ZERO
await get_tree().create_timer(1.0).timeout
if towards_left:
ball.velocity = Vector2(-300, 0)
else:
ball.velocity = Vector2(300, 0)
# rotate by 10% of 180 degrees for random kickoff
ball.velocity = ball.velocity.rotated(randf() * PI * 0.1)
kickoff.emit()
PongGame verbinden
GoalLeft→Signal body_entered
PongGame→_on_goal_left_body_entered
GoalRight→Signal body_entered
PongGame→_on_goal_right_body_entered
class_name SimpleAI
extends Node
@export var ball: PongBall
@onready var paddle: Paddle = get_parent()
@export var avoid_vibration: bool = true
func _physics_process(delta):
if avoid_vibration:
_chase_ball_no_vibration(delta)
else:
_chase_ball_simple()
#-- Wir müssen hier bei der Movement nicht auf -1 bis 1 begrenzen
#-- da wir das bereits im Ball Skript gemacht haben.
#-- Hier gibt es zwei Versionen der Bewegungen, eine sehr einfache
## just move towards the ball
func _chase_ball_simple() -> void:
paddle.movement = ball.position.y - paddle.position.y
#-- und eine die die gebrauchte Distanz Ausrechnet und macht
## try to not overshoot
func _chase_ball_no_vibration(delta: float) -> void:
paddle.movement = (ball.position.y - paddle.position.y) / paddle.speed / delta
PongGame node einen
StaticBody2D erstellen
DetectionWallLeftRectangleShape2D(30, 720/2)detectors auf den StaticBody hinzufügen
Layer: 2Detection: -DetectionWallRight
(30, 720/2)(1280-30, 720/2)
class_name PredictiveAI
extends Node2D
@export var ball: PongBall
#-- detector_layer ist für den Raycast als Bitmaske für die Layer.
#-- Eine Seite braucht Layer 1 und 2, die andere 1 und 3
@export var detector_layer: int = 0b0011
@onready var paddle: Paddle = get_parent()
signal target_predicted(target_height: float)
var target_y: float = 720.0 / 2.0 # Default to center of the screen
var raycast_path: Array = []
func _physics_process(delta):
# go towards the target Y position
paddle.movement = (target_y - paddle.position.y) / paddle.speed / delta
func _on_ball_collision(collision: KinematicCollision2D) -> void:
var collider: Node = collision.get_collider()
#-- Wenn der Ball das andere Paddle trifft, rechne einen neue Prediction aus
# check if the ball just collided with the other paddle
if collider.is_in_group("paddles") and collider != paddle:
raycast_path.clear()
# raycast and bounce until hit a detection layer
var curr_pos: Vector2 = collision.get_position()
raycast_path.append(curr_pos)
var direction: Vector2 = ball.velocity.normalized()
calc_new_target_y(curr_pos, direction, collider)
queue_redraw()
#-- Wenn der Ball das eigene Paddle trifft, lösche die Visualisierung
#-- und zurück auf neutrale Position
elif collider == paddle:
# if the ball hit itself, just set the target to the center
target_y = 720.0 / 2.0
raycast_path.clear()
# print("Ball hit itself, resetting target to center.")
queue_redraw()
#-- Bei Kickoff muss getestet werden ob der Ball auf das eigene Paddle zugeht
#-- dafür wird verglichen ob der Ball links vom Paddle ist und nach rechts geht
#-- und umgekehrt
func _on_kickoff() -> void:
var ball_left_of_paddle: bool = ball.global_position.x < paddle.global_position.x
if ((ball_left_of_paddle and ball.velocity.x > 0.0)
or
(not ball_left_of_paddle and ball.velocity.x < 0.0)):
calc_new_target_y(
ball.global_position,
ball.velocity.normalized(),
null
)
#-- Um eine Prediction zu machen werden raycasts geschossen bis eine Wand getroffen wird
#-- und reflektiert. Das wirdd wiederholt bis ein detector getroffen wird.
func calc_new_target_y(start_pos: Vector2, start_dir: Vector2, prev_collider: Node) -> void:
var curr_pos: Vector2 = start_pos
var direction: Vector2 = start_dir
var emergency_counter: int = 0
var ray_length: float = 2000.0 # Arbitrary long distance
var is_finished: bool = false
var ray_end: Vector2 = start_pos + start_dir * ray_length
var space_state = paddle.get_world_2d().direct_space_state
# fallback to paddle if no previous collider is provided
var last_collider: Node = prev_collider if prev_collider else paddle
while not is_finished:
emergency_counter += 1
if emergency_counter > 50:
push_error("Emergency counter exceeded 100. Stopping raycast to prevent infinite loop.")
return
# print("dir: ", direction)
var query = PhysicsRayQueryParameters2D.create(
curr_pos,
ray_end,
detector_layer,
[last_collider, paddle, ball],
)
query.hit_from_inside = false
var result = space_state.intersect_ray(query)
if result.is_empty():
push_error("Raycast did not hit anything. Aborting...")
return
if result["collider"].is_in_group("detectors"):
is_finished = true
target_y = result["position"].y
target_y = paddle.get_parent().to_local(Vector2(0, target_y)).y
target_predicted.emit(result["position"].y)
raycast_path.append(result["position"])
target_y += (randf() - 0.5) * 40 # Add some randomness to the target Y position
# print("target_y set to: ", target_y)
else:
if result["normal"] == Vector2.ZERO:
# print("pos: ", result["position"])
push_error("Raycast hit a collider with no normal. Aborting...")
return
curr_pos = result["position"]
direction = direction.bounce(result["normal"])
ray_end = curr_pos + direction * ray_length
# print("bounce at: ", curr_pos)
raycast_path.append(curr_pos)
last_collider = result["collider"]
#-- Mit der Draw funktion wird direkt auf das malen der Node2D zugegriffen
#-- hier wird der zuvor gespeicherte predictive path gemalt
#-- außerdem werden Start und Ende markiert.
func _draw():
if raycast_path.size() > 0:
var draw_arr: Array = []
for point in raycast_path:
draw_arr.append(point)
# print(raycast_path)
for i in range(raycast_path.size() - 1):
draw_line(draw_arr[i], draw_arr[i + 1], Color(1, 0, 0), 3)
draw_circle(draw_arr[0], 5, Color(0, 1, 0)) # Start point
draw_circle(draw_arr[-1], 5, Color(0, 0, 1)) # End point
PredictiveAI node unter
PadddleLeft hinzufügen
Top Level machen
(CanvasItem/Visibility/TopLevel)
PongBall in export variable setzenkickoff von PongGame verbinden
zu _on_kickoff()
ball_collision von
PongBall verbinden zu
_on_ball_collision()
DetectorLayer von 3 auf 5 setzen)