2025/09/23

- Type
- Learning Resource
- Format
- Video
- Version
- Godot 4.x
- Downloadable Demo
- Created
- Updated
- 2023/12/06
- 2025/09/23
It may come as a surprise, but roguelike games like Vampire Survivors are some of the easiest, most satisfying games that you can whip up in a single sitting. In this tutorial, you will learn how to make a simple 2D game in Godot 4, move a character around, aim at enemies, shoot projectiles, and spawn enemies that follow the player.
Below you can find the complete code for all the scripts used in this project at the end of the video. You can use it to compare your code against it if you get stuck.
Here's the complete code for the script at the end of the video:
extends CharacterBody2D
signal health_depleted
var health = 100.0
func _physics_process(delta):
const SPEED = 600.0
var direction = Input.get_vector("move_left", "move_right", "move_up", "move_down")
velocity = direction * SPEED
move_and_slide()
if velocity.length() > 0.0:
%HappyBoo.play_walk_animation()
else:
%HappyBoo.play_idle_animation()
# Taking damage
const DAMAGE_RATE = 6.0
var overlapping_mobs = %HurtBox.get_overlapping_bodies()
if overlapping_mobs:
health -= DAMAGE_RATE * overlapping_mobs.size() * delta
%HealthBar.value = health
if health <= 0.0:
health_depleted.emit()
Here's the complete code for the script at the end of the video:
extends Area2D
func _process(_delta):
var enemies_in_range = get_overlapping_bodies()
if enemies_in_range.size() > 0:
var target_enemy = enemies_in_range.front()
look_at(target_enemy.global_position)
func shoot():
const BULLET = preload("res://bullet_2d.tscn")
var new_bullet = BULLET.instantiate()
new_bullet.global_transform = %ShootingPoint.global_transform
%ShootingPoint.add_child(new_bullet)
func _on_timer_timeout() -> void:
shoot()
Here's the complete code for the script at the end of the video:
extends CharacterBody2D
signal died
var speed = randf_range(200, 300)
var health = 3
@onready var player = get_node("/root/Game/Player")
func _ready():
%Slime.play_walk()
func _physics_process(_delta):
var direction = global_position.direction_to(player.global_position)
velocity = direction * speed
move_and_slide()
func take_damage():
%Slime.play_hurt()
health -= 1
if health == 0:
var smoke_scene = preload("res://smoke_explosion/smoke_explosion.tscn")
var smoke = smoke_scene.instantiate()
get_parent().add_child(smoke)
smoke.global_position = global_position
queue_free()
Here's the complete code for the script at the end of the video:
extends Area2D
var travelled_distance = 0
func _physics_process(delta):
const SPEED = 1000
const RANGE = 1200
position += Vector2.RIGHT.rotated(rotation) * SPEED * delta
travelled_distance += SPEED * delta
if travelled_distance > RANGE:
queue_free()
func _on_body_entered(body):
queue_free()
if body.has_method("take_damage"):
body.take_damage()
Here's the complete code for the script at the end of the video:
extends Node2D
func spawn_mob():
%PathFollow2D.progress_ratio = randf()
var new_mob = preload("res://mob.tscn").instantiate()
new_mob.global_position = %PathFollow2D.global_position
add_child(new_mob)
func _on_timer_timeout():
spawn_mob()
func _on_player_health_depleted():
%GameOver.show()
get_tree().paused = true
In this project, you might notice that when slimes touch the top of your player character, they sometimes get stuck there. This happens because the node we use for the player is set to the "grounded" motion mode by default, which is designed for platformer games. CharacterBody2D
In grounded mode, the engine treats the top of the character as a floor with a slope! So when slimes collide with it, they behave like they're walking up a hill instead of just generally hitting something they cannot move through. This actually creates a fun little mechanic where enemies can stick onto the player without needing complex code.
If you want to prevent this sticking effect, you can change the player's motion mode to "floating":
The "floating" mode is the setting we normally use for top-down games when we don't want characters to stick to each other. With that, slimes will not stick to the top of the player anymore!
This video comes with two zip files:
In the game, mobs will keep spawning forever and accumulate. This can cause performance problems and make the screen too crowded if the game runs for a long time. Here's how to limit the total number of mobs that can exist at the same time.
To implement this feature, you need to modify the script. Here's the updated code broken down step by step.
First, we add a variable to keep track of the current number of mobs and a constant to define the maximum allowed (MAX_MOBS
):
extends Node2D
+const MAX_MOBS = 50
+
+var mob_count = 0
Then, we update the spawn_mob()
function to count mobs and check if we've reached the maximum number of mobs before spawning a new one. When we create a new mob, we also increase our mob_count
variable.
func spawn_mob():
+ if mob_count >= MAX_MOBS:
+ return
%PathFollow2D.progress_ratio = randf()
var new_mob = preload("res://mob.tscn").instantiate()
new_mob.global_position = %PathFollow2D.global_position
add_child(new_mob)
+ mob_count += 1
Next, we need to detect when mobs leave the game. For that we connect to the tree_exited
signal for each mob we create. Every node emits this signal when it gets removed from the scene tree, which happens when mobs get destroyed by bullets.
func spawn_mob():
if mob_count >= MAX_MOBS:
return
%PathFollow2D.progress_ratio = randf()
var new_mob = preload("res://mob.tscn").instantiate()
new_mob.global_position = %PathFollow2D.global_position
add_child(new_mob)
mob_count += 1
+ new_mob.tree_exited.connect(_on_mob_tree_exited)
+func _on_mob_tree_exited():
+ mob_count -= 1
Here, we connect the new mob's tree_exited
signal to our _on_mob_tree_exited()
function. This means whenever this mob gets destroyed, Godot will call our function once.
The _on_mob_tree_exited()
function decreases our counter by 1. This makes room for a new mob to spawn next time the timer times out.
With these changes, the game will now limit the number of mobs to 50 at any given time. You can adjust the MAX_MOBS
constant to increase or decrease this limit for your game.
Don't stop here. Step-by-step tutorials are fun but they only take you so far.
Try one of our proven study programs to become an independent Gamedev truly capable of realizing the games you’ve always wanted to make.
Get help from peers and pros on GDQuest's Discord server!
20,000 membersJoin ServerThere are multiple ways you can join our effort to create free and open source gamedev resources that are accessible to everyone!
Sponsor this library by learning gamedev with us onGDSchool
Learn MoreImprove and build on assets or suggest edits onGithub
Contributeshare this page and talk about GDQUest onRedditYoutubeTwitter…