HOME

EARTH DEFENSE SURVIVORS

can you survive long enough to save earth?

a work in progress prototype of a top down survivors game made in godot. right now it's a few rough levels where i build and try out mechanics, not a finished game

the clips below are recorded from the prototype with a script doing the steering.

✚✚✚

Figure 1 - the squad level

WHAT IS THIS?

each level is there to test an idea. invasion is endless waves that get bigger every round, king hill is a boss ant hill that spawns ants faster the more you hurt it, and the squad level has three npc squad mates and buildings you can knock down. you only steer and your weapons fire on their own

buildings that fall apart

skyscrapers take damage from your bullets and catch fire and then collapse into pieces. it's the newest mechanic and nothing in the level reacts to it yet

Figure 2 - two skyscrapers collapsing

hand made sprites

every character and prop is hand made pixel art, from the squad and the giant ants to the worm and the ant hill. the ant walks on its four frame cycle from the game

  • player

    player

  • squad mate

    squad mate

  • giant ant

  • bee

    bee

  • honey trap

    honey trap

  • worm

    worm

  • snake

    snake

  • ant hill

    ant hill

  • skyscraper

    skyscraper

  • xp crystal

    xp crystal

  • slow badge

    slow badge

  • wasp stinger

    wasp stinger

Figure 3 - the sprites in the game so far

SOUNDTRACK

i wrote the music for it too

  1. 01title theme - loops on the menus and under invasion and king hill

    0:00 / 0:35

HOW IT'S BUILT

it's all godot 4 and gdscript. every level is a scene whose root node is called Main, with the player and the hud and two containers under it, and almost everything else is a scene of its own that gets dropped into one of those containers while you play

the level keeps what the enemy leaves behind

an enemy never adds its own blood or xp crystal or damage number to the scene. it emits on_enemy_spawn_node with the node and a type and the level puts it in the right container, so when the enemy frees itself on death everything it dropped stays on the ground

# enemy.gd
signal on_enemy_spawn_node(node_to_spawn:Node, node_type:String)

func _try_spawn_xp_crystal():
    ...
    on_enemy_spawn_node.emit(new_crystal, "ITEM")

# level_template.gd
func spawn_new_node(new_node:Node, node_type:String):
    if node_type == "ITEM":
        item_container.add_child(new_node)
    elif node_type == "FLOAT_LABEL":
        float_label_container.add_child(new_node)

one base class and a hook per enemy

Enemy holds the stats every bug has and does the shared work. health is a setter that plays the hurt sound and calls _die() at zero, and take_damage shows the number before it calls custom_take_damage, which does nothing in the base class. the ant fills that hook in to spray blood back the way the bullet came, more of it the harder it got hit

# enemy.gd
func take_damage(projectile_direction:Vector2, damage:int):
    var new_damage_number = damage_number.instantiate()
    new_damage_number.format_damage_number(damage, global_position)
    on_enemy_spawn_node.emit(new_damage_number, "FLOAT_LABEL")
    custom_take_damage(projectile_direction, damage)
    current_health -= damage

# enemy_ant.gd
func custom_take_damage(projectile_direction: Vector2, damage: int):
    blood_splatter.trigger_from_damage(projectile_direction, damage)

one weapon scene with a different script on it

the rifle and the flamethrower are the same weapon scene with a fire timer and a reload timer. the Weapons node makes two of them and swaps the script on each one, and the numbers live in a .tres resource so i can tune them in the inspector without touching code. each bullet rolls its damage when it lands, anywhere from 0 to 20

# weapons.gd
var primary_weapon:Weapon = weapon_node.instantiate()
primary_weapon.set_script(rifle)
var secondary_weapon:Weapon = weapon_node.instantiate()
secondary_weapon.set_script(flamethrower)

# rifle.tres
clip_size = 5
fire_time = 0.85
reload_time = 2.0
base_damage = 1
damage_random_multiplier = 20

leveling up is a chain of signals

each step only knows about the next one, so the upgrade menu has no idea a rifle exists and the rifle has no idea there was a menu. each crystal's xp is divided by (level + 1) / 2 before it counts, which is what makes the later levels slower

  1. Player collect_xp_crystal()
  2. ⚡ player_leveled_up
  3. Level pauses the tree and opens the UpgradeMenu
  4. ⚡ on_upgrade_selected
  5. PlayerUpgrades apply_upgrade()
  6. ⚡ on_upgrade_applied
  7. Weapons apply_upgrade()
  8. Rifle handle_upgrade_application("rifle_reload_speed")

enemies think on a timer

any enemy with a plan has a state enum and a StateTimer. every timeout runs one arm of a match statement, which does its thing and sets the next state and restarts the timer. the worm hides and rolls a die to decide whether to come up, screams for four seconds and goes back down. the wasp flies at you and thinks and then shoots a needle. THINK always goes to SHOOT for now

HIDING
EMERGING
FLEEING
SCREAMING

Figure 4 - the worm's states, one per StateTimer timeout

buildings simulate their own gravity

the game is top down so physics gravity is set to zero. when a skyscraper runs out of health it hides its sprite and frees its collision and a BuildingCollapse node drops 28 sprite pieces of it with gravity it works out itself every frame. the higher pieces start falling later and each one bounces up to three times off a ground line set in the inspector

# collapse.gd
@export var stagger_by_height: bool = true
@export var gravity: float = 900.0
@export var ground_y: float = 400.0
@export var bounce_damping: float = 0.35
@export var max_bounces: int = 3

the shake

getting hit adds 0.3 trauma to the camera. trauma drains at 1.5 a second and gets squared before it moves the camera along some noise, so small hits barely move it and stacked hits shake it hard. when a mission ends the game drops to 0.3x speed for a moment before it pauses on the results screen

STACK:

godot 4.7gdscriptgdfxr for the sound effects.tres resources for weapons and upgrades and status effects

SIZE:

~3.7k lines of gdscriptabout a third of it is the worm's shake and strobe effects

TESTS:

a manual testing levelno automated tests

WHAT I'D DO DIFFERENTLY

  • ✚make the level base class before the second level. invasion and king hill each have their own copy of the same hud and spawn and level up handlers, and the shared Level class only showed up this september, so only the newest level uses it so far
  • ✚hand nodes a reference instead of having them look one up by name. every enemy finds its level with get_tree().root.get_node("Main"), which means every level's root node has to be named Main
  • ✚deal upgrade cards without putting them back in the deck. it picks each card with pick_random() from the whole pool, so it can deal the same upgrade three times