Asteroids Destroyer Dev Log – 13 April 2026

Asteroids Destroyer โ€” Development Journal

Dev Log โ€” 13 April 2026

๐Ÿ“… Session 2
๐ŸŽฎ Godot 4.6
๐Ÿ”ง Bug Fix & Polish
โœจ Thruster VFX
๐Ÿ”ง

Session Overview

I focused this session on clearing the carry-over bugs from Session 1, adding the thruster VFX system, and writing the first dev log. The game is now properly playable. The player takes damage, bounces off asteroids, rockets explode on impact, and thrusters animate correctly per ship type.

Fixed Today

  • โœ… Player losing lives on asteroid collision
  • โœ… Player bounce-off instead of stuck
  • โœ… Asteroid collision shapes fitting sprites
  • โœ… Bullet explosion animation (frames 1โ€“9)
  • โœ… Health HUD: single swap-image system
  • โœ… Asset paths โ†’ Game Assests folder

New Today

  • โœจ Thruster animation (7 frames, 16fps)
  • โœจ Per-ship thruster layout (center/sides/both)
  • โœจ Explosion VFX on bullet impact
  • โœจ Dev Log system started
  • ๐Ÿ“ Dev_Log_120426.html (yesterday retrospective)
  • ๐Ÿ“ Dev_Log_130426.html (this file)
๐Ÿ’ฅ

Collision System Rework

Root Cause: One Size Fits None

The asteroid collision shape was a single CircleShape2D with radius = 210 in local space. After node scale is applied, this becomes:

Asteroid size Node scale Local radius World radius Visual half-width โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ LARGE 0.28 210 58.8 px 84 px โ† too small (70% fit)MEDIUM 0.18 210 37.8 px 27 px โ† LARGER than sprite! โŒSMALL 0.11 210 23.1 px 13 px โ† nearly 2ร— sprite! โŒ

MEDIUM and SMALL asteroids had invisible collision halos extending well beyond their sprite, which let the player “collide with empty space” and get trapped.

Dynamic Radius from Texture Size

_fit_collision_shape() is called in _ready() after textures load. It derives the collision radius from the actual loaded image dimensions:

func _fit_collision_shape() -> void:
    var img_size: Vector2 = _textures[0].get_size()
    var radius: float = min(img_size.x, img_size.y) * 0.42
    var shape := CircleShape2D.new()
    shape.radius = radius          # local space โ€” scales with node
    collision_shape.shape = shape
Asteroid Image size New local r New world r Visual half-w โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ A1 (LARGE) 600ร—497 208.7 58.4 px 84 px โ† 70% (good inset)A7 (MEDIUM) 300ร—250 105.0 18.9 px 27 px โ† 70% (matches)A11 (SMALL) 240ร—150 63.0 6.9 px 13 px โ† tight but fair

Player Bounce Fix

Switched from move_and_collide() to move_and_slide(). This handles multiple simultaneous collisions per frame (asteroid clusters), preventing the “stuck between two rocks” scenario. On asteroid hit, velocity is reflected off the collision normal with 0.6ร— damping:

func _move() -> void:
    move_and_slide()    # handles multi-collision clusters

    for i in get_slide_collision_count():
        var c := get_slide_collision(i)
        var collider := c.get_collider()
        if collider and collider.is_in_group("asteroids"):
            velocity = velocity.bounce(c.get_normal()) * 0.6  # bounce + dampen
            take_damage()
            break
The 1.5s invincibility window after taking damage prevents repeated damage in the same collision. The bounce ensures the player physically separates from the asteroid within the same frame.
โœจ

Explosion VFX System

New scenes/effects/Explosion.tscn + scripts/effects/Explosion.gd. Cycles through 9 frames at ~18fps, then self-destructs. Spawned at bullet world position on impact.

Explosion Animation โ€” 9 frames @ 55ms/frame โ‰ˆ 0.5s total Frame: 1 2 3 4 5 6 7 8 9 โ†’ queue_free() Size: 68 80 110 140 176 150 120 90 68 px (grows then shrinks) small โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ peak โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ fade

The explosion is added to get_tree().current_scene rather than the bullet’s parent, so it persists for its full duration after the bullet is freed:

func _spawn_explosion() -> void:
    var e: Node2D = _explosion_scene.instantiate()
    get_tree().current_scene.add_child(e)
    e.global_position = global_position   # world position of impact
Bullet lifecycle on asteroid hit: bullet._on_body_entered(asteroid) โ”‚ โ”œโ”€โ–ถ asteroid.hit() โ† damage the rock โ”œโ”€โ–ถ _spawn_explosion() โ† add explosion at world pos โ””โ”€โ–ถ queue_free() โ† bullet gone, explosion lives on
๐Ÿš€

Thruster Animation System

Thrusters animate through 7 frames at 16fps when the thrust key is held. Each ship variant in a series has a different nozzle configuration. The pattern is consistent across all 3 ship series (CX16, DKO-api, WO84-wu):

X1: Center Only

๐Ÿ”ฅ

Ships: CX16-X1, DKO-api-X1, WO84-wu-X1

Single center nozzle at bottom of fuselage. Position: (0, 53)

X2: Sides Only

๐Ÿ”ฅ ๐Ÿ”ฅ

Ships: CX16-X2, DKO-api-X2, WO84-wu-X2

Two wing pod nozzles. Positions: (-20, 40) and (20, 40)

X3: Center + Sides

