Godot Nodes as Logic

I recently realized an interesting way to use Godot's nodes as a sort of "visual scripting". I would not be surprised if this is already common knowledge among Godot users but that doesn't mean it's not worth discussing. It arose fairly naturally from trying to solve a problem, so I figured I'd detail what happened here.
 
While working on the first area for Chasing Elysium, I was finding myself wanting a way to be able to trigger specific cutscenes or check for specific story flags to control what nodes actually appear in a scene. Writing a bespoke script every time would be... tedious at best. Inefficient. And a blatant violation of the DRY principle.
 
So I ended up writing a few reusable nodes. Initially, I extended Area2D to
  • Only trigger when the entering body is the player
  • (Optionally) only trigger once
  • (Optionally) check the current story flag against a per-node range
  • Emit a `triggered` signal
 
This forms a basic area trigger, one which can be set to only go off if story flag conditions are met. However, this is fairly limiting - baking the story flags into the area trigger gives an implicit "these triggers are intended to be attached to story progress" aspect. It also violates single responsibility.
 
Next step - separate the story flag checks out. Made a simple node that watches for story flag changes. If the current story flag is within the given range, all of this node's children are made active and processing. If the story flag falls out of the given range, the children are deactivated. Extremely simple! Then I can strip out the story flag checks from the area triggers and just compose the nodes. Anything that needs a story flag check can be a child of the story flag node. Simple! Small, simple responsibilities that I can compose using the parent-child relationship.
 
The code for the story flag check node is:
class_name StoryCondition
extends Node

@export var minimum: StoryFlag.Flag = StoryFlag.Flag.NONE
@export var maximum: StoryFlag.Flag = StoryFlag.Flag.NONE

func _ready() -> void:
	StoryFlag.story_flag_updated.connect( _on_story_flag_updated )

func _on_story_flag_updated( flag: StoryFlag.Flag ) -> void:
	var min_met = flag >= minimum or minimum == StoryFlag.Flag.NONE
	var max_met = flag <= maximum or maximum == StoryFlag.Flag.NONE
	process_mode = Node.PROCESS_MODE_INHERIT if min_met and max_met \
			else Node.PROCESS_MODE_DISABLED
I included failsafes so that if no story flag is set it is considered a non-condition. That way we can have conditions that aren't just within a range, but also pure minimums or maximums.
 
The code for the area trigger is:
class_name TriggerArea
extends Area2D

signal triggered

@export var story_flag_update: StoryFlag.Flag = StoryFlag.Flag.NONE

var _activated: bool = false

func _is_trigger_active() -> bool:
	return true

func _on_body_entered( _body: Node2D ) -> void:	
	if _is_trigger_active() and not _activated:
		# We know they're the player cause the mask is set to JUST the player so we're good to go
		_activated = true
		if story_flag_update != StoryFlag.Flag.NONE:
			StoryFlag.current = story_flag_update
		triggered.emit()
I do have a bit more clean up to do here. I haven't yet separated out the story_flag_update parameter into its own node, but that will be easy to do when I get around to it. I also want to tweak how the masks are set as currently it is set in a scene that I need to instantiate each time it's used. I just need to find a good way to ensure I am setting the mask to the player's collision mask. I'm sure this again will be easy, I simply haven't done the research to do it as there is so much to do in general. It's on my list though.
 
You get the gist though. We can make a series of very, *very* simple nodes with individual bits of functionality. Some can activate or deactivate their children, allowing for control of what appears in a scene and when. Some can emit signals. Some (which are on my list to write) can set variables or run logic.
 
This is probably a very basic strategy for many Godot users, but I am still learning and it nevertheless is part of what makes the engine so powerful. Compositing simple nodes to create complex logic, enhanced via signals. There's no need for bespoke scripts when you can create modular, reusable components and tie them together. I still have a bit of refactoring to do but it's already making my scenes much cleaner and easier to put together.