basic ECS spawner
This commit is contained in:
9
addons/gecs/io/gecs_data.gd
Normal file
9
addons/gecs/io/gecs_data.gd
Normal file
@@ -0,0 +1,9 @@
|
||||
class_name GecsData
|
||||
extends Resource
|
||||
|
||||
@export var version: String = "0.2"
|
||||
@export var entities: Array[GecsEntityData] = []
|
||||
|
||||
|
||||
func _init(_entities: Array[GecsEntityData] = []):
|
||||
entities = _entities
|
||||
1
addons/gecs/io/gecs_data.gd.uid
Normal file
1
addons/gecs/io/gecs_data.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://pagmg5srhrnd
|
||||
18
addons/gecs/io/gecs_entity_data.gd
Normal file
18
addons/gecs/io/gecs_entity_data.gd
Normal file
@@ -0,0 +1,18 @@
|
||||
class_name GecsEntityData
|
||||
extends Resource
|
||||
|
||||
@export var entity_name: String = ""
|
||||
@export var scene_path: String = ""
|
||||
@export var components: Array[Component] = []
|
||||
@export var relationships: Array[GecsRelationshipData] = []
|
||||
@export var auto_included: bool = false
|
||||
@export var id: String = ""
|
||||
|
||||
|
||||
func _init(_name: String = "", _scene_path: String = "", _components: Array[Component] = [], _relationships: Array[GecsRelationshipData] = [], _auto_included: bool = false, _id: String = ""):
|
||||
entity_name = _name
|
||||
scene_path = _scene_path
|
||||
components = _components
|
||||
relationships = _relationships
|
||||
auto_included = _auto_included
|
||||
id = _id
|
||||
1
addons/gecs/io/gecs_entity_data.gd.uid
Normal file
1
addons/gecs/io/gecs_entity_data.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://cphey3uadg1ai
|
||||
95
addons/gecs/io/gecs_relationship_data.gd
Normal file
95
addons/gecs/io/gecs_relationship_data.gd
Normal file
@@ -0,0 +1,95 @@
|
||||
## GecsRelationshipData
|
||||
## Resource class for serializing relationship data in GECS
|
||||
##
|
||||
## This class stores all the necessary information to recreate a [Relationship]
|
||||
## during deserialization, including the relation component and target information.
|
||||
class_name GecsRelationshipData
|
||||
extends Resource
|
||||
|
||||
## The relation component data (duplicated for serialization)
|
||||
@export var relation_data: Component
|
||||
|
||||
## The type of target this relationship points to
|
||||
## Valid values: "Entity", "Component", "Script"
|
||||
@export var target_type: String = ""
|
||||
|
||||
## The id of the target entity (used when target_type is "Entity")
|
||||
@export var target_entity_id: String = ""
|
||||
|
||||
## The target component data (used when target_type is "Component")
|
||||
@export var target_component_data: Component
|
||||
|
||||
## The resource path of the target script (used when target_type is "Script")
|
||||
@export var target_script_path: String = ""
|
||||
|
||||
|
||||
## Constructor to create relationship data from a Relationship instance
|
||||
func _init(
|
||||
_relation_data: Component = null,
|
||||
_target_type: String = "",
|
||||
_target_entity_id: String = "",
|
||||
_target_component_data: Component = null,
|
||||
_target_script_path: String = ""
|
||||
):
|
||||
relation_data = _relation_data
|
||||
target_type = _target_type
|
||||
target_entity_id = _target_entity_id
|
||||
target_component_data = _target_component_data
|
||||
target_script_path = _target_script_path
|
||||
|
||||
## Creates GecsRelationshipData from a Relationship instance
|
||||
static func from_relationship(relationship: Relationship) -> GecsRelationshipData:
|
||||
var data = GecsRelationshipData.new()
|
||||
|
||||
# Store relation component (duplicate to avoid reference issues)
|
||||
if relationship.relation:
|
||||
data.relation_data = relationship.relation.duplicate(true)
|
||||
|
||||
# Determine target type and store appropriate data
|
||||
if relationship.target == null:
|
||||
data.target_type = "null"
|
||||
elif relationship.target is Entity:
|
||||
data.target_type = "Entity"
|
||||
data.target_entity_id = relationship.target.id
|
||||
elif relationship.target is Component:
|
||||
data.target_type = "Component"
|
||||
data.target_component_data = relationship.target.duplicate(true)
|
||||
elif relationship.target is Script:
|
||||
data.target_type = "Script"
|
||||
data.target_script_path = relationship.target.resource_path
|
||||
else:
|
||||
push_warning("GecsRelationshipData: Unknown target type: " + str(type_string(typeof(relationship.target))))
|
||||
data.target_type = "unknown"
|
||||
|
||||
return data
|
||||
|
||||
|
||||
## Recreates a Relationship from this data (requires entity mapping for Entity targets)
|
||||
func to_relationship(entity_mapping: Dictionary = {}) -> Relationship:
|
||||
var relationship = Relationship.new()
|
||||
|
||||
# Restore relation component
|
||||
if relation_data:
|
||||
relationship.relation = relation_data.duplicate(true)
|
||||
|
||||
# Restore target based on type
|
||||
match target_type:
|
||||
"null":
|
||||
relationship.target = null
|
||||
"Entity":
|
||||
if target_entity_id in entity_mapping:
|
||||
relationship.target = entity_mapping[target_entity_id]
|
||||
else:
|
||||
push_warning("GecsRelationshipData: Could not resolve entity with ID: " + target_entity_id)
|
||||
return null
|
||||
"Component":
|
||||
if target_component_data:
|
||||
relationship.target = target_component_data.duplicate(true)
|
||||
"Script":
|
||||
if target_script_path != "":
|
||||
relationship.target = load(target_script_path)
|
||||
_:
|
||||
push_warning("GecsRelationshipData: Unknown target type during deserialization: " + target_type)
|
||||
return null
|
||||
|
||||
return relationship
|
||||
1
addons/gecs/io/gecs_relationship_data.gd.uid
Normal file
1
addons/gecs/io/gecs_relationship_data.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://bbqc1v8555562
|
||||
219
addons/gecs/io/io.gd
Normal file
219
addons/gecs/io/io.gd
Normal file
@@ -0,0 +1,219 @@
|
||||
## GECS IO Utility Class[br]
|
||||
##
|
||||
## Provides functions for generating UUIDs, serializing/deserializing [Entity]s to/from [GecsData],
|
||||
## and saving/loading [GecsData] to/from files.
|
||||
class_name GECSIO
|
||||
|
||||
## Generates a custom GUID using random bytes.[br]
|
||||
## The format uses 16 random bytes encoded to hex and formatted with hyphens.
|
||||
static func uuid() -> String:
|
||||
const BYTE_MASK: int = 0b11111111
|
||||
# 16 random bytes with the bytes on index 6 and 8 modified
|
||||
var b = [
|
||||
randi() & BYTE_MASK, randi() & BYTE_MASK, randi() & BYTE_MASK, randi() & BYTE_MASK,
|
||||
randi() & BYTE_MASK, randi() & BYTE_MASK, ((randi() & BYTE_MASK) & 0x0f) | 0x40, randi() & BYTE_MASK,
|
||||
((randi() & BYTE_MASK) & 0x3f) | 0x80, randi() & BYTE_MASK, randi() & BYTE_MASK, randi() & BYTE_MASK,
|
||||
randi() & BYTE_MASK, randi() & BYTE_MASK, randi() & BYTE_MASK, randi() & BYTE_MASK,
|
||||
]
|
||||
|
||||
return '%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-%02x%02x%02x%02x%02x%02x' % [
|
||||
# low
|
||||
b[0], b[1], b[2], b[3],
|
||||
|
||||
# mid
|
||||
b[4], b[5],
|
||||
|
||||
# hi
|
||||
b[6], b[7],
|
||||
|
||||
# clock
|
||||
b[8], b[9],
|
||||
|
||||
# clock
|
||||
b[10], b[11], b[12], b[13], b[14], b[15]
|
||||
]
|
||||
|
||||
## Serialize a [QueryBuilder] of [Entity](s) to [GecsData] format.[br]
|
||||
## Optionally takes a [GECSSerializeConfig] to customize what gets serialized.
|
||||
static func serialize(query: QueryBuilder, config: GECSSerializeConfig = null) -> GecsData:
|
||||
return serialize_entities(query.execute() as Array[Entity], config)
|
||||
|
||||
## Serialize a list of [Entity](s) to [GecsData] format.[br]
|
||||
## Optionally takes a [GECSSerializeConfig] to customize what gets serialized.
|
||||
static func serialize_entities(entities: Array, config: GECSSerializeConfig = null) -> GecsData:
|
||||
# Pass 1: Serialize entities from original query
|
||||
var entity_data_array: Array[GecsEntityData] = []
|
||||
var processed_entities: Dictionary = {} # id -> bool
|
||||
var entity_id_mapping: Dictionary = {} # id -> Entity
|
||||
for entity in entities:
|
||||
var effective_config = _resolve_config(entity, config)
|
||||
var entity_data = _serialize_entity(entity, false, effective_config)
|
||||
entity_data_array.append(entity_data)
|
||||
processed_entities[entity.id] = true
|
||||
entity_id_mapping[entity.id] = entity
|
||||
|
||||
# Pass 2: Scan relationships and auto-include referenced entities (if enabled)
|
||||
var entities_to_check = entities.duplicate()
|
||||
var check_index = 0
|
||||
|
||||
while check_index < entities_to_check.size():
|
||||
var entity = entities_to_check[check_index]
|
||||
var effective_config = _resolve_config(entity, config)
|
||||
|
||||
# Only proceed if config allows including related entities
|
||||
if effective_config.include_related_entities:
|
||||
# Check all relationships of this entity
|
||||
for relationship in entity.relationships:
|
||||
if relationship.target is Entity:
|
||||
var target_entity = relationship.target as Entity
|
||||
var target_id = target_entity.id
|
||||
|
||||
# If this entity hasn't been processed yet, auto-include it
|
||||
if not processed_entities.has(target_id):
|
||||
var target_config = _resolve_config(target_entity, config)
|
||||
var auto_entity_data = _serialize_entity(target_entity, true, target_config)
|
||||
entity_data_array.append(auto_entity_data)
|
||||
processed_entities[target_id] = true
|
||||
entity_id_mapping[target_id] = target_entity
|
||||
|
||||
# Add to list for further relationship checking
|
||||
entities_to_check.append(target_entity)
|
||||
|
||||
check_index += 1
|
||||
|
||||
return GecsData.new(entity_data_array)
|
||||
|
||||
## Save [GecsData] to a file at the specified path.[br]
|
||||
## If binary is true, saves in binary format (.res), otherwise text format (.tres).
|
||||
static func save(gecs_data: GecsData, filepath: String, binary: bool = false) -> bool:
|
||||
var final_path = filepath
|
||||
var flags = 0
|
||||
|
||||
if binary:
|
||||
# Convert .tres to .res for binary format
|
||||
final_path = filepath.replace(".tres", ".res")
|
||||
flags = ResourceSaver.FLAG_COMPRESS # Binary format uses no flags, .res extension determines format
|
||||
# else: text format (default flags = 0)
|
||||
|
||||
var result = ResourceSaver.save(gecs_data, final_path, flags)
|
||||
if result != OK:
|
||||
push_error("GECS save: Failed to save resource to: " + final_path)
|
||||
return false
|
||||
return true
|
||||
|
||||
## Load and deserialize [Entity](s) from a file at the specified path.[br]
|
||||
## Supports both binary (.res) and text (.tres) formats, tries binary first.
|
||||
static func deserialize(gecs_filepath: String) -> Array[Entity]:
|
||||
# Try binary first (.res), then text (.tres)
|
||||
var binary_path = gecs_filepath.replace(".tres", ".res")
|
||||
|
||||
if ResourceLoader.exists(binary_path):
|
||||
return _load_from_path(binary_path)
|
||||
elif ResourceLoader.exists(gecs_filepath):
|
||||
return _load_from_path(gecs_filepath)
|
||||
else:
|
||||
push_error("GECS deserialize: File not found: " + gecs_filepath)
|
||||
return []
|
||||
|
||||
## Deserialize [GecsData] into a list of [Entity](s).[br]
|
||||
## This can be used so you can serialize entities to GECS Data and then Deserailize that [GecsSData] later
|
||||
static func deserialize_gecs_data(gecs_data: GecsData) -> Array[Entity]:
|
||||
var entities: Array[Entity] = []
|
||||
var id_to_entity: Dictionary = {} # id -> Entity
|
||||
|
||||
# Pass 1: Create all entities and build ID mapping
|
||||
for entity_data in gecs_data.entities:
|
||||
var entity = _deserialize_entity(entity_data)
|
||||
entities.append(entity)
|
||||
id_to_entity[entity.id] = entity
|
||||
|
||||
# Pass 2: Restore relationships using ID mapping
|
||||
for i in entities.size():
|
||||
var entity = entities[i]
|
||||
var entity_data = gecs_data.entities[i]
|
||||
|
||||
# Restore relationships
|
||||
for rel_data in entity_data.relationships:
|
||||
var relationship = rel_data.to_relationship(id_to_entity)
|
||||
if relationship != null:
|
||||
entity.add_relationship(relationship)
|
||||
# Note: Invalid relationships are skipped with warning logged in to_relationship()
|
||||
|
||||
return entities
|
||||
|
||||
## Helper function to resolve the effective configuration for an entity
|
||||
## Priority: provided_config > entity.serialize_config > world.default_serialize_config > fallback
|
||||
static func _resolve_config(entity: Entity, provided_config: GECSSerializeConfig) -> GECSSerializeConfig:
|
||||
if provided_config != null:
|
||||
return provided_config
|
||||
return entity.get_effective_serialize_config()
|
||||
|
||||
## Helper function to serialize a single entity with its components and relationships
|
||||
static func _serialize_entity(entity: Entity, auto_included: bool, config: GECSSerializeConfig) -> GecsEntityData:
|
||||
# Serialize components (filtered by config)
|
||||
var components: Array[Component] = []
|
||||
for component in entity.components.values():
|
||||
if config.should_include_component(component):
|
||||
# Duplicate the component to avoid modifying the original
|
||||
components.append(component.duplicate(true))
|
||||
|
||||
# Serialize relationships (if enabled by config)
|
||||
var relationships: Array[GecsRelationshipData] = []
|
||||
if config.include_relationships:
|
||||
for relationship in entity.relationships:
|
||||
var rel_data = GecsRelationshipData.from_relationship(relationship)
|
||||
relationships.append(rel_data)
|
||||
|
||||
return GecsEntityData.new(
|
||||
entity.name,
|
||||
entity.scene_file_path if entity.scene_file_path != "" else "",
|
||||
components,
|
||||
relationships,
|
||||
auto_included,
|
||||
entity.id
|
||||
)
|
||||
|
||||
## Helper function to load and deserialize entities from a given file path
|
||||
static func _load_from_path(file_path: String) -> Array[Entity]:
|
||||
print("GECS _load_from_path: Loading file: ", file_path)
|
||||
var gecs_data = load(file_path) as GecsData
|
||||
if not gecs_data:
|
||||
push_error("GECS deserialize: Could not load GecsData resource: " + file_path)
|
||||
return []
|
||||
|
||||
print("GECS _load_from_path: Loaded GecsData with ", gecs_data.entities.size(), " entities")
|
||||
|
||||
return deserialize_gecs_data(gecs_data)
|
||||
|
||||
## Helper function to deserialize a single entity with its components and uuid
|
||||
static func _deserialize_entity(entity_data: GecsEntityData) -> Entity:
|
||||
var entity: Entity
|
||||
|
||||
# Check if this entity is a prefab (has scene file)
|
||||
if entity_data.scene_path != "":
|
||||
var scene_path = entity_data.scene_path
|
||||
if ResourceLoader.exists(scene_path):
|
||||
var packed_scene = load(scene_path) as PackedScene
|
||||
if packed_scene:
|
||||
entity = packed_scene.instantiate() as Entity
|
||||
else:
|
||||
push_warning("GECS deserialize: Could not load scene: " + scene_path + ", creating new entity")
|
||||
entity = Entity.new()
|
||||
else:
|
||||
push_warning("GECS deserialize: Scene file not found: " + scene_path + ", creating new entity")
|
||||
entity = Entity.new()
|
||||
else:
|
||||
# Create new entity
|
||||
entity = Entity.new()
|
||||
|
||||
# Set entity name
|
||||
entity.name = entity_data.entity_name
|
||||
|
||||
# Restore id (important: set this directly)
|
||||
entity.id = entity_data.id
|
||||
|
||||
# Add components (they're already properly typed as Component resources)
|
||||
for component in entity_data.components:
|
||||
entity.add_component(component.duplicate(true))
|
||||
|
||||
return entity
|
||||
1
addons/gecs/io/io.gd.uid
Normal file
1
addons/gecs/io/io.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://drhirabcyqlvk
|
||||
33
addons/gecs/io/serialize_config.gd
Normal file
33
addons/gecs/io/serialize_config.gd
Normal file
@@ -0,0 +1,33 @@
|
||||
## This config defines what to include when serializing
|
||||
## It can be appled to the world as a whole or to specific entities
|
||||
## This way you can define project level defaults and override them for specific cases
|
||||
class_name GECSSerializeConfig
|
||||
extends Resource
|
||||
|
||||
## Include all components (true) or only specific components (false)
|
||||
@export var include_all_components: bool = true
|
||||
## Which component types to include in serialization (only used when include_all_components = false)
|
||||
@export var components: Array = []
|
||||
## Whether to include relationships in serialization
|
||||
@export var include_relationships: bool = true
|
||||
## Whether to include related entities in serialization (Related entities are entities referenced by relationships from the serialized entities)
|
||||
@export var include_related_entities: bool = true
|
||||
|
||||
|
||||
## Helper method to determine if a component should be included in serialization
|
||||
func should_include_component(component: Component) -> bool:
|
||||
var comp_type = component.get_script()
|
||||
return include_all_components or components.any(func(type): return comp_type == type)
|
||||
|
||||
|
||||
## Merge this config with another config, with the other config taking priority
|
||||
func merge_with(other: GECSSerializeConfig) -> GECSSerializeConfig:
|
||||
if other == null:
|
||||
return self
|
||||
|
||||
var merged = GECSSerializeConfig.new()
|
||||
merged.include_all_components = other.include_all_components
|
||||
merged.components = other.components.duplicate()
|
||||
merged.include_relationships = other.include_relationships
|
||||
merged.include_related_entities = other.include_related_entities
|
||||
return merged
|
||||
1
addons/gecs/io/serialize_config.gd.uid
Normal file
1
addons/gecs/io/serialize_config.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://cf84mkp0nv2mk
|
||||
Reference in New Issue
Block a user