Defender clone (devlog, WIP)
Asked Answered
S

20

0

A (kind of) clone of the 80's arcade game Defender, a 2D sidescrolling shooter.

This is WIP and all the visuals are placeholders (based on the graphics in the C64 manual of Defender) that I'll eventually replace by proper pixel sprites. Also I won't make a perfect clone of Defender, I am just using the game as an example project to learn Godot. Also this is actually my first Godot project and so far I am having a pretty good time while learning the engine.

Playable web version: https://toxe.itch.io/godot-defender-clone

Latest update (milestone 6): Spawn waves, player lives, extra lives, proper gameplay loop, improved level wrap-around mechanic, debug/cheat menu.

  • Player has two lives at the beginning and gains an extra life every 10000 points.
  • Each level has 3 spawn waves to complete.
  • Complete a level by killing all Landers and Mutants from all waves.
  • Each spawn wave consists of 6 Landers, 2 Bombers and 2+ random enemies. Every two levels an additional random enemy is spawned.
  • Every 15 seconds a Baiter gets spawned.
  • Important gameplay change: The player speed is no longer "normalized", meaning the player keeps their horizontal speed even when flying diagonally which feels a lot better.
  • Completely reworked the level wrap-around mechanic.

Project source code on GitHub (MIT license):
https://github.com/Toxe/godot-defender-clone

Playlist of short snippets of all the milestones:
https://www.youtube.com/playlist?list=PLGbGLZ39xAFiEQbf70v8F0RuCvVGr5roT

Suprarenal answered 8/9, 2023 at 15:34 Comment(0)
S
0

Milestone 01: Simple enemies and player controls.
https://github.com/Toxe/godot-defender-clone/tree/milestone-01/G01_Defender

Suprarenal answered 8/9, 2023 at 15:39 Comment(0)
S
0

Milestone 02: Horizontal scrolling, wrap-around, camera and level made out of chunks.
https://github.com/Toxe/godot-defender-clone/tree/milestone-02/G01_Defender

Suprarenal answered 8/9, 2023 at 15:40 Comment(0)
S
0

Milestone 03: Multiple enemy types, humans, enemies roughly aim at the player.
https://github.com/Toxe/godot-defender-clone/tree/milestone-03/G01_Defender

Suprarenal answered 8/9, 2023 at 15:40 Comment(0)
S
0

Milestone 04: Score, game controller, title screens, game over message and player explosion.
https://github.com/Toxe/godot-defender-clone/tree/milestone-04/G01_Defender

Suprarenal answered 8/9, 2023 at 15:41 Comment(0)
S
0

Milestone 5: Internal refactoring, improved GameController and scene loading, proper high score screen.
https://github.com/Toxe/godot-defender-clone/tree/milestone-05/G01_Defender

Suprarenal answered 18/9, 2023 at 13:9 Comment(0)
S
0

I uploaded the project on GitHub (MIT license) and added links to every update.

Main repository: https://github.com/Toxe/godot-defender-clone

Suprarenal answered 18/9, 2023 at 15:6 Comment(0)
C
0

Suprarenal Come on, give us the online version to playtest πŸ™‚

Chardin answered 18/9, 2023 at 15:39 Comment(0)
S
0

Chardin Haha true, good point! I'll look into it.

Suprarenal answered 18/9, 2023 at 15:53 Comment(0)
S
0

Chardin Alright, here we go: https://toxe.itch.io/godot-defender-clone

The export was surprisingly easy. And first time that I am publishing something on itch.io.

Although I am having the issue that I cannot exactly specify the size of the game. My desktop version is 1152 x 648 and that is what I am supporting. But the HTML preview version on itch.io slightly bigger. Therefore parts of the game window shouldn't be visible which might result in clipping left and right. Also the player and enemies shouldn't be able to fly so low.

But whatever, I can address these issues in the future. For now this worked pretty well.

Suprarenal answered 25/9, 2023 at 11:57 Comment(0)
C
0