๐Ÿ”ฅ๐Ÿ”ฅ๐Ÿ”ฅ

Ships: CX16-X3, DKO-api-X3, WO84-wu-X3

All three nozzles active simultaneously.

Ship Variant Detection

The thruster config is derived from selected_ship % 3. This works because all 3 ship series follow the same X1/X2/X3 pattern:

var variant: int = gm.selected_ship % 3
# 0 โ†’ center only (X1)   1 โ†’ sides only (X2)   2 โ†’ center + sides (X3)
_use_center = (variant == 0 or variant == 2)
_use_sides  = (variant == 1 or variant == 2)

Thruster Nodes in Player.tscn

Player (CharacterBody2D) โ”œโ”€โ”€ Sprite2D scale (0.5, 0.5) โ€” ship texture, 80ร—80px โ”œโ”€โ”€ CollisionShape2D CapsuleShape2D radius=22 height=52 โ”œโ”€โ”€ GunBarrel Marker2D at (0, -40) โ€” bullet spawn point โ”œโ”€โ”€ ThrustAudio AudioStreamPlayer2D โ”œโ”€โ”€ ThrusterCenter Sprite2D at (0, 53) scale (1.5, 1.5) โ€” center nozzle โ”œโ”€โ”€ ThrusterLeft Sprite2D at (-20, 40) scale (1.2, 1.2) โ€” left pod โ””โ”€โ”€ ThrusterRight Sprite2D at ( 20, 40) scale (1.2, 1.2) โ€” right pod

Animation Loop

func _animate_thrusters(delta: float) -> void:
    if not is_thrusting or _thruster_textures.is_empty():
        return
    _thruster_timer += delta
    if _thruster_timer >= THRUSTER_FRAME_DURATION:   # 0.06s
        _thruster_timer -= THRUSTER_FRAME_DURATION
        _thruster_frame = (_thruster_frame + 1) % 7
        var tex := _thruster_textures[_thruster_frame]
        if _use_center: thruster_center.texture = tex
        if _use_sides:  thruster_left.texture = tex
                        thruster_right.texture = tex
๐Ÿ“

Files Changed Today

File Change Type Summary
scripts/entities/Player.gd Rework Thruster animation system, move_and_slide bounce, per-ship thruster config
scenes/entities/Player.tscn Updated Added ThrusterCenter/Left/Right Sprite2D nodes, removed GPUParticles2D
scripts/entities/Asteroid.gd Fix Dynamic collision radius from texture size, size-grouped variant system
scripts/ui/HUD.gd Fix Single health image swap, new asset paths, runtime GameManager ref
scenes/ui/HUD.tscn Rework 3 heart nodes โ†’ single HealthDisplay TextureRect, new joystick asset paths
scripts/effects/Explosion.gd New 9-frame explosion animation, auto queue_free on completion
scenes/effects/Explosion.tscn New Explosion Node2D + Sprite2D
scripts/entities/Bullet.gd Updated Spawns Explosion on impact, explosion added to scene root
Dev Log/Dev_Log_120426.html New Session 1 retrospective
Dev Log/Dev_Log_130426.html New This file
๐ŸŽฑ

Asteroid Bounce Rework (later same day)

The initial bounce using velocity.bounce(collision_normal) felt wrong and was still causing sticking. The root cause: physics collision normals are unreliable when collision shapes don’t perfectly match the sprite. I replaced it with a centre-to-centre push that matches the wall-bounce behaviour exactly.

Why Collision Normals Fail Here

Asteroid CollisionShape Player CollisionShape (circle, slightly off-centre) (capsule) When shapes overlap before physics resolves them, the normal can point in an unexpected direction โ”€โ”€โ–ถ player pushed sideways or along a tangent โ”€โ”€โ–ถ player slides along surface, gets stuck in gaps

The Fix: Centre-to-Centre Push

func _bounce_off_body(body: Object) -> void:
    # Direction from body centre โ†’ player centre (always correct regardless of shape)
    var push_dir := (global_position - body.global_position).normalized()
    if push_dir == Vector2.ZERO:
        push_dir = Vector2.RIGHT   # exact overlap fallback

    # Preserve speed; guarantee minimum 200 px/s exit velocity
    var speed := maxf(velocity.length(), 200.0)
    velocity = push_dir * speed

    # Physical separation โ€” moves player out of collision zone before next frame
    global_position += push_dir * 6.0
Comparison: Before (unreliable) velocity = velocity.bounce(collision_normal) * 0.6 โ†’ normal can be wrong โ†’ sticking, sliding, dampened feel After (robust) push_dir = (player_pos – asteroid_pos).normalized() velocity = push_dir * max(speed, 200) position += push_dir * 6 โ†’ always pushes away from rock centre โ†’ no sticking โ†’ wall-bounce feel

Also extended to enemies group, so the player now bounces off enemy ships the same way.

๐Ÿ“‹

Open Items / Next Session

Thruster position tuning: positions set to estimated values based on ship sprite analysis. May need visual fine-tuning in-editor once tested.
Main Menu: no menu scene yet; game starts directly in GameScene
Level Select: HUD transitions to scenes/menus/LevelSelect.tscn which doesn’t exist yet
Audio: ThrustAudio node present but no audio stream assigned. Explosion sound, asteroid break sound all missing.
Enemy ships: planned from Level 4 onwards, not yet implemented
Power-ups: Health-PU.png and Shield-PU.png assets exist in Game Assests but not yet implemented