basic ECS spawner
This commit is contained in:
191
addons/gecs/lib/array_extensions.gd
Normal file
191
addons/gecs/lib/array_extensions.gd
Normal file
@@ -0,0 +1,191 @@
|
||||
class_name ArrayExtensions
|
||||
|
||||
## Intersects two arrays of entities.[br]
|
||||
## In common terms, use this to find items appearing in both arrays.
|
||||
## [param array1] The first array to intersect.[br]
|
||||
## [param array2] The second array to intersect.[br]
|
||||
## [b]return Array[/b] The intersection of the two arrays.
|
||||
static func intersect(array1: Array, array2: Array) -> Array:
|
||||
# Optimize by using the smaller array for lookup
|
||||
if array1.size() > array2.size():
|
||||
return intersect(array2, array1)
|
||||
|
||||
# Use dictionary for O(1) lookup instead of O(n) Array.has()
|
||||
var lookup := {}
|
||||
for entity in array2:
|
||||
lookup[entity] = true
|
||||
|
||||
var result: Array = []
|
||||
for entity in array1:
|
||||
if lookup.has(entity):
|
||||
result.append(entity)
|
||||
return result
|
||||
|
||||
## Unions two arrays of entities.[br]
|
||||
## In common terms, use this to combine items without duplicates.[br]
|
||||
## [param array1] The first array to union.[br]
|
||||
## [param array2] The second array to union.[br]
|
||||
## [b]return Array[/b] The union of the two arrays.
|
||||
static func union(array1: Array, array2: Array) -> Array:
|
||||
# Use dictionary to track uniqueness for O(1) lookups
|
||||
var seen := {}
|
||||
var result: Array = []
|
||||
|
||||
# Add all from array1
|
||||
for entity in array1:
|
||||
if not seen.has(entity):
|
||||
seen[entity] = true
|
||||
result.append(entity)
|
||||
|
||||
# Add unique items from array2
|
||||
for entity in array2:
|
||||
if not seen.has(entity):
|
||||
seen[entity] = true
|
||||
result.append(entity)
|
||||
|
||||
return result
|
||||
|
||||
## Differences two arrays of entities.[br]
|
||||
## In common terms, use this to find items only in the first array.[br]
|
||||
## [param array1] The first array to difference.[br]
|
||||
## [param array2] The second array to difference.[br]
|
||||
## [b]return Array[/b] The difference of the two arrays (entities in array1 not in array2).
|
||||
static func difference(array1: Array, array2: Array) -> Array:
|
||||
# Use dictionary for O(1) lookup instead of O(n) Array.has()
|
||||
var lookup := {}
|
||||
for entity in array2:
|
||||
lookup[entity] = true
|
||||
|
||||
var result: Array = []
|
||||
for entity in array1:
|
||||
if not lookup.has(entity):
|
||||
result.append(entity)
|
||||
return result
|
||||
|
||||
## systems_by_group is a dictionary of system groups and their systems
|
||||
## { "Group1": [SystemA, SystemB], "Group2": [SystemC, SystemD] }
|
||||
static func topological_sort(systems_by_group: Dictionary) -> void:
|
||||
# Iterate over each group key in 'systems_by_group'
|
||||
for group in systems_by_group.keys():
|
||||
var systems = systems_by_group[group]
|
||||
# If the group has 1 or fewer systems, no sorting is needed
|
||||
if systems.size() <= 1:
|
||||
continue
|
||||
|
||||
# Create two data structures:
|
||||
# 1) adjacency: stores, for a given system, which systems must come after it
|
||||
# 2) indegree: tracks how many "prerequisite" systems each system has
|
||||
var adjacency = {}
|
||||
var indegree = {}
|
||||
var wildcard_front = []
|
||||
var wildcard_back = []
|
||||
for s in systems:
|
||||
adjacency[s] = []
|
||||
indegree[s] = 0
|
||||
|
||||
# Build adjacency and indegree counts based on dependencies returned by s.deps()
|
||||
for s in systems:
|
||||
var deps_dict = s.deps()
|
||||
|
||||
# Check for Runs.Before array on s
|
||||
# If present, each item in s.Runs.Before means "s must run before that item"
|
||||
# So we add the item to adjacency[s], and increment the item's indegree
|
||||
# If item is null or ECS.wildcard, we treat it as "run before everything" by pushing 's' onto wildcard_front
|
||||
if deps_dict.has(System.Runs.Before):
|
||||
for b in deps_dict[System.Runs.Before]:
|
||||
if b == null:
|
||||
# ECS.wildcard AKA 'null' means s should run before all systems
|
||||
wildcard_front.append(s)
|
||||
else:
|
||||
# Find system instance that matches the dependency type
|
||||
var target_system = _find_system_by_type(systems, b)
|
||||
if target_system:
|
||||
# Normal dependency within the group
|
||||
adjacency[s].append(target_system)
|
||||
indegree[target_system] += 1
|
||||
|
||||
# Check for Runs.After array on s
|
||||
# If present, each item in s.Runs.After means "s must run after that item"
|
||||
# So we add 's' to adjacency[item], and increment s's indegree
|
||||
# If item is null or ECS.wildcard, we treat it as "run after everything" by pushing 's' onto wildcard_back
|
||||
if deps_dict.has(System.Runs.After):
|
||||
for a in deps_dict[System.Runs.After]:
|
||||
if a == null:
|
||||
# ECS.wildcard AKA 'null' means s should run after all systems
|
||||
wildcard_back.append(s)
|
||||
else:
|
||||
# Find system instance that matches the dependency type
|
||||
var dependency_system = _find_system_by_type(systems, a)
|
||||
if dependency_system:
|
||||
# Normal dependency within the group
|
||||
adjacency[dependency_system].append(s)
|
||||
indegree[s] += 1
|
||||
|
||||
# Kahn's Algorithm begins:
|
||||
# 1) Insert all systems with zero indegree into a queue
|
||||
# 2) Pop from the queue, add to sorted_result
|
||||
# 3) Decrement indegree for each adjacent system
|
||||
# 4) Any adjacent system that reaches zero indegree is appended to the queue
|
||||
var queue = []
|
||||
|
||||
# Adjust for wildcard_front and wildcard_back:
|
||||
# wildcard_front: s runs before everything -> point s -> other
|
||||
for w in wildcard_front:
|
||||
for other in systems:
|
||||
if other != w and not adjacency[w].has(other):
|
||||
adjacency[w].append(other)
|
||||
indegree[other] += 1
|
||||
|
||||
# wildcard_back: s runs after everything -> point other -> s
|
||||
for w in wildcard_back:
|
||||
for other in systems:
|
||||
if other != w and not adjacency[other].has(w):
|
||||
adjacency[other].append(w)
|
||||
indegree[w] += 1
|
||||
|
||||
# Collect all systems with zero indegree into the queue as our starting point
|
||||
for s in systems:
|
||||
if indegree[s] == 0:
|
||||
queue.append(s)
|
||||
|
||||
var sorted_result = []
|
||||
|
||||
# While there are systems with no remaining prerequisites
|
||||
while queue.size() > 0:
|
||||
var current = queue.pop_front()
|
||||
# Add that system to the sorted list
|
||||
sorted_result.append(current)
|
||||
# For each system that depends on 'current'
|
||||
for nxt in adjacency[current]:
|
||||
# Decrement its indegree because 'current' is now accounted for
|
||||
indegree[nxt] -= 1
|
||||
# If it has no more prerequisites, add it to the queue
|
||||
if indegree[nxt] == 0:
|
||||
queue.append(nxt)
|
||||
|
||||
# If we successfully placed all systems, overwrite the original array with sorted_result
|
||||
if sorted_result.size() == systems.size():
|
||||
systems_by_group[group] = sorted_result
|
||||
else:
|
||||
assert(
|
||||
false,
|
||||
(
|
||||
"Topological sort failed for group '%s'. Possible cycle or mismatch in dependencies."
|
||||
% group
|
||||
)
|
||||
)
|
||||
# Otherwise, we found a cycle or mismatch. Fallback to the original unsorted array
|
||||
systems_by_group[group] = systems
|
||||
|
||||
# The function modifies 'systems_by_group' in-place with a topologically sorted order
|
||||
|
||||
## Helper function to find a system instance by its type/class
|
||||
static func _find_system_by_type(systems: Array, target_type) -> System:
|
||||
for system in systems:
|
||||
# Check if the system is an instance of the target type
|
||||
if system.get_script() == target_type:
|
||||
return system
|
||||
# Also check class name matching for backward compatibility
|
||||
if system.get_script() and system.get_script().get_global_name() == str(target_type).get_file().get_basename():
|
||||
return system
|
||||
return null
|
||||
1
addons/gecs/lib/array_extensions.gd.uid
Normal file
1
addons/gecs/lib/array_extensions.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://h7vbvqjotxmf
|
||||
108
addons/gecs/lib/component_query_matcher.gd
Normal file
108
addons/gecs/lib/component_query_matcher.gd
Normal file
@@ -0,0 +1,108 @@
|
||||
## ComponentQueryMatcher
|
||||
## Static utility for matching components against query criteria.
|
||||
## Used by QueryBuilder and Relationship systems for consistent component filtering.
|
||||
##
|
||||
## Supports comparison operators (_gt, _lt, _eq), array membership (_in, _nin),
|
||||
## and custom functions for property-based filtering.
|
||||
##
|
||||
## [b]Query Operators:[/b]
|
||||
## [br]• [b]_eq:[/b] Equal [code]property == value[/code]
|
||||
## [br]• [b]_ne:[/b] Not equal [code]property != value[/code]
|
||||
## [br]• [b]_gt:[/b] Greater than [code]property > value[/code]
|
||||
## [br]• [b]_lt:[/b] Less than [code]property < value[/code]
|
||||
## [br]• [b]_gte:[/b] Greater or equal [code]property >= value[/code]
|
||||
## [br]• [b]_lte:[/b] Less or equal [code]property <= value[/code]
|
||||
## [br]• [b]_in:[/b] In array [code]property in [values][/code]
|
||||
## [br]• [b]_nin:[/b] Not in array [code]property not in [values][/code]
|
||||
## [br]• [b]func:[/b] Custom function [code]func(property) -> bool[/code]
|
||||
##
|
||||
## [codeblock]
|
||||
## var component = C_Health.new(75)
|
||||
## var query = {"health": {"_gte": 50, "_lte": 100}}
|
||||
## var matches = ComponentQueryMatcher.matches_query(component, query)
|
||||
##
|
||||
## # Custom functions
|
||||
## var func_query = {"level": {"func": func(level): return level >= 40}}
|
||||
##
|
||||
## # Array membership
|
||||
## var type_query = {"type": {"_in": ["fire", "ice"]}}
|
||||
## [/codeblock]
|
||||
class_name ComponentQueryMatcher
|
||||
extends RefCounted
|
||||
|
||||
## Checks if a component matches the given query criteria.
|
||||
## All query operators must pass for the component to match.
|
||||
##
|
||||
## [param component]: The [Component] to evaluate
|
||||
## [param query]: Dictionary mapping property names to operator dictionaries
|
||||
## [return]: [code]true[/code] if all criteria match, [code]false[/code] otherwise
|
||||
##
|
||||
## Returns [code]true[/code] for empty queries. Returns [code]false[/code] if any
|
||||
## property doesn't exist or any operator fails.
|
||||
static func matches_query(component: Component, query: Dictionary) -> bool:
|
||||
if query.is_empty():
|
||||
return true
|
||||
|
||||
for property in query:
|
||||
# Check if property exists (can't use truthiness check because 0, false, etc. are valid values)
|
||||
if not property in component:
|
||||
return false
|
||||
|
||||
var property_value = component.get(property)
|
||||
var property_query = query[property]
|
||||
|
||||
for operator in property_query:
|
||||
match operator:
|
||||
"func":
|
||||
if not property_query[operator].call(property_value):
|
||||
return false
|
||||
"_eq":
|
||||
if property_value != property_query[operator]:
|
||||
return false
|
||||
"_gt":
|
||||
if property_value <= property_query[operator]:
|
||||
return false
|
||||
"_lt":
|
||||
if property_value >= property_query[operator]:
|
||||
return false
|
||||
"_gte":
|
||||
if property_value < property_query[operator]:
|
||||
return false
|
||||
"_lte":
|
||||
if property_value > property_query[operator]:
|
||||
return false
|
||||
"_ne":
|
||||
if property_value == property_query[operator]:
|
||||
return false
|
||||
"_nin":
|
||||
if property_value in property_query[operator]:
|
||||
return false
|
||||
"_in":
|
||||
if not (property_value in property_query[operator]):
|
||||
return false
|
||||
|
||||
return true
|
||||
|
||||
## Separates component types from query dictionaries in a mixed array.
|
||||
## Used by QueryBuilder to process component lists that may contain queries.
|
||||
##
|
||||
## [param components]: Array of [Component] classes and/or query dictionaries
|
||||
## [return]: Dictionary with [code]"components"[/code] and [code]"queries"[/code] arrays
|
||||
##
|
||||
## Regular components get empty query dictionaries. Query dictionaries are
|
||||
## split into their component type and criteria.
|
||||
static func process_component_list(components: Array) -> Dictionary:
|
||||
var result := {"components": [], "queries": []}
|
||||
|
||||
for component in components:
|
||||
if component is Dictionary:
|
||||
# Handle component query case
|
||||
for component_type in component:
|
||||
result.components.append(component_type)
|
||||
result.queries.append(component[component_type])
|
||||
else:
|
||||
# Handle regular component case
|
||||
result.components.append(component)
|
||||
result.queries.append({}) # Empty query for regular components (matches all)
|
||||
|
||||
return result
|
||||
1
addons/gecs/lib/component_query_matcher.gd.uid
Normal file
1
addons/gecs/lib/component_query_matcher.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://beqw44pppbpl
|
||||
26
addons/gecs/lib/gecs_settings.gd
Normal file
26
addons/gecs/lib/gecs_settings.gd
Normal file
@@ -0,0 +1,26 @@
|
||||
class_name GecsSettings
|
||||
extends Node
|
||||
|
||||
const SETTINGS_LOG_LEVEL = "gecs/settings/log_level"
|
||||
const SETTINGS_DEBUG_MODE = "gecs/settings/debug_mode"
|
||||
|
||||
const project_settings = {
|
||||
"log_level":
|
||||
{
|
||||
"path": SETTINGS_LOG_LEVEL,
|
||||
"default_value": GECSLogger.LogLevel.ERROR,
|
||||
"type": TYPE_INT,
|
||||
"hint": PROPERTY_HINT_ENUM,
|
||||
"hint_string": "TRACE,DEBUG,INFO,WARNING,ERROR",
|
||||
"doc": "What log level GECS should log at.",
|
||||
},
|
||||
"debug_mode":
|
||||
{
|
||||
"path": SETTINGS_DEBUG_MODE,
|
||||
"default_value": false,
|
||||
"type": TYPE_BOOL,
|
||||
"hint": PROPERTY_HINT_NONE,
|
||||
"hint_string": "",
|
||||
"doc": "Enable debug mode for GECS operations. Enables editor debugger integration but impacts performance significantly.",
|
||||
}
|
||||
}
|
||||
1
addons/gecs/lib/gecs_settings.gd.uid
Normal file
1
addons/gecs/lib/gecs_settings.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://buvg6dnpqcnys
|
||||
90
addons/gecs/lib/logger.gd
Normal file
90
addons/gecs/lib/logger.gd
Normal file
@@ -0,0 +1,90 @@
|
||||
## Simplified Logger for GECS
|
||||
class_name GECSLogger
|
||||
extends RefCounted
|
||||
|
||||
const disabled := true
|
||||
|
||||
enum LogLevel {TRACE, DEBUG, INFO, WARNING, ERROR}
|
||||
|
||||
var current_level: LogLevel = ProjectSettings.get_setting(GecsSettings.SETTINGS_LOG_LEVEL, LogLevel.ERROR)
|
||||
var current_domain: String = ""
|
||||
|
||||
|
||||
func set_level(level: LogLevel):
|
||||
current_level = level
|
||||
|
||||
|
||||
func domain(domain_name: String) -> GECSLogger:
|
||||
current_domain = domain_name
|
||||
return self
|
||||
|
||||
|
||||
func log(level: LogLevel, msg = ""):
|
||||
if disabled:
|
||||
return
|
||||
var level_name: String
|
||||
if level >= current_level:
|
||||
match level:
|
||||
LogLevel.TRACE:
|
||||
level_name = "TRACE"
|
||||
LogLevel.DEBUG:
|
||||
level_name = "DEBUG"
|
||||
LogLevel.INFO:
|
||||
level_name = "INFO"
|
||||
LogLevel.WARNING:
|
||||
level_name = "WARNING"
|
||||
LogLevel.ERROR:
|
||||
level_name = "ERROR"
|
||||
_:
|
||||
level_name = "UNKNOWN"
|
||||
print("%s [%s]: %s" % [current_domain, level_name, msg])
|
||||
|
||||
|
||||
func trace(msg = "", arg1 = null, arg2 = null, arg3 = null, arg4 = null, arg5 = null):
|
||||
self.log(LogLevel.TRACE, concatenate_msg_and_args(msg, arg1, arg2, arg3, arg4, arg5))
|
||||
|
||||
|
||||
func debug(msg = "", arg1 = null, arg2 = null, arg3 = null, arg4 = null, arg5 = null):
|
||||
self.log(LogLevel.DEBUG, concatenate_msg_and_args(msg, arg1, arg2, arg3, arg4, arg5))
|
||||
|
||||
|
||||
func info(msg = "", arg1 = null, arg2 = null, arg3 = null, arg4 = null, arg5 = null):
|
||||
self.log(LogLevel.INFO, concatenate_msg_and_args(msg, arg1, arg2, arg3, arg4, arg5))
|
||||
|
||||
|
||||
func warning(msg = "", arg1 = null, arg2 = null, arg3 = null, arg4 = null, arg5 = null):
|
||||
self.log(LogLevel.WARNING, concatenate_msg_and_args(msg, arg1, arg2, arg3, arg4, arg5))
|
||||
|
||||
|
||||
func error(msg = "", arg1 = null, arg2 = null, arg3 = null, arg4 = null, arg5 = null):
|
||||
self.log(LogLevel.ERROR, concatenate_msg_and_args(msg, arg1, arg2, arg3, arg4, arg5))
|
||||
|
||||
## Concatenates all given args into one single string, in consecutive order starting with 'msg'.[br]
|
||||
## Stolen from Loggie
|
||||
static func concatenate_msg_and_args(
|
||||
msg: Variant,
|
||||
arg1: Variant = null,
|
||||
arg2: Variant = null,
|
||||
arg3: Variant = null,
|
||||
arg4: Variant = null,
|
||||
arg5: Variant = null,
|
||||
arg6: Variant = null
|
||||
) -> String:
|
||||
var final_msg = convert_to_string(msg)
|
||||
var arguments = [arg1, arg2, arg3, arg4, arg5, arg6]
|
||||
for arg in arguments:
|
||||
if arg != null:
|
||||
final_msg += (" " + convert_to_string(arg))
|
||||
return final_msg
|
||||
|
||||
## Converts [param something] into a string.[br]
|
||||
## If [param something] is a Dictionary, uses a special way to convert it into a string.[br]
|
||||
## You can add more exceptions and rules for how different things are converted to strings here.[br]
|
||||
## Stolen from Loggie
|
||||
static func convert_to_string(something: Variant) -> String:
|
||||
var result: String
|
||||
if something is Dictionary:
|
||||
result = JSON.new().stringify(something, " ", false, true)
|
||||
else:
|
||||
result = str(something)
|
||||
return result
|
||||
1
addons/gecs/lib/logger.gd.uid
Normal file
1
addons/gecs/lib/logger.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://betmoqpwcq0wc
|
||||
321
addons/gecs/lib/set.gd
Normal file
321
addons/gecs/lib/set.gd
Normal file
@@ -0,0 +1,321 @@
|
||||
## Set is Mathematical set data structure for collections of unique values.[br]
|
||||
##
|
||||
## Built on Dictionary for O(1) membership testing. Used throughout GECS for
|
||||
## entity filtering and component indexing.
|
||||
##
|
||||
## Supports standard set operations like union, intersection, and difference.
|
||||
## No inherent ordering - elements are stored by hash.
|
||||
##
|
||||
## [codeblock]
|
||||
## var numbers = Set.new([1, 2, 3, 4, 5])
|
||||
## numbers.add(6)
|
||||
## print(numbers.has(3)) # true
|
||||
##
|
||||
## var set_a = Set.new([1, 2, 3, 4])
|
||||
## var set_b = Set.new([3, 4, 5, 6])
|
||||
## var intersection = set_a.intersect(set_b) # [3, 4]
|
||||
## [/codeblock]
|
||||
class_name Set
|
||||
extends RefCounted
|
||||
|
||||
## Internal storage using Dictionary keys for O(1) average-case operations.
|
||||
## Values in the dictionary are always [code]true[/code] and ignored.
|
||||
var _data: Dictionary = {}
|
||||
|
||||
|
||||
## Initializes a new Set from Array, Dictionary keys, or another Set.
|
||||
## [param data]: Optional initial data. Duplicates are automatically removed.
|
||||
func _init(data = null) -> void:
|
||||
if data:
|
||||
if data is Array:
|
||||
# Add array elements, automatically deduplicating
|
||||
for value in data:
|
||||
_data[value] = true
|
||||
elif data is Set:
|
||||
# Copy from another set
|
||||
for value in data._data.keys():
|
||||
_data[value] = true
|
||||
elif data is Dictionary:
|
||||
# Use dictionary keys as set elements
|
||||
for key in data.keys():
|
||||
_data[key] = true
|
||||
|
||||
#region Basic Set Operations
|
||||
|
||||
|
||||
## Adds a value to the set. Has no effect if the value is already present.
|
||||
## [param value]: The value to add to the set. Can be any hashable type.
|
||||
##
|
||||
## [b]Time Complexity:[/b] O(1) average case
|
||||
## [codeblock]
|
||||
## var my_set = Set.new([1, 2, 3])
|
||||
## my_set.add(4) # Set now contains [1, 2, 3, 4]
|
||||
## my_set.add(2) # No change, 2 already exists
|
||||
## [/codeblock]
|
||||
func add(value) -> void:
|
||||
_data[value] = true
|
||||
|
||||
|
||||
## Removes a value from the set. Has no effect if the value is not present.
|
||||
## [param value]: The value to remove from the set
|
||||
##
|
||||
## [b]Time Complexity:[/b] O(1) average case
|
||||
## [codeblock]
|
||||
## var my_set = Set.new([1, 2, 3, 4])
|
||||
## my_set.erase(3) # Set now contains [1, 2, 4]
|
||||
## my_set.erase(5) # No change, 5 doesn't exist
|
||||
## [/codeblock]
|
||||
func erase(value) -> void:
|
||||
_data.erase(value)
|
||||
|
||||
|
||||
## Tests whether a value exists in the set.
|
||||
## [param value]: The value to test for membership
|
||||
## [return]: [code]true[/code] if the value exists in the set, [code]false[/code] otherwise
|
||||
##
|
||||
## [b]Time Complexity:[/b] O(1) average case
|
||||
## [codeblock]
|
||||
## var my_set = Set.new(["apple", "banana", "cherry"])
|
||||
## print(my_set.has("banana")) # true
|
||||
## print(my_set.has("grape")) # false
|
||||
## [/codeblock]
|
||||
func has(value) -> bool:
|
||||
return _data.has(value)
|
||||
|
||||
|
||||
## Removes all elements from the set, making it empty.
|
||||
## [b]Time Complexity:[/b] O(1)
|
||||
## [codeblock]
|
||||
## var my_set = Set.new([1, 2, 3, 4, 5])
|
||||
## my_set.clear()
|
||||
## print(my_set.is_empty()) # true
|
||||
## [/codeblock]
|
||||
func clear() -> void:
|
||||
_data.clear()
|
||||
|
||||
|
||||
## Returns the number of elements in the set.
|
||||
## [return]: Integer count of unique elements in the set
|
||||
##
|
||||
## [b]Time Complexity:[/b] O(1)
|
||||
## [codeblock]
|
||||
## var my_set = Set.new(["a", "b", "c", "a", "b"]) # Duplicates ignored
|
||||
## print(my_set.size()) # 3
|
||||
## [/codeblock]
|
||||
func size() -> int:
|
||||
return _data.size()
|
||||
|
||||
|
||||
## Tests whether the set contains no elements.
|
||||
## [return]: [code]true[/code] if the set is empty, [code]false[/code] otherwise
|
||||
##
|
||||
## [b]Time Complexity:[/b] O(1)
|
||||
## [codeblock]
|
||||
## var empty_set = Set.new()
|
||||
## var filled_set = Set.new([1, 2, 3])
|
||||
## print(empty_set.is_empty()) # true
|
||||
## print(filled_set.is_empty()) # false
|
||||
## [/codeblock]
|
||||
func is_empty() -> bool:
|
||||
return _data.is_empty()
|
||||
|
||||
|
||||
## Returns all elements in the set as an Array.
|
||||
## The order of elements is not guaranteed and may vary between calls.
|
||||
## [return]: Array containing all set elements
|
||||
##
|
||||
## [b]Time Complexity:[/b] O(n) where n is the number of elements
|
||||
## [codeblock]
|
||||
## var my_set = Set.new([3, 1, 4, 1, 5])
|
||||
## var elements = my_set.values() # [1, 3, 4, 5] (order may vary)
|
||||
## [/codeblock]
|
||||
func values() -> Array:
|
||||
return _data.keys()
|
||||
|
||||
#endregion
|
||||
|
||||
#region Set Algebra Operations
|
||||
|
||||
|
||||
## Returns the union of this set with another set (A ∪ B).
|
||||
## Creates a new set containing all elements that exist in either set.
|
||||
## [param other]: The other set to union with
|
||||
## [return]: New [Set] containing all elements from both sets
|
||||
##
|
||||
## [b]Time Complexity:[/b] O(|A| + |B|) where |A| and |B| are set sizes
|
||||
## [codeblock]
|
||||
## var set_a = Set.new([1, 2, 3])
|
||||
## var set_b = Set.new([3, 4, 5])
|
||||
## var union_set = set_a.union(set_b) # Contains [1, 2, 3, 4, 5]
|
||||
## [/codeblock]
|
||||
func union(other: Set) -> Set:
|
||||
var result = Set.new()
|
||||
result._data = _data.duplicate()
|
||||
for key in other._data.keys():
|
||||
result._data[key] = true
|
||||
return result
|
||||
|
||||
|
||||
## Returns the intersection of this set with another set (A ∩ B).
|
||||
## Creates a new set containing only elements that exist in both sets.
|
||||
## Automatically optimizes by iterating over the smaller set.
|
||||
## [param other]: The other set to intersect with
|
||||
## [return]: New [Set] containing elements common to both sets
|
||||
##
|
||||
## [b]Time Complexity:[/b] O(min(|A|, |B|)) - optimized for smaller set
|
||||
## [codeblock]
|
||||
## var set_a = Set.new([1, 2, 3, 4])
|
||||
## var set_b = Set.new([3, 4, 5, 6])
|
||||
## var intersection = set_a.intersect(set_b) # Contains [3, 4]
|
||||
## [/codeblock]
|
||||
func intersect(other: Set) -> Set:
|
||||
# Optimization: iterate over smaller set for better performance
|
||||
if other.size() < _data.size():
|
||||
return other.intersect(self )
|
||||
|
||||
var result = Set.new()
|
||||
for key in _data.keys():
|
||||
if other._data.has(key):
|
||||
result._data[key] = true
|
||||
return result
|
||||
|
||||
|
||||
## Returns the difference of this set minus another set (A - B).
|
||||
## Creates a new set containing elements in this set but not in the other.
|
||||
## [param other]: The set whose elements to exclude
|
||||
## [return]: New [Set] containing elements only in this set
|
||||
##
|
||||
## [b]Time Complexity:[/b] O(|A|) where |A| is the size of this set
|
||||
## [codeblock]
|
||||
## var set_a = Set.new([1, 2, 3, 4])
|
||||
## var set_b = Set.new([3, 4, 5, 6])
|
||||
## var difference = set_a.difference(set_b) # Contains [1, 2]
|
||||
## [/codeblock]
|
||||
func difference(other: Set) -> Set:
|
||||
var result = Set.new()
|
||||
for key in _data.keys():
|
||||
if not other._data.has(key):
|
||||
result._data[key] = true
|
||||
return result
|
||||
|
||||
|
||||
## Returns the symmetric difference of this set with another set (A ⊕ B).
|
||||
## Creates a new set containing elements in either set, but not in both.
|
||||
## Equivalent to (A - B) ∪ (B - A).
|
||||
## [param other]: The other set for symmetric difference
|
||||
## [return]: New [Set] containing elements in exactly one of the two sets
|
||||
##
|
||||
## [b]Time Complexity:[/b] O(|A| + |B|)
|
||||
## [codeblock]
|
||||
## var set_a = Set.new([1, 2, 3, 4])
|
||||
## var set_b = Set.new([3, 4, 5, 6])
|
||||
## var sym_diff = set_a.symmetric_difference(set_b) # Contains [1, 2, 5, 6]
|
||||
## [/codeblock]
|
||||
func symmetric_difference(other: Set) -> Set:
|
||||
var result = Set.new()
|
||||
# Add elements from this set that aren't in other
|
||||
for key in _data.keys():
|
||||
if not other._data.has(key):
|
||||
result._data[key] = true
|
||||
# Add elements from other set that aren't in this set
|
||||
for key in other._data.keys():
|
||||
if not _data.has(key):
|
||||
result._data[key] = true
|
||||
return result
|
||||
|
||||
#endregion
|
||||
|
||||
#region Set Relationship Testing
|
||||
|
||||
|
||||
## Tests whether this set is a subset of another set (A ⊆ B).
|
||||
## Returns [code]true[/code] if every element in this set also exists in the other set.
|
||||
## [param other]: The potential superset to test against
|
||||
## [return]: [code]true[/code] if this set is a subset of other, [code]false[/code] otherwise
|
||||
##
|
||||
## [b]Time Complexity:[/b] O(|A|) where |A| is the size of this set
|
||||
## [codeblock]
|
||||
## var small_set = Set.new([1, 2])
|
||||
## var large_set = Set.new([1, 2, 3, 4, 5])
|
||||
## print(small_set.is_subset(large_set)) # true
|
||||
## print(large_set.is_subset(small_set)) # false
|
||||
## [/codeblock]
|
||||
func is_subset(other: Set) -> bool:
|
||||
for key in _data.keys():
|
||||
if not other._data.has(key):
|
||||
return false
|
||||
return true
|
||||
|
||||
|
||||
## Tests whether this set is a superset of another set (A ⊇ B).
|
||||
## Returns [code]true[/code] if this set contains every element from the other set.
|
||||
## [param other]: The potential subset to test
|
||||
## [return]: [code]true[/code] if this set is a superset of other, [code]false[/code] otherwise
|
||||
##
|
||||
## [b]Time Complexity:[/b] O(|B|) where |B| is the size of the other set
|
||||
## [codeblock]
|
||||
## var large_set = Set.new([1, 2, 3, 4, 5])
|
||||
## var small_set = Set.new([2, 4])
|
||||
## print(large_set.is_superset(small_set)) # true
|
||||
## [/codeblock]
|
||||
func is_superset(other: Set) -> bool:
|
||||
return other.is_subset(self )
|
||||
|
||||
|
||||
## Tests whether this set contains exactly the same elements as another set (A = B).
|
||||
## Two sets are equal if they have the same size and this set is a subset of the other.
|
||||
## [param other]: The set to compare for equality
|
||||
## [return]: [code]true[/code] if sets contain identical elements, [code]false[/code] otherwise
|
||||
##
|
||||
## [b]Time Complexity:[/b] O(min(|A|, |B|)) - fails fast on size mismatch
|
||||
## [codeblock]
|
||||
## var set_a = Set.new([1, 2, 3])
|
||||
## var set_b = Set.new([3, 1, 2]) # Order doesn't matter
|
||||
## var set_c = Set.new([1, 2, 3, 4])
|
||||
## print(set_a.is_equal(set_b)) # true
|
||||
## print(set_a.is_equal(set_c)) # false
|
||||
## [/codeblock]
|
||||
func is_equal(other) -> bool:
|
||||
# Quick size check for early exit
|
||||
if _data.size() != other._data.size():
|
||||
return false
|
||||
return self.is_subset(other)
|
||||
|
||||
#endregion
|
||||
|
||||
#region Utility Methods
|
||||
|
||||
|
||||
## Creates a shallow copy of this set.
|
||||
## The returned set is independent - modifications to either set won't affect the other.
|
||||
## However, if the set contains reference types, the references are shared.
|
||||
## [return]: New [Set] containing the same elements
|
||||
##
|
||||
## [b]Time Complexity:[/b] O(n) where n is the number of elements
|
||||
## [codeblock]
|
||||
## var original = Set.new([1, 2, 3])
|
||||
## var copy = original.duplicate()
|
||||
## copy.add(4) # Only affects copy
|
||||
## print(original.size()) # 3
|
||||
## print(copy.size()) # 4
|
||||
## [/codeblock]
|
||||
func duplicate() -> Set:
|
||||
var result = Set.new()
|
||||
result._data = _data.duplicate()
|
||||
return result
|
||||
|
||||
|
||||
## Converts the set to an Array containing all elements.
|
||||
## This is an alias for [method values] provided for API consistency.
|
||||
## The order of elements is not guaranteed.
|
||||
## [return]: Array containing all set elements
|
||||
##
|
||||
## [b]Time Complexity:[/b] O(n) where n is the number of elements
|
||||
## [codeblock]
|
||||
## var my_set = Set.new(["x", "y", "z"])
|
||||
## var array = my_set.to_array() # ["x", "y", "z"] (order may vary)
|
||||
## [/codeblock]
|
||||
func to_array() -> Array:
|
||||
return _data.keys()
|
||||
|
||||
#endregion
|
||||
1
addons/gecs/lib/set.gd.uid
Normal file
1
addons/gecs/lib/set.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://oqdcekkxyt52
|
||||
34
addons/gecs/lib/system_group.gd
Normal file
34
addons/gecs/lib/system_group.gd
Normal file
@@ -0,0 +1,34 @@
|
||||
## This is a node that automatically fills in the [member System.group] property
|
||||
## of any [System] that is a child of this node.
|
||||
## Allowing you to visually organize your systems in the scene tree
|
||||
## without having to manually set the group property on each [System].
|
||||
## Add this node to your scene tree and make [System]s children of it.
|
||||
## The name of the SystemGroup node will be set to [member System.group] for
|
||||
## all child [System]s.
|
||||
@tool
|
||||
@icon('res://addons/gecs/assets/system_folder.svg')
|
||||
class_name SystemGroup
|
||||
extends Node
|
||||
|
||||
## Put the [System]s in the group based on the [member Node.name] of the [SystemGroup]
|
||||
@export var auto_group := true
|
||||
|
||||
|
||||
## called when the node enters the scene tree for the first time.
|
||||
func _enter_tree() -> void:
|
||||
# Connect signals
|
||||
if not child_entered_tree.is_connected(_on_child_entered_tree):
|
||||
child_entered_tree.connect(_on_child_entered_tree)
|
||||
|
||||
# Set the group for all child systems
|
||||
if auto_group:
|
||||
for child in get_children():
|
||||
if child is System:
|
||||
child.group = name
|
||||
|
||||
|
||||
## Anytime a child enters the tree, set its group if it's a System
|
||||
func _on_child_entered_tree(node: Node) -> void:
|
||||
if auto_group:
|
||||
if node is System:
|
||||
node.group = name
|
||||
1
addons/gecs/lib/system_group.gd.uid
Normal file
1
addons/gecs/lib/system_group.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://b3vi2ingux88g
|
||||
Reference in New Issue
Block a user