Suprarenal Works fine so far, but it could use a lot more polish. It reminded me of this presentation on juicing up a game.

Chardin answered 25/9, 2023 at 12:6 Comment(0)
S
1

Chardin Oh of course it does. πŸ˜€

This is WIP and all the visuals are placeholders (based on the graphics in the C64 manual of Defender) that I'll eventually replace by proper pixel sprites.

My focus is mostly on other things than presentation. At the moment it's the internals and foundation to have something to build upon (structurally) in future projects. If I just wanted to hack "my first game" together I would have been finished already.

At the moment I am refactoring a couple of things like the wrap-around-the-level mechanic.

Suprarenal answered 25/9, 2023 at 13:5 Comment(0)
C
0

Suprarenal Sure. But these things are not just mere "presentation". They are the "feel" of the game so they are quite important. But yeah focus on architectural thing first πŸ‘οΈ

Btw I notices bullets standing still in the air in couple of occasions. Not sure if it's intentional (like mines) or just a bug.

Chardin answered 25/9, 2023 at 13:23 Comment(0)
S
0

Chardin They are the "feel" of the game so they are quite important.

Absolutely!

Chardin Btw I notices bullets standing still in the air in couple of occasions. Not sur if it's intentional (like mines) or just a bug.

Yes, those are mines. There should be 2 to 4 in a row.

Suprarenal answered 25/9, 2023 at 13:34 Comment(0)
S
0

Ha, awesome! I updated my level-wrap-around code and this worked a lot better than I expected.

The old stuff

As you may know Defender has this level wrapping mechanic where you can fly in one direction and the level will eventually wrap around and repeat. Previously I implemented this basically by carrying two big (2 and 3 times the screen size) collision shapes around with the player (camera). One collider would detect if an enemy would exit on the left or right side and if that is the case wrap it around to the other side of the level.

The level is made out of 4 level chunks. Each chunk is the size of the screen.

And so I had I second collider that would detect if a level chunk would leave this second collider when the player moved too far into one direction.

So basically the player sits in the center and the world gets wrapped around them, meaning level chunks and enemies jump left or right off-screen.

Well, that's the short of it. All of this worked good enough so far but I didn't really like it all that much because a) it caused issues after the player was destroyed and respawned and b) it's very player-centric and I didn't like that the player (or camera actually) was lugging around these colliders and have authority over things that they shouldn't have.

The new stuff

So I changed it. The camera is just a dumb camera once again and now each level chunk has a collider that detects if something leaves to the left or right.

If an enemy or shot leaves the left-most or right-most level chunk then it will wrap around to other side of the level.

Should the player leave a level chunk then level chunks opposite of the player movement direction will wrap around to the other side of the level. Also now more than one chunk can wrap around, to make sure there is always ample space off-screen in front of the player.

A possible problem

So far so good. But initially I thought that this would still cause some headache because when I wrap around the level chunks then I would also need to wrap around the enemies that are "in" those chunks. But: Enemies and chunks are both children of the world and not of each other. Enemies are drawn over the chunks but they are not children of those chunks, meaning when I wrap a chunk it doesn't automatically wrap enemies with it.

I thought about changing the hierarchy so that enemies and shots are children of chunks but that would mean that I would need to re-parent them when they move around. And generally it would make everything ugly.

So I decided that for now it would be better to find all the enemies and shots that are "inside" a chunk once it moves and move them as well.

But...! πŸ™‚

Turns out I don't have to. Everything happens automagically, which is always the best.

I totally did not think about this initially but once I move a level chunk to the other side of the level that would of course automatically trigger the "level chunk exited" signal for all those enemies and shots that were "inside" the chunk. After all it doesn't matter if small collider A leaves big collider B or collider B gets pulled away from under A. The result is the same: A is no longer inside B and has exited.

And that made everything fairly simple and clean.

I made a recording with the normal game view initially and then zoomed out.

