res://scenes/main_scene.tscn erstellenMainSceneParallax2D node hinzufügenRepeat Size: (360,360)Repeat Times: 17Sprite2DTexture: darkPurple.pngScale: 2.0RigidBody2D hinzufügenSprite2DTexture: spaceShips_005.pngRotation: 180Scale: 0.34Create CollisionPolygon2D sibling
Camera2D hinzufügenProcess Callback: PhysicsPosition Smoothing: enabledposition_smoothing_speed: 15
shader_type canvas_item;
const float max_red_value = 190.0 / 255.0;
uniform vec4 target_color : source_color = vec4(0.7, 0.0, 0.0, 1.0);
void fragment() {
vec4 current_pixel = texture(TEXTURE, UV);
// check if pixel is primarily red coloured
if(current_pixel.r > (current_pixel.b + current_pixel.g) * 0.5)
{
// divide by 190 (converted from 255 to 1.0 value), ca highest red value of the original images
float mult = current_pixel.r / max_red_value;
// check how close the colors are to approximate blending between colors
float diff_g = current_pixel.g / current_pixel.r;
// get min value of current color to adjust blending target
float avg_val = (current_pixel.r + current_pixel.g + current_pixel.b) / 3.0;
// adapt target color to actual pixel color
current_pixel.rgb = target_color.rgb * mult + (vec3(avg_val) - target_color.rgb * mult) * diff_g;
}
COLOR = current_pixel;
}
res://components/shaders/team_color_shader.gdshader
res://components/materials/team_color_shadermat.tres
@icon("res://assets/icons/heart.svg")
class_name HealthComponent
extends Node2D
@export var health: float = 100.0:
set(val):
health = val
health_changed.emit()
if health <= 0 and not was_zero:
health_zero.emit()
var health_percentage: float:
get():
return health / initial_health
var was_zero: bool = false
var initial_health: float
signal health_changed
signal health_zero
func _ready():
health_changed.connect(on_health_changed)
health_zero.connect(on_health_zero)
get_parent().add_to_group("has_health")
get_parent().set_meta("health_path", get_path())
initial_health = health
func on_health_zero():
was_zero = true
print("ded")
func on_health_changed():
if health >= 0 and was_zero:
was_zero = false
func damage(value: float):
health = clampf(health - value, 0.0, initial_health)
class_name HealthBar
extends ProgressBar
var hp_comp_parent: Node2D
var hp_comp: HealthComponent
var hp_offset: Vector2
func _ready():
hp_comp = get_parent()
hp_comp_parent = hp_comp.get_parent()
hp_offset = hp_comp.position
hp_comp.top_level = true
hp_comp.health_changed.connect(_on_health_changed)
_on_health_changed()
func _process(_delta):
hp_comp.global_position = hp_comp_parent.global_position + hp_offset
func _on_health_changed():
value = hp_comp.health_percentage * 100
if value >= 100:
hide()
elif not visible:
show()
res://components/health_component.gd anlegenres://components/health_bar.gd anlegenres://components/health_bar.tscn anlegneres://components/health_bar.tscn:
false
size: (70,10)position: (-35,-5)HealthComponent unter dem Spieler instantiieren und
etwas nach oben schieben
HealthBar unter der HealthComponent instantiieren
Gravity: (0,0)Viewport Width/Height: (1280, 720)
move_up: Wmove_down: Smove_left: Amove_right: Dboost: SPACEres://scenes/character/manual_control.gd anlegen
ManualControl Node unter Spieler anlegenres://scenes/character/player_ship.gdanlegen
@icon("res://assets/icons/node_steering.svg")
class_name ManualControl
extends Node
@export var is_enabled: bool = true
@onready var player: SpaceShip = get_parent()
var camera: Camera2D
var camera_zoom_tween: Tween
var camera_zoom = 1
const ZOOM_FACTOR = 0.1
const MAX_ZOOM = 3.5
const MIN_ZOOM = 0.3
func _ready():
camera = get_node_or_null("../Camera2D")
if not camera:
push_error("No Camera found to control, disabling camera controls...")
func _physics_process(_delta):
if not is_enabled:
return
player.targeting_point = player.get_global_mouse_position()
func _input(event):
if event is InputEventMouseButton:
var mouse_event: InputEventMouseButton = event
if mouse_event.button_index == MOUSE_BUTTON_LEFT and mouse_event.pressed:
player.shoot_primary()
if mouse_event.button_index == MOUSE_BUTTON_RIGHT and mouse_event.pressed:
player.shoot_secondary()
if camera:
if mouse_event.button_index == MOUSE_BUTTON_WHEEL_UP and mouse_event.pressed:
var factor = clamp(camera_zoom * (1 + ZOOM_FACTOR), MIN_ZOOM, MAX_ZOOM)
_zoom_camera_to(factor)
elif mouse_event.button_index == MOUSE_BUTTON_WHEEL_DOWN and mouse_event.pressed:
var factor = clamp(camera_zoom * (1 - ZOOM_FACTOR), MIN_ZOOM, MAX_ZOOM)
_zoom_camera_to(factor)
elif mouse_event.button_index == MOUSE_BUTTON_MIDDLE and mouse_event.pressed:
_zoom_camera_to(1)
elif event is InputEventKey:
player.acceleration_input = Input.get_vector("move_left", "move_right", "move_up", "move_down")
if event.is_action("boost"):
player.is_boosting = event.pressed
func _zoom_camera_to(factor: float):
if not camera:
push_warning("Zoom camera called without camera, aborting...")
if camera_zoom_tween:
camera_zoom_tween.kill()
camera_zoom_tween = camera.create_tween()
camera_zoom_tween.tween_property(camera, "zoom", Vector2(factor, factor), 0.05)
camera_zoom = factor
class_name SpaceShip
extends RigidBody2D
@export var speed: float = 500
@export var acceleration: float = 1000
var is_thrusting : bool = false:
set(val):
is_thrusting = val
thrusting_changed.emit(is_thrusting)
var is_boosting : bool = false:
set(val):
is_boosting = val
boosting_changed.emit(is_boosting)
var targeting_point: Vector2 = Vector2.ZERO
var acceleration_input: Vector2 = Vector2.ZERO
signal thrusting_changed(is_thrusting: bool)
signal boosting_changed(is_boosting: bool)
signal primary_fire_triggered()
signal secondary_fire_triggered()
signal damaged(damage: float)
func _ready():
add_to_group("ship")
func _physics_process(_delta):
# handle acceleration input
apply_central_force(acceleration_input * acceleration)
# handle aiming
look_at(targeting_point)
rotation_degrees += 90
check_for_thrusting_state()
func check_for_thrusting_state() -> void:
if acceleration_input.is_zero_approx() != not is_thrusting:
is_thrusting = !is_thrusting
thrusting_changed.emit(is_thrusting)
func shoot_primary():
primary_fire_triggered.emit()
func shoot_secondary():
secondary_fire_triggered.emit()
func take_damage(damage: float):
damaged.emit(damage)
ExhaustFire anlegenSprite2D und ein
CPUParticles2D
extends Sprite2D
var tween: Tween
@onready var smoke_particles: CPUParticles2D = $ExhaustSmoke
func _ready():
get_parent().thrusting_changed.connect(on_is_boosting_changed)
modulate = Color.TRANSPARENT
smoke_particles.scale_amount_min *= scale.x
smoke_particles.scale_amount_max *= scale.x
smoke_particles.emitting = false
func on_is_boosting_changed(is_boosting: bool) -> void:
if tween:
tween.kill()
if is_boosting:
tween = create_tween()
tween.set_loops()
tween.tween_property(self, "modulate", Color.WHITE, 0.3)
tween.tween_property(self, "modulate", Color(1, 1, 1, 0.5), 0.3)
smoke_particles.emitting = true
else:
tween = create_tween()
tween.tween_property(self, "modulate", Color.TRANSPARENT, 0.5)
smoke_particles.emitting = false
res://scenes/weapons/projectile_settings.gd anlegen
ProjectileSettings)
res://scenes/weapons/laser_shot.tscn anlegen mit
Script
Area2DCollisionShape2D in Form eines CapsuleSprite2D zB als "laserRed05.png"
class_name ProjectileSetting
extends Resource
@export var projectile_scene: PackedScene = null
@export_group("Missile Settings")
@export_custom(PROPERTY_HINT_GROUP_ENABLE,"") var is_missile: bool = false
## fuel duration of missile until it runs out
@export var fuel_duration: float = 5
## Turning speed in degree/seconds
@export var turning_speed: float = 30
@export var turning_gain: float = 100
@export var turning_damp: float = 10
@export var velocity_alignment_rate: float = 1.0
@export var boosting_strength: float = 50
## Distance of detector polygon (FOV 45deg) in px
@export var detector_distance: float = 180
## Explosion radius
@export var explosion_radius: float = 50
@export var explosion_damage: float = 15
@export var impact_damage: float = 10
@export_group("Laser Settings")
@export_custom(PROPERTY_HINT_GROUP_ENABLE, "") var is_laser: bool = false
@export var laser_velocity: float = 50
@export var laser_damage: float = 10
@export var laser_energy_cost: float = 5
@export var laser_lifetime: float = 3.0
class_name LaserShot
extends Area2D
var settings: ProjectileSetting
@export var ray: RayCast2D
var lifetime: float
var base_velocity: Vector2
var source_body: Node2D
func init_laser(projectile_settings: ProjectileSetting, shooter: Node2D , initial_velocity: Vector2 = Vector2.ZERO):
settings = projectile_settings
base_velocity = initial_velocity
source_body = shooter
func _ready():
add_to_group("laser")
if settings == null:
push_warning("No settings given on spawn. Self destructing...")
# queue_free()
settings = ProjectileSetting.new()
return
ray.target_position = Vector2.RIGHT * settings.laser_velocity
lifetime = settings.laser_lifetime
func _physics_process(delta):
lifetime -= delta
if lifetime <= 0:
queue_free()
return
# move laser by laser_velocity/seconds
position += transform.x.normalized() * settings.laser_velocity * delta + base_velocity * delta
func _on_body_entered(body: Node2D):
# to prevent multiple collisions after moving
if is_queued_for_deletion():
return
# exclude shooter
if body == source_body:
return
# check if body can be damaged
if body.is_in_group("has_health"):
# get health component of body and damage it
body.get_node(body.get_meta("health_path")).damage(settings.laser_damage)
#-- wird später gebraucht, noch auskommmentiert da die missile noch fehlt (danach wieder reinmachen!)
# if body.is_in_group("missile"):
# var missile = body as Missile
# missile.explode()
# self destruct
queue_free()
@icon("res://assets/icons/cannon.svg")
class_name ShipGun
extends Node2D
@export var loaded_primary: ProjectileSetting
@export var loaded_secondary: ProjectileSetting
@export var available_projectiles: Array[ProjectileSetting]
@onready var ship: SpaceShip = get_parent()
func _ready():
ship.primary_fire_triggered.connect(on_primary_fire)
ship.secondary_fire_triggered.connect(on_secondary_fire)
func on_primary_fire():
if not loaded_primary or not loaded_primary.projectile_scene:
push_warning("Loaded projectile missing or incorrect, not firing!")
return
shoot_shot(loaded_primary)
func on_secondary_fire():
if not loaded_secondary or not loaded_secondary.projectile_scene:
push_warning("Loaded projectile missing or incorrect, not firing!")
return
shoot_shot(loaded_secondary)
func shoot_shot(settings: ProjectileSetting):
var new_shot = settings.projectile_scene.instantiate()
new_shot.global_position = global_position
new_shot.global_rotation = global_rotation
if settings.is_laser:
new_shot.init_laser(settings, ship, ship.linear_velocity)
else:
new_shot.init_missile(settings, ship, ship.linear_velocity)
ship.add_sibling(new_shot)
res://components/ship_gun.gd anlegenShipGun nodes unter den Spieler auf die Guns
legen und rotieren dass X richtung vorne Zeigt
_on_body_entered →
./_on_body_entered()
RigidBody2D namens Missiletrue1CollisionLayer: 2CollisionMask: 1Sprite2D mit spaceMissiles_001.png,
90° rotation und scale 0.4
CollisionShape2D als capsule und auch rotieren
und anpassen
ExhaustFire spawnen, rotieren und auch auf
0.4 skalieren und dann positionieren
class_name Missile
extends RigidBody2D
@export var draw_target_debug: bool = true
signal thrusting_changed(is_thrusting)
var settings: ProjectileSetting
var lifetime: float
var base_velocity: Vector2
var source_body: Node2D
var fuel: float
var visible_targets: Array[Node2D]
@onready var detector: Area2D = $ShipDetector
@onready var explosion_particles: CPUParticles2D = $ExplosionParticles
var is_burnt_out: bool = false
var is_boosting: bool = false
var current_target: Node2D:
set(val):
current_target = val
queue_redraw()
func init_missile(projectile_settings: ProjectileSetting, shooter: Node2D, initial_velocity: Vector2 = Vector2.ZERO):
await ready
settings = projectile_settings
base_velocity = initial_velocity
source_body = shooter
# shrink/grow detection polygon according to settings
var detector_polygon = detector.get_child(0) as CollisionPolygon2D
var detector_factor = settings.detector_distance / 60.0
detector_polygon.polygon = PackedVector2Array([
Vector2.ZERO,
Vector2(-60, -60) * detector_factor,
Vector2(60, -60) * detector_factor,
]
)
# set initial setting values
fuel = settings.fuel_duration
func _ready():
add_to_group("missile")
var col_layer = collision_layer
var col_mask = collision_mask
collision_layer = 0
collision_mask = 0
get_tree().create_timer(1).timeout.connect(func():
collision_layer = col_layer
collision_mask = col_mask
is_boosting = true
thrusting_changed.emit(true)
)
apply_impulse(global_transform.x * 300)
pass
func explode(deal_damage = true):
if is_queued_for_deletion():
return
# change explosion particles to be independent from missile
remove_child(explosion_particles)
get_parent().add_child(explosion_particles)
explosion_particles.global_position = global_position
# handle explosion
if deal_damage:
var space_state = get_world_2d().direct_space_state
var shape = CircleShape2D.new()
shape.radius = settings.explosion_radius
var query = PhysicsShapeQueryParameters2D.new()
query.shape = shape
query.transform = query.transform.translated(global_position)
query.collide_with_bodies = true
query.collide_with_areas = false
query.exclude = [self]
var results = space_state.intersect_shape(query)
for result in results:
var node: Node = result.collider
if node.is_in_group("has_health"):
var health_comp: HealthComponent = node.get_node(node.get_meta("health_path"))
health_comp.damage(settings.explosion_damage)
# make the one shot particles emit
explosion_particles.emitting = true
# adjust visuals to explosion radius
explosion_particles.emission_sphere_radius = settings.explosion_radius
explosion_particles.scale_amount_min *= settings.explosion_radius / 50
explosion_particles.scale_amount_max *= settings.explosion_radius / 50
# make explosion particles delete themselves after emission finished
explosion_particles.finished.connect(explosion_particles.queue_free.bind())
queue_free()
func _physics_process(delta):
if is_burnt_out:
return
if not is_boosting:
return
fuel -= delta
# check if rocket is out of fuel
if fuel <= 0.0:
is_burnt_out = true
thrusting_changed.emit(false)
current_target = null
queue_redraw()
# explode in 2 seconds if it didn't already happen
get_tree().create_timer(2).timeout.connect(
func():
if self:
self.explode()
)
return
# get closest target to aim at
for body: Node2D in visible_targets:
if not current_target or current_target.global_position.distance_squared_to(global_position) < body.global_position.distance_squared_to(global_position):
current_target = body
if visible_targets.is_empty():
current_target = null
# aim towards target
if current_target:
queue_redraw()
## PID like aiming
var target_dir = (current_target.global_position - global_position).normalized()
var target_error = target_dir.angle() - global_rotation
target_error = wrapf(target_error, -PI, PI)
var proportional_torque = target_error * settings.turning_gain
var damping_torque = - angular_velocity * settings.turning_damp
apply_torque(proportional_torque + damping_torque)
var missile_angle = global_transform.x.angle()
var vel_angle = linear_velocity.angle()
var angle_diff = wrapf(missile_angle - vel_angle, -PI, PI)
var rotation_amount = clampf(angle_diff, -settings.velocity_alignment_rate, settings.velocity_alignment_rate)
linear_velocity = linear_velocity.rotated(rotation_amount)
else:
apply_torque(-angular_velocity * 0.8)
# accelerate
if current_target:
apply_central_force(global_transform.x * settings.boosting_strength)
else:
# only apply half acceleration without a target (be "idle")
apply_central_force(global_transform.x * settings.boosting_strength * 0.5)
func _on_ship_detector_body_entered(body: Node2D):
# ignore self
if body == self:
return
if body.is_in_group("ship"):
visible_targets.append(body)
func _on_ship_detector_body_exited(body: Node2D):
if body in visible_targets:
visible_targets.erase(body)
func _draw():
if current_target and draw_target_debug:
draw_line(Vector2.ZERO, to_local(current_target.global_position), Color(0,1,0,0.5), 5)
func _on_body_entered(_body: Node):
explode()
Area2D mit
CollisionPolygon2D erstellen mit Namen
"ShipDetector"
CollisionLayer: 0CollisionMask: 1CPUParticles2D erstellen mit Namen
ExplosionParticles und visuell anpassen
body_entered() und
body_exited mit Missile Script verbinden
body_entered
ProjectileSettings anlegenspaceShips_008.png)Wenn man noch Zeit hat zum experimentieren