basic ECS spawner
This commit is contained in:
184
addons/gecs/ecs/query_cache_key.gd
Normal file
184
addons/gecs/ecs/query_cache_key.gd
Normal file
@@ -0,0 +1,184 @@
|
||||
## QueryCacheKey
|
||||
## ------------------------------------------------------------------------------
|
||||
## PURPOSE
|
||||
## Build a structural query signature (cache key) that is:
|
||||
## * Order-insensitive inside each domain (with_all / with_any / with_none)
|
||||
## * Order-sensitive ACROSS domains (the same component in different domains => different key)
|
||||
## * Extremely fast (single allocation + contiguous integer writes)
|
||||
## * Stable for the lifetime of loaded component scripts (uses script.instance_id)
|
||||
##
|
||||
## WHY NOT JUST MERGE & SORT?
|
||||
## A naive approach merges all component IDs + domain markers then sorts. That destroys
|
||||
## domain boundaries and lets these collide:
|
||||
## with_all([A,B]) vs with_any([A,B])
|
||||
## After a full sort both become the same multiset {1,2,3,A,B}. We prevent that by
|
||||
## emitting DOMAIN MARKER then COUNT then the sorted IDs for that domain – preserving
|
||||
## domain structure while still being permutation-insensitive within the domain.
|
||||
##
|
||||
## LAYOUT (integers in final array):
|
||||
## [ 1, |count_all|, sorted(all_ids)...,
|
||||
## 2, |count_any|, sorted(any_ids)...,
|
||||
## 3, |count_none|, sorted(ex_ids)... ]
|
||||
## 1/2/3 : domain sentinels (ALL / ANY / NONE)
|
||||
## count_* : disambiguates empty vs non-empty ( [] vs [X] ) and prevents boundary ambiguity
|
||||
## sorted(ids) : order-insensitivity; identical sets different order => same run of ints
|
||||
##
|
||||
## COMPLEXITY
|
||||
## Sorting dominates: O(a log a + y log y + n log n). Typical domain sizes are tiny.
|
||||
## Allocation: exactly one integer array sized to final layout.
|
||||
## Hash: Godot's native Array.hash() (64-bit) – very fast.
|
||||
##
|
||||
## COLLISION PROFILE
|
||||
## 64-bit space (~1.84e19). Even 1,000,000 distinct structural queries => ~2.7e-8 collision probability.
|
||||
## Practically zero for real ECS usage. See PERFORMANCE_CACHE_KEY_NOTE.md for math.
|
||||
##
|
||||
## EXTENSION POINTS
|
||||
## * Add a leading VERSION marker if the format evolves.
|
||||
## * Add extra domains (e.g. relationship structure) by appending new marker + count + IDs.
|
||||
## * Add enabled-state separation by injecting a synthetic domain marker (kept separate currently).
|
||||
##
|
||||
## INLINE COMMENT LEGEND
|
||||
## all_ids / any_ids / ex_ids : per-domain sorted component script instance IDs
|
||||
## total : exact integer count used for one-shot allocation (prevents incremental reallocation)
|
||||
## layout[i] = marker/count/id : sequential write building final signature array
|
||||
##
|
||||
class_name QueryCacheKey
|
||||
extends RefCounted
|
||||
|
||||
static func build(
|
||||
all_components: Array,
|
||||
any_components: Array,
|
||||
exclude_components: Array,
|
||||
relationships: Array = [],
|
||||
exclude_relationships: Array = [],
|
||||
groups: Array = [],
|
||||
exclude_groups: Array = []
|
||||
) -> int:
|
||||
# Collect & sort per-domain IDs (order-insensitive inside each domain)
|
||||
var all_ids: Array[int] = []
|
||||
for c in all_components: all_ids.append(c.get_instance_id())
|
||||
all_ids.sort()
|
||||
var any_ids: Array[int] = []
|
||||
for c in any_components: any_ids.append(c.get_instance_id())
|
||||
any_ids.sort()
|
||||
var ex_ids: Array[int] = []
|
||||
for c in exclude_components: ex_ids.append(c.get_instance_id())
|
||||
ex_ids.sort()
|
||||
|
||||
# Collect & sort relationship IDs
|
||||
var rel_ids: Array[int] = []
|
||||
for rel in relationships:
|
||||
# Use Script instance ID for type matching (consistent with component queries)
|
||||
# Relationship.new(C_TestB.new()) creates component instance, we want the Script's ID
|
||||
if rel.relation:
|
||||
rel_ids.append(rel.relation.get_script().get_instance_id())
|
||||
else:
|
||||
rel_ids.append(0)
|
||||
|
||||
# Handle target - use Script instance ID for Components (type matching)
|
||||
if rel.target is Component:
|
||||
# Component target: use Script instance ID for type matching
|
||||
rel_ids.append(rel.target.get_script().get_instance_id())
|
||||
elif rel.target is Entity:
|
||||
# Entity target: use entity instance ID (entities are specific instances)
|
||||
rel_ids.append(rel.target.get_instance_id())
|
||||
elif rel.target is Script:
|
||||
# Archetype target: use Script instance ID
|
||||
rel_ids.append(rel.target.get_instance_id())
|
||||
elif rel.target != null:
|
||||
# Other types: use generic hash
|
||||
rel_ids.append(rel.target.hash())
|
||||
else:
|
||||
rel_ids.append(0) # null target
|
||||
rel_ids.sort()
|
||||
|
||||
var ex_rel_ids: Array[int] = []
|
||||
for rel in exclude_relationships:
|
||||
# Use Script instance ID for type matching (consistent with component queries)
|
||||
if rel.relation:
|
||||
ex_rel_ids.append(rel.relation.get_script().get_instance_id())
|
||||
else:
|
||||
ex_rel_ids.append(0)
|
||||
|
||||
# Handle target - use Script instance ID for Components (type matching)
|
||||
if rel.target is Component:
|
||||
ex_rel_ids.append(rel.target.get_script().get_instance_id())
|
||||
elif rel.target is Entity:
|
||||
ex_rel_ids.append(rel.target.get_instance_id())
|
||||
elif rel.target is Script:
|
||||
ex_rel_ids.append(rel.target.get_instance_id())
|
||||
elif rel.target != null:
|
||||
ex_rel_ids.append(rel.target.hash())
|
||||
else:
|
||||
ex_rel_ids.append(0)
|
||||
ex_rel_ids.sort()
|
||||
|
||||
# Collect & sort group name hashes
|
||||
var group_ids: Array[int] = []
|
||||
for group_name in groups:
|
||||
group_ids.append(group_name.hash())
|
||||
group_ids.sort()
|
||||
|
||||
var ex_group_ids: Array[int] = []
|
||||
for group_name in exclude_groups:
|
||||
ex_group_ids.append(group_name.hash())
|
||||
ex_group_ids.sort()
|
||||
|
||||
# Compute exact total length: (marker + count) per domain + IDs
|
||||
var total = 1 + 1 + all_ids.size() # ALL marker + count + ids
|
||||
total += 1 + 1 + any_ids.size() # ANY marker + count + ids
|
||||
total += 1 + 1 + ex_ids.size() # NONE marker + count + ids
|
||||
total += 1 + 1 + rel_ids.size() # RELATIONSHIPS marker + count + ids
|
||||
total += 1 + 1 + ex_rel_ids.size() # EXCLUDE_RELATIONSHIPS marker + count + ids
|
||||
total += 1 + 1 + group_ids.size() # GROUPS marker + count + ids
|
||||
total += 1 + 1 + ex_group_ids.size() # EXCLUDE_GROUPS marker + count + ids
|
||||
|
||||
# Single allocation for final signature layout
|
||||
var layout: Array[int] = []
|
||||
layout.resize(total)
|
||||
|
||||
var i := 0
|
||||
# --- Domain: ALL ---
|
||||
layout[i] = 1; i += 1 # Marker for ALL domain
|
||||
layout[i] = all_ids.size(); i += 1 # Count (disambiguates empty vs non-empty)
|
||||
for id in all_ids:
|
||||
layout[i] = id; i += 1 # Sorted ALL component IDs
|
||||
|
||||
# --- Domain: ANY ---
|
||||
layout[i] = 2; i += 1 # Marker for ANY domain
|
||||
layout[i] = any_ids.size(); i += 1 # Count
|
||||
for id in any_ids:
|
||||
layout[i] = id; i += 1 # Sorted ANY component IDs
|
||||
|
||||
# --- Domain: NONE (exclude) ---
|
||||
layout[i] = 3; i += 1 # Marker for NONE domain
|
||||
layout[i] = ex_ids.size(); i += 1 # Count
|
||||
for id in ex_ids:
|
||||
layout[i] = id; i += 1 # Sorted EXCLUDE component IDs
|
||||
|
||||
# --- Domain: RELATIONSHIPS ---
|
||||
layout[i] = 4; i += 1 # Marker for RELATIONSHIPS domain
|
||||
layout[i] = rel_ids.size(); i += 1 # Count
|
||||
for id in rel_ids:
|
||||
layout[i] = id; i += 1 # Sorted relationship IDs
|
||||
|
||||
# --- Domain: EXCLUDE_RELATIONSHIPS ---
|
||||
layout[i] = 5; i += 1 # Marker for EXCLUDE_RELATIONSHIPS domain
|
||||
layout[i] = ex_rel_ids.size(); i += 1 # Count
|
||||
for id in ex_rel_ids:
|
||||
layout[i] = id; i += 1 # Sorted exclude relationship IDs
|
||||
|
||||
# --- Domain: GROUPS ---
|
||||
layout[i] = 6; i += 1 # Marker for GROUPS domain
|
||||
layout[i] = group_ids.size(); i += 1 # Count
|
||||
for id in group_ids:
|
||||
layout[i] = id; i += 1 # Sorted group name hashes
|
||||
|
||||
# --- Domain: EXCLUDE_GROUPS ---
|
||||
layout[i] = 7; i += 1 # Marker for EXCLUDE_GROUPS domain
|
||||
layout[i] = ex_group_ids.size(); i += 1 # Count
|
||||
for id in ex_group_ids:
|
||||
layout[i] = id; i += 1 # Sorted exclude group name hashes
|
||||
|
||||
# Hash the structural layout -> 64-bit key
|
||||
return layout.hash()
|
||||
Reference in New Issue
Block a user