Couple of notes:

  • I debug-draw the collision shapes.
  • Player is invulnerable.
  • When zoomed out shots leave the world at the top and bottom and player lasers fly around infinitely. This doesn't happen in the real game because they automatically get destroyed once they leave the visible screen area.
  • At one point there is an enemy shooting in exactly the frame where its level chunk wraps around to the other side, leaving its shot off-screen outside the level. This also wouldn't happen in the real game because enemies don't shoot when off-screen.
  • There will always be two screens in front of the player.

For those curious, this is the code. The component is attached to World. And yeah, I know that I am naughty casting the value of an enum to int, but hey, it's just so convenient... πŸ˜‡

(Ignore the "1152" constant, this will get replaced eventually.)

class_name WrapAroundLevelComponent extends Node

enum WrapAroundSide {
    left = -1,
    right = 1
}


func _ready():
    var world = get_parent() as World

    if world:
        assert(world.get_ordered_level_chunks().size() >= 3)

        for chunk in world.get_ordered_level_chunks():
            chunk.level_chunk_exited.connect(_on_level_chunk_exited)


func number_of_level_chunks_in_front_of_player(total_number_of_level_chunks: int) -> int:
    return ceili((total_number_of_level_chunks - 1) / 2.0)


func wrap_node_around_level(node: Node2D, wrap_to_side: WrapAroundSide, number_of_level_chunks: int, level_chunk_width: int):
    node.global_position.x += number_of_level_chunks * level_chunk_width * int(wrap_to_side)


func wrap_chunks_around_level(level_chunks: Array[Node], begin_index: int, end_index: int, wrap_to_side: WrapAroundSide):
    for i in range(begin_index, end_index):
        wrap_node_around_level(level_chunks[i], wrap_to_side, level_chunks.size(), 1152)


func _on_level_chunk_exited(level_chunk: LevelChunk, node: Node2D):
    var world = get_parent() as World
    var level_chunks = world.get_ordered_level_chunks()

    if node is Player:
        var chunk_exited_index := level_chunks.find(level_chunk)
        assert(chunk_exited_index >= 0)

        if node.global_position.x < level_chunk.global_position.x:
            # wrap to left side
            var chunk_index := chunk_exited_index - 1
            var number_of_chunks_to_wrap = number_of_level_chunks_in_front_of_player(level_chunks.size()) - chunk_index

            if number_of_chunks_to_wrap > 0:
                wrap_chunks_around_level(level_chunks, level_chunks.size() - number_of_chunks_to_wrap, level_chunks.size(), WrapAroundSide.left)
        elif node.global_position.x > level_chunk.global_position.x + 1152:
            # wrap to right side
            var chunk_index := chunk_exited_index + 1
            var number_of_chunks_to_wrap = number_of_level_chunks_in_front_of_player(level_chunks.size()) - (level_chunks.size() - chunk_index) + 1

            if number_of_chunks_to_wrap > 0:
                wrap_chunks_around_level(level_chunks, 0, number_of_chunks_to_wrap, WrapAroundSide.right)
    else:
        if node.global_position.x < level_chunks.front().global_position.x:
            wrap_node_around_level(node, WrapAroundSide.right, level_chunks.size(), 1152)
        elif node.global_position.x > level_chunks.back().global_position.x + 1152:
            wrap_node_around_level(node, WrapAroundSide.left, level_chunks.size(), 1152)
Suprarenal answered 26/9, 2023 at 9:2 Comment(0)
S
0

Added a simple debug/cheat UI. The code is very hacky and directly accesses other nodes and private values. But... whatever. Because a) I don't even know how long I want to keep it and b) I don't want to bloat all that other code just for some debug stuff.

extends Control

@export var main_level: MainLevel = null

var _spawn_waves_component: SpawnWaves = null


func _ready():
    if OS.has_feature("debug"):
        if main_level:
            _spawn_waves_component = main_level.get_node("SpawnWaves")
        start_or_stop_ui_update_timer()


