This post is part of Godot FPS tutorial series.
Concept: A game without goals can feel directionless. By building a robust and data-driven objective system, we can easily create engaging missions and guide the player’s experience. This system will be our storyteller, turning a simple sandbox into a structured adventure.
Now, it’s time to give the player a purpose. A simple “shoot everything” goal is fine, but a structured objective system can create compelling scenarios, from “reach the evac zone” to “eliminate the high-value target.”
In this tutorial, we will build a powerful, data-driven, and highly reusable objective system.
Our objectives for this tutorial are:
- Create the Base Objective Blueprint: We’ll build a BaseObjective resource that defines the core properties of any objective (name, description, etc.).
- Build the Objective Manager: This will be a central node in our level responsible for tracking and progressing through a list of objectives.
- Implement Specific Objective Types: We’ll create several specialized objective scripts that inherit from our base, allowing us to easily create common mission types:
- AreaObjective: “Go to a specific location.”
- KillObjective: “Eliminate a specific enemy.”
- InteractionObjective: “Interact with a specific object.”
By the end, you’ll have a system where you can simply create a few resource files, drop an ObjectiveManager into your level, and have a complete, trackable mission for your players.
The BaseObjective
Resource
First, we need a blueprint. What is an objective? It has a name, a description, and a state (completed or not). We’ll define this using a Resource.
- Create a new folder:
Objectives
. - Inside it, create a new script named
base_objective.gd
.
# base_objective.gd
class_name BaseObjective
extends Resource
signal objective_completed
signal objective_updated(progress: float)
@export var objective_name: String = "New Objective"
@export var objective_description: String = "Complete this objective"
@export var is_optional: bool = false
@export var show_progress: bool = false
var is_completed: bool = false
var progress: float = 0.0
# Called by ObjectiveManager when this objective becomes active
func initialize(objective_manager: ObjectiveManager) -> void:
pass
# Called by ObjectiveManager when this objective is no longer active
func cleanup() -> void:
pass
func update_progress(new_progress: float) -> void:
progress = clamp(new_progress, 0.0, 1.0)
objective_updated.emit(progress)
if progress >= 1.0:
complete()
func complete() -> void:
if is_completed:
print("β οΈ Objective already completed: ", objective_name)
return
is_completed = true
print("\nπ OBJECTIVE COMPLETED: ", objective_name, " π")
print("Description: ", objective_description)
if is_optional:
print("Type: OPTIONAL - Bonus objective completed!")
else:
print("Type: REQUIRED - Mandatory objective completed!")
objective_completed.emit()
This base script is a “contract.” It doesn’t do anything on its own, but it defines the functions and signals that all our future objective types will have, allowing the ObjectiveManager
to interact with them in a consistent way.
The ObjectiveManager
This node is the brain of the system. You’ll add it to your level scene, give it a list of objectives, and it will handle the rest.
- In the
Objectives
folder, create a new script namedObjectiveManager.gd
.
# ObjectivesSystem/ObjectiveManager.gd
class_name ObjectiveManager
extends Node
signal objective_changed(current_objective: BaseObjective)
signal all_objectives_completed
@export_category("Objectives")
@export var objectives: Array[BaseObjective] = []
@export var sequential_mode: bool = true ## If true, objectives must be completed in order. If false, objectives can be completed in any order.
var current_objective_index: int = 0
var current_objective: BaseObjective = null
var completed_objectives: Array[bool] = []
func _ready() -> void:
print("=== OBJECTIVE SYSTEM INITIALIZED ===")
print("Total objectives loaded: ", objectives.size())
print("Sequential mode: ", sequential_mode)
# Initialize completed objectives tracking
completed_objectives.resize(objectives.size())
for i in range(objectives.size()):
completed_objectives[i] = false
if objectives.size() > 0:
for i in range(objectives.size()):
print("Objective ", i + 1, ": ", objectives[i].objective_name, " - ", objectives[i].objective_description)
print("======================================")
if sequential_mode:
start_objective(0)
else:
start_all_objectives()
else:
print("WARNING: No objectives found! Please add objectives to the ObjectiveManager.")
print("======================================")
func start_objective(index: int) -> void:
print("\n--- STARTING NEW OBJECTIVE ---")
# Clean up previous objective
if current_objective:
print("Cleaning up previous objective: ", current_objective.objective_name)
current_objective.cleanup()
current_objective.objective_completed.disconnect(_on_objective_completed)
current_objective.objective_updated.disconnect(_on_objective_updated)
# Set new objective
current_objective_index = index
current_objective = objectives[index]
print("Objective ", index + 1, " of ", objectives.size(), ": ", current_objective.objective_name)
print("Description: ", current_objective.objective_description)
if current_objective.is_optional:
print("Type: OPTIONAL")
else:
print("Type: REQUIRED")
# Connect signals
current_objective.objective_completed.connect(_on_objective_completed)
current_objective.objective_updated.connect(_on_objective_updated)
# Initialize the objective
current_objective.initialize(self)
# Notify UI
objective_changed.emit(current_objective)
print("Objective is now active and ready!")
print("-----------------------------")
func start_all_objectives() -> void:
print("\n--- STARTING ALL OBJECTIVES (FLEXIBLE MODE) ---")
for i in range(objectives.size()):
var objective = objectives[i]
print("Initializing objective ", i + 1, ": ", objective.objective_name)
# Connect signals for each objective
objective.objective_completed.connect(_on_objective_completed_flexible.bind(i))
objective.objective_updated.connect(_on_objective_updated_flexible.bind(i))
# Initialize the objective
objective.initialize(self)
# Set current objective to first one for UI purposes
current_objective = objectives[0]
current_objective_index = 0
objective_changed.emit(current_objective)
print("All objectives are now active and ready!")
print("You can complete them in any order!")
print("----------------------------------------------")
func _on_objective_completed() -> void:
print("\nπ OBJECTIVE COMPLETED! π")
print("Completed: ", current_objective.objective_name)
# Check if all objectives are completed
if current_objective_index == objectives.size() - 1:
print("\nπ === MISSION PASSED === π")
print("ALL OBJECTIVES COMPLETED!")
print("Congratulations, you have finished all tasks!")
print("========================")
all_objectives_completed.emit()
else:
var remaining = objectives.size() - current_objective_index - 1
print("Objectives remaining: ", remaining)
print("Moving to next objective...")
print("=========================")
# Start next objective
start_objective(current_objective_index + 1)
func _on_objective_completed_flexible(objective_index: int) -> void:
var completed_objective = objectives[objective_index]
print("\nπ OBJECTIVE COMPLETED! π")
print("Completed: ", completed_objective.objective_name, " (", objective_index + 1, " of ", objectives.size(), ")")
# Mark as completed
completed_objectives[objective_index] = true
# Check if all objectives are completed
var all_completed = true
var completed_count = 0
for i in range(objectives.size()):
if completed_objectives[i]:
completed_count += 1
else:
all_completed = false
print("Progress: ", completed_count, "/", objectives.size(), " objectives completed")
if all_completed:
print("\nπ === MISSION PASSED === π")
print("ALL OBJECTIVES COMPLETED!")
print("Congratulations, you have finished all tasks!")
print("========================")
all_objectives_completed.emit()
else:
var remaining = objectives.size() - completed_count
print("Objectives remaining: ", remaining)
print("=========================")
# Update current objective to next incomplete one for UI
update_current_objective_display()
func _on_objective_updated(progress: float) -> void:
var percentage = int(progress * 100)
print("π Objective Progress: ", current_objective.objective_name, " - ", percentage, "%")
if percentage >= 75:
print("Almost there! Keep going!")
elif percentage >= 50:
print("Halfway done!")
elif percentage >= 25:
print("Good progress!")
func _on_objective_updated_flexible(objective_index: int, progress: float) -> void:
var objective = objectives[objective_index]
var percentage = int(progress * 100)
print("π Objective Progress: ", objective.objective_name, " (", objective_index + 1, "/", objectives.size(), ") - ", percentage, "%")
if percentage >= 75:
print("Almost there! Keep going!")
elif percentage >= 50:
print("Halfway done!")
elif percentage >= 25:
print("Good progress!")
func update_current_objective_display() -> void:
# Find the next incomplete objective for UI display
for i in range(objectives.size()):
if not completed_objectives[i]:
current_objective = objectives[i]
current_objective_index = i
objective_changed.emit(current_objective)
return
# If all are completed, this shouldn't happen but just in case
current_objective = null
# Helper function to get objective by name
func get_objective_by_name(name: String) -> BaseObjective:
for objective in objectives:
if objective.objective_name == name:
return objective
return null
# Debug function to print current objective status
func debug_print_status() -> void:
print("\n=== OBJECTIVE STATUS DEBUG ===")
print("Sequential mode: ", sequential_mode)
print("Current objective index: ", current_objective_index + 1, " of ", objectives.size())
if sequential_mode:
if current_objective:
print("Current objective: ", current_objective.objective_name)
print("Description: ", current_objective.objective_description)
print("Progress: ", current_objective.progress * 100, "%")
print("Completed: ", current_objective.is_completed)
print("Optional: ", current_objective.is_optional)
else:
print("No current objective")
else:
print("=== ALL OBJECTIVES STATUS ===")
for i in range(objectives.size()):
var obj = objectives[i]
var status = "β
" if completed_objectives[i] else "β³"
print(status, " Objective ", i + 1, ": ", obj.objective_name)
print(" Progress: ", obj.progress * 100, "%")
print(" Completed: ", obj.is_completed)
print("=============================")
# Function to skip current objective (for testing)
func debug_skip_current_objective() -> void:
if sequential_mode:
if current_objective and not current_objective.is_completed:
print("π§ DEBUG: Skipping current objective: ", current_objective.objective_name)
current_objective.complete()
else:
print("π§ DEBUG: No active objective to skip")
else:
print("π§ DEBUG: In flexible mode, specify which objective to skip using debug_skip_objective(index)")
# Function to skip specific objective by index (for flexible mode)
func debug_skip_objective(index: int) -> void:
if index >= 0 and index < objectives.size():
var objective = objectives[index]
if not completed_objectives[index]:
print("π§ DEBUG: Skipping objective ", index + 1, ": ", objective.objective_name)
objective.complete()
else:
print("π§ DEBUG: Objective ", index + 1, " is already completed")
else:
print("π§ DEBUG: Invalid objective index: ", index)
This manager is designed to handle objectives both sequentially or not, depending on the value of sequential_mode
. If it is true
, it automatically starts the next one in the array if previous one is completed. However, if it is false
, we can do objectives in any order and it will be fine.
Implementing Specific Objective Types
Now we’ll create specialized scripts that inherit from BaseObjective
and implement real game logic.
- Create a new folder:
Objectives/ObjectiveTypes
. AreaObjective.gd
: This objective is completed when the player enters a specificArea3D
.
# ObjectivesSystem/AreaObjective.gd
class_name AreaObjective
extends BaseObjective
@export var target_area_nodepath: NodePath ## Area3D
var target_area: Area3D
func initialize(manager: ObjectiveManager) -> void:
print("π― AreaObjective: Setting up area detection...")
target_area = manager.get_node(target_area_nodepath) as Area3D
if target_area:
if not target_area.body_entered.is_connected(_on_area_entered):
target_area.body_entered.connect(_on_area_entered)
print("β
AreaObjective: Connected to area '", target_area.name, "'")
print("π Objective: Reach the designated area")
else:
print("β AreaObjective: No target area assigned!")
push_error("AreaObjective: No target area assigned!")
func cleanup() -> void:
if target_area and target_area.body_entered.is_connected(_on_area_entered):
target_area.body_entered.disconnect(_on_area_entered)
func _on_area_entered(body: Node) -> void:
print("πΆ Someone entered the area: ", body.name)
# Only complete if the player entered the area
if body.is_in_group("players"):
print("β
Player reached the target area!")
complete()
else:
print("β οΈ Not the player - waiting for player to enter...")
KillObjective.gd: This objective is completed when a specific enemy is killed:
# ObjectivesSystem/KillObjective.gd
class_name KillObjective
extends BaseObjective
@export var target_enemy: NodePath
@export var required_kills: int = 1
var enemy_node: Node
var kills: int = 0
var health_component: Health
func initialize(manager: ObjectiveManager) -> void:
print("π― KillObjective: Setting up kill tracking...")
print("Target kills required: ", required_kills)
if not target_enemy.is_empty():
enemy_node = manager.get_node(target_enemy)
if enemy_node:
print("β
Found target enemy: ", enemy_node.name)
# Try to find Health component component
health_component = enemy_node.health_component
if health_component:
if not health_component.died.is_connected(_on_enemy_died):
health_component.died.connect(_on_enemy_died)
print("β
Connected to enemy's health system")
print("π Objective: Eliminate the target enemy")
else:
print("β Enemy node doesn't have a Health component!")
push_error("KillObjective: Enemy node doesn't have a Health component!")
else:
print("β Could not find enemy node at path: ", target_enemy)
push_error("KillObjective: Could not find enemy node at path: ", target_enemy)
else:
print("β No target enemy assigned!")
push_error("KillObjective: No target enemy assigned!")
func cleanup() -> void:
if health_component and health_component.died.is_connected(_on_enemy_died):
health_component.died.disconnect(_on_enemy_died)
func _on_enemy_died() -> void:
kills += 1
print("π Enemy eliminated! Kills: ", kills, "/", required_kills)
if show_progress:
update_progress(float(kills) / float(required_kills))
if kills >= required_kills:
print("β
All required enemies eliminated!")
complete()
else:
var remaining = required_kills - kills
print("π― Enemies remaining: ", remaining)
InteractionObjective.gd: This objective is completed when the player interacts with a specific object:
# ObjectivesSystem/InteractionObjective.gd
class_name InteractionObjective
extends BaseObjective
@export var target_interactable_nodepath: NodePath
var target_interactable: Interactable
var objective_manager: ObjectiveManager
func initialize(manager: ObjectiveManager) -> void:
print("π― InteractionObjective: Setting up interaction detection...")
objective_manager = manager
target_interactable = manager.get_node(target_interactable_nodepath).interactable # Hacky?
if target_interactable:
print("β
Found target interactable: ", target_interactable.name if target_interactable.has_method("get_name") else "Unknown")
# Connect to the interactable's completion signal
if not target_interactable.interaction_completed.is_connected(_on_interaction_completed):
target_interactable.interaction_completed.connect(_on_interaction_completed)
print("π€ Objective: Interact with the target object")
else:
print("β No target interactable assigned!")
push_error("InteractionObjective: No target interactable assigned!")
func cleanup() -> void:
if target_interactable and target_interactable.interaction_completed.is_connected(_on_interaction_completed):
target_interactable.interaction_completed.disconnect(_on_interaction_completed)
func _on_interaction_completed(interactor: Node) -> void:
print("π€ Interaction detected with: ", interactor.name)
# Only complete if the player interacted
if interactor.is_in_group("players"):
print("β
Player successfully completed the interaction!")
complete()
else:
print("β οΈ Non-player interaction - waiting for player...")
Setting Up a Mission
The system is built! Let’s create a simple three-part mission in our level.
- Add the Manager:
- Open Level.tscn.
- Add a Node and name it
ObjectiveManager
. AttachObjectiveManager.gd
to it.
- Create Mission Objects:
- The Alarm: Drag your
Alarm.tscn
into the level. We’ll use this for an interaction objective. - The Enemy: Drag an
Enemy.tscn
into the level. Make sure it has a unique name, like TargetEnemy. - The Zone: Create an
Area3D
with aCollisionShape3D
. Place it somewhere interesting. This will be our “reach the destination” objective.
- The Alarm: Drag your
- Create Objectives:
- Select the
ObjectiveManager
node, and see the inspector on right.- In the
Objectives
array, create a new element and select new InteractionObjective. - Set its
Target Interactable
to theAlarm
node. SetObjective Name
to “Raise Alarm”.
- In the
- Similarly create a new KillObjective.
- Objective Name: “Eliminate the Guard”
- Target Enemy Path: Select your target Enemy node.
- Create a new AreaObjective.
- Objective Name: “Reach the Extraction Point”
- Target Area Path: Select your
Area3D
zone.
- Select the
Great! All three objectives are loaded in the ObjectiveManager
.

ObjectiveSystem
node looks like in scene tree. And on inspector, see how I configured three different objectives.Run the game.
You have successfully created a complete mission using a fully data-driven objective system. You can now create complex scenarios by simply creating resources and placing items in your level, with no additional coding required.
Current version simply implements a bare-bones objectives/mission system and only prints outputs to console, for example when you complete an objective, or when all objectives have been completed.
More Ideas for Improvements
You can extend the system and do whatever you want with it. You can implement UI to display objectives (and objectives status) in HUD. You can control when a mission passes (for example, when all the objectives are done).
You can make different FPS gameplay types from this system such as:
- Mission-based gameplay (like in the IGI series), where each mission has multiple objectives you need to complete before extraction.
- Checkpoint-based progression, where completing objectives unlocks the next area or wave of enemies.
- Survival modes, where βobjectivesβ could be things like holding an area for a certain time, defending an NPC, or collecting resources while under attack.
I mark this as the end of our tutorial series.
Thank you so much for reading <3