func _unhandled_key_input(event: InputEvent):
    if OS.has_feature("debug"):
        if event is InputEventKey and event.is_pressed() and not event.is_echo():
            match (event.keycode):
                KEY_C: toggle_visibility()
                KEY_P: debug_kill_player()
                KEY_K: debug_kill_all_enemies()
                KEY_L: debug_kill_landers_and_mutants()
                KEY_N: debug_spawn_new_wave()


func debug_kill_player():
    get_tree().get_first_node_in_group("player").get_node("HitboxComponent")._on_area_entered(null)


func debug_kill_all_enemies():
    for enemy in Enemy.collect_existing_enemies(get_tree()):
        enemy.get_node("HitboxComponent")._on_area_entered(null)


func debug_kill_landers_and_mutants():
    for enemy in Enemy.collect_existing_enemies(get_tree()):
        if enemy.type == Enums.EnemyType.Lander || enemy.type == Enums.EnemyType.Mutant:
            enemy.get_node("HitboxComponent")._on_area_entered(null)


func debug_spawn_new_wave():
    if _spawn_waves_component:
        _spawn_waves_component.get_node("Timer").start(0.2)


func toggle_visibility():
    visible = !visible
    start_or_stop_ui_update_timer()


func start_or_stop_ui_update_timer():
    if visible:
        $UIUpdateTimer.start()
    else:
        $UIUpdateTimer.stop()


func _on_ui_update_timer_timeout():
    if _spawn_waves_component:
        var s = "spawn waves left: %d, time until next wave: %.01f" % [_spawn_waves_component._spawn_waves_left, _spawn_waves_component.get_node("Timer").time_left]
        if _spawn_waves_component._spawning_new_wave:
            s += " [spawning new wave]"
        %SpawnWavesLabel.text = s
Suprarenal answered 1/10, 2023 at 15:0 Comment(0)
S
0

I recently added spawn waves, so... why not spawn 30 waves at once? (Game runs smooth, I just had to convert the GIF down to 12 FPS to keep it under the upload limit of 2 MB.)

Suprarenal answered 4/10, 2023 at 8:56 Comment(0)
S
0

"why not spawn 30 waves at once?"

Always the right way to think about things. Quite a captivating image! It makes me wonder if 99% of the game could be inside of a single shader.

Splice answered 4/10, 2023 at 9:3 Comment(0)
S
0

Milestone 6: Spawn waves, player lives, extra lives, proper gameplay loop, improved level wrap-around mechanic, debug/cheat menu.

We have a proper game now! \o/ The were a lot of changes but most of it internally. Visually not much has changed.

  • Player has two lives at the beginning and gains an extra life every 10000 points.
  • Each level has 3 spawn waves to complete.
  • Complete a level by killing all Landers and Mutants from all waves.
  • Each spawn wave consists of 6 Landers, 2 Bombers and 2+ random enemies. Every two levels an additional random enemy is spawned.
  • Every 15 seconds a Baiter gets spawned.
  • Important gameplay change: The player speed is no longer "normalized", meaning the player keeps their horizontal speed even when flying diagonally which feels a lot better.
  • Completely reworked the level wrap-around mechanic. Look some posts above for the details.
  • Debug/cheat menu for quickly spawning new waves, kill enemies or the player.

Next milestone will probably see a lot of gameplay changes like adjusting timings (spawns, shooting) and speed (enemies, player) and also proper AI behavior for some enemies. After that we need the Defender mini map, sound and finally proper graphics.

Code: https://github.com/Toxe/godot-defender-clone/tree/milestone-06

Playable web version: https://toxe.itch.io/godot-defender-clone (Which for some reason only seems to have the correct viewport size if the desktop has a resolution scale of 100%. Bummer.)

Suprarenal answered 7/10, 2023 at 13:27 Comment(0)
C
0

Suprarenal Needs enemy explosions, ASAP! πŸ˜‰

Chardin answered 7/10, 2023 at 14:59 Comment(0)
S
0

Chardin Haha it sure does! Also some spawn/warp-in animations for the enemies.

Suprarenal answered 7/10, 2023 at 15:45 Comment(0)

© 2022 - 2024 β€” McMap. All rights reserved.