├── addons └── gdscript-ecs │ ├── EcsComponentBase.gd │ ├── EcsComponentScanner.gd │ ├── EcsEntity.gd │ ├── EcsRegistrarBase.gd │ ├── EcsRegistrarScanner.gd │ ├── EcsSystemBase.gd │ ├── EcsSystemScanner.gd │ ├── EcsWorld.gd │ ├── README-zh.md │ ├── README.md │ ├── _EcsGDScriptScanner.gd │ ├── plugin.cfg │ └── plugin.gd └── gdscript_ecs_test ├── ecs ├── components │ ├── Component_Mob.gd │ ├── Component_MovableNode.gd │ ├── Component_Player.gd │ └── Component_Position.gd └── systems │ ├── System_Movement.gd │ └── subsystem │ ├── System_MobAI.gd │ ├── System_MobSpawner.gd │ └── System_PlayerController.gd ├── icon.svg ├── icon.svg.import ├── main.gd ├── main.tscn └── node ├── Mob.gd ├── Mob.gdshader ├── Mob.tscn ├── Player.gd ├── Player.gdshader └── Player.tscn /addons/gdscript-ecs/EcsComponentBase.gd: -------------------------------------------------------------------------------- 1 | ## Component 2 | class_name EcsComponentBase extends RefCounted 3 | 4 | ## Constant name of component name 5 | ## 6 | ## Used in EcsComponentScanner.gd 7 | const COMPONENT_NAME_CONSTANT_NAME: StringName = "NAME" 8 | 9 | ## The ID of the entity to which the component 10 | var _entity_id: int = -1 11 | 12 | ## [override] 13 | func is_hide_all_properties() -> bool: 14 | return false 15 | 16 | ## [override] 17 | func get_property_name_list_of_hidden() -> PackedStringArray: 18 | return [] 19 | 20 | ## [override] 21 | func get_property_name_list() -> PackedStringArray: 22 | if is_hide_all_properties(): 23 | return [] 24 | var property_name_list_of_hidden := get_property_name_list_of_hidden() 25 | var property_name_list: PackedStringArray = [] 26 | var property_list = get_property_list() 27 | for property in property_list: 28 | if property_name_list_of_hidden.has(property.name): 29 | continue 30 | if (property.usage & PROPERTY_USAGE_SCRIPT_VARIABLE) != 0: 31 | property_name_list.append(property.name) 32 | return property_name_list 33 | 34 | ## [override] 35 | func get_property_value(property_name: StringName) -> Variant: 36 | return get(property_name) 37 | 38 | ## [override] 39 | func set_property_value(property_name: StringName, property_value: Variant) -> void: 40 | set(property_name, property_value) 41 | 42 | -------------------------------------------------------------------------------- /addons/gdscript-ecs/EcsComponentScanner.gd: -------------------------------------------------------------------------------- 1 | class_name EcsComponentScanner extends _EcsGDScriptScanner 2 | 3 | var _component_base_script: Script 4 | var _component_name_constant: StringName 5 | var _component_script_dict: Dictionary = {} 6 | 7 | func _init(component_base_script: Script = null, component_name_constant: StringName = EcsComponentBase.COMPONENT_NAME_CONSTANT_NAME) -> void: 8 | _component_base_script = EcsComponentBase if component_base_script == null else component_base_script 9 | _component_name_constant = component_name_constant 10 | 11 | func add_script(component_script: GDScript) -> bool: 12 | if not _is_parent_script(component_script, _component_base_script): 13 | return false 14 | if not component_script.get_script_constant_map().has(_component_name_constant): 15 | return false 16 | var component_name = get_component_name(component_script) 17 | if has_component_script(component_name): 18 | return false 19 | _component_script_dict[component_name] = component_script 20 | return true 21 | 22 | func has_component_script(component_name: StringName) -> bool: 23 | return _component_script_dict.has(component_name) 24 | 25 | func get_component_name(component_script: GDScript) -> StringName: 26 | return component_script.get_script_constant_map().get(_component_name_constant) 27 | 28 | func instantiate_component(component_name: StringName) -> EcsComponentBase: 29 | var component_script: GDScript = _component_script_dict.get(component_name) 30 | return component_script.new() 31 | 32 | func component_to_dict(component: EcsComponentBase) -> Dictionary: 33 | var data: Dictionary = {} 34 | var property_name_list = component.get_property_name_list() 35 | for property_name in property_name_list: 36 | data[property_name] = component.get_property_value(property_name) 37 | return data 38 | 39 | func dict_to_component(component_name: StringName, data: Dictionary) -> EcsComponentBase: 40 | var component := instantiate_component(component_name) 41 | var property_name_list := component.get_property_name_list() 42 | for property_name in property_name_list: 43 | if not data.has(property_name): 44 | continue 45 | component.set_property_value(property_name, data[property_name]) 46 | pass 47 | return component 48 | -------------------------------------------------------------------------------- /addons/gdscript-ecs/EcsEntity.gd: -------------------------------------------------------------------------------- 1 | # Entity 2 | class_name EcsEntity extends RefCounted 3 | 4 | var _ref_world: WeakRef 5 | var _entity_id: int 6 | 7 | var entity_id: int: 8 | get: return entity_id 9 | 10 | var world: EcsWorld: 11 | get: return _ref_world.get_ref() 12 | 13 | var components: Dictionary: 14 | get: return world._get_components(_entity_id) 15 | 16 | func _init(world: EcsWorld, entity_id: int) -> void: 17 | _ref_world = weakref(world) 18 | _entity_id = entity_id 19 | pass 20 | 21 | func remove_self(): 22 | world._remove_entity(_entity_id) 23 | 24 | func create_component(component_name: StringName) -> EcsComponentBase: 25 | return world._create_component(_entity_id, component_name) 26 | 27 | func has_component(component_name: StringName) -> bool: 28 | return components.has(component_name) 29 | 30 | func get_component(component_name: StringName) -> EcsComponentBase: 31 | return components.get(component_name, null) 32 | 33 | func remove_component(component_name: StringName) -> bool: 34 | return world._remove_component(_entity_id, component_name) 35 | 36 | func remove_component_all(entity_id: int) -> void: 37 | world._remove_component_all(_entity_id) 38 | -------------------------------------------------------------------------------- /addons/gdscript-ecs/EcsRegistrarBase.gd: -------------------------------------------------------------------------------- 1 | ## EcsCommandRegistrarBase 2 | ## - Register commands or events before EcsSystem initialization. 3 | class_name EcsRegistrarBase 4 | extends RefCounted 5 | 6 | var _ref_world: WeakRef 7 | 8 | var world: EcsWorld: 9 | get: 10 | return _ref_world.get_ref() 11 | 12 | func _on_set_world(ecs_world: EcsWorld): 13 | _ref_world = weakref(ecs_world) 14 | 15 | ## Step 1. 16 | ## [override] 17 | func _on_before_ready() -> void: 18 | pass 19 | 20 | ## Step 2. 21 | ## [override] 22 | func _on_ready() -> void: 23 | pass 24 | 25 | ## Step 3. 26 | ## [override] 27 | func _on_system_before_ready() -> void: 28 | pass 29 | 30 | ## Step 3. 31 | ## [override] 32 | func _on_system_on_ready() -> void: 33 | pass 34 | 35 | 36 | func has_event(event_name: StringName) -> bool: 37 | return world.has_event(event_name) 38 | 39 | func add_event(event_signal: Signal) -> void: 40 | world.add_event(event_signal) 41 | 42 | func remove_event(event_name: StringName) -> bool: 43 | return world.remove_event(event_name) 44 | 45 | func get_event(event_name: StringName) -> Signal: 46 | return world.get_event(event_name) 47 | 48 | func event(event_name: StringName) -> Signal: 49 | return world.get_event(event_name) 50 | 51 | 52 | func has_command(command_name: StringName) -> bool: 53 | return world.has_command(command_name) 54 | 55 | func add_command(command_name: StringName, callable: Callable) -> void: 56 | world.add_command(command_name, callable) 57 | 58 | func get_command(command_name: StringName) -> Callable: 59 | return world.get_command(command_name) 60 | 61 | func command(command_name: StringName) -> Callable: 62 | return world.get_command(command_name) 63 | 64 | -------------------------------------------------------------------------------- /addons/gdscript-ecs/EcsRegistrarScanner.gd: -------------------------------------------------------------------------------- 1 | ## Registrar 2 | ## 3 | ## Sometimes you only need to add events and commands without dealing with 'EcsSystemBase._on_update()', 4 | ## so you don't need to add a system, but just a registrar. 5 | ## 6 | class_name EcsRegistrarScanner extends _EcsGDScriptScanner 7 | 8 | var _registrar_base_script: Script 9 | 10 | ## Array[EcsRegistrarBase | '... Duck Type'] 11 | var _registrar_list: Array = [] 12 | 13 | func _init(registrar_base_script: Script = null) -> void: 14 | _registrar_base_script = EcsRegistrarBase if registrar_base_script == null else registrar_base_script 15 | pass 16 | 17 | func add_script(script: GDScript) -> bool: 18 | if not _is_parent_script(script, _registrar_base_script): 19 | return false 20 | var registrar = script.new() 21 | _registrar_list.append(registrar) 22 | return true 23 | 24 | func add_registrar(registrar: EcsRegistrarBase) -> void: 25 | assert(registrar != null, "registrar is null") 26 | _registrar_list.append(registrar) 27 | 28 | func get_registrar_list() -> Array: 29 | return _registrar_list 30 | -------------------------------------------------------------------------------- /addons/gdscript-ecs/EcsSystemBase.gd: -------------------------------------------------------------------------------- 1 | ## System 2 | class_name EcsSystemBase extends RefCounted 3 | 4 | var _ref_world: WeakRef 5 | var bind_event_name_list: Array[StringName] = [] 6 | var bind_command_name_list: Array[StringName] = [] 7 | 8 | var world: EcsWorld: 9 | get: 10 | return _ref_world.get_ref() 11 | 12 | 13 | ## Step 0. 14 | ## When registering the system, this function will be called before '_on'beforeready'. 15 | func _on_set_world(ecs_world: EcsWorld): 16 | _ref_world = weakref(ecs_world) 17 | 18 | ## Step 1. 19 | ## Preparing to register the system. 20 | ## 21 | ## At this stage, it is suitable to call 22 | ## the 'add_system_command()' function to register events. 23 | ## [override] 24 | func _on_before_ready() -> void: 25 | pass 26 | 27 | ## Step 2. 28 | ## Indicates that all systems have been registered and completed. 29 | ## 30 | ## At this stage, it is suitable to listen to signals registered with other systems. 31 | ## [override] 32 | func _on_ready() -> void: 33 | pass 34 | 35 | ## Step 3. 36 | ## [override] 37 | func _on_pre_update(uplink_data: Dictionary) -> Dictionary: 38 | return uplink_data 39 | 40 | ## Step 4. 41 | ## [override] 42 | func _on_update(downlink_data: Dictionary) -> Dictionary: 43 | return downlink_data 44 | 45 | ## Step 5. 46 | ## [override] 47 | func _on_removed() -> void: 48 | for event_name in bind_event_name_list: 49 | remove_system_event(event_name) 50 | for command_name in bind_command_name_list: 51 | remove_system_command(command_name) 52 | pass 53 | 54 | 55 | func has_event(event_name: StringName) -> bool: 56 | return world.has_event(event_name) 57 | 58 | func add_system_event(event_signal: Signal) -> void: 59 | if world.add_event(event_signal): 60 | bind_event_name_list.append(event_signal.get_name()) 61 | 62 | func remove_system_event(event_name: StringName) -> bool: 63 | if not bind_event_name_list.has(event_name): 64 | return false 65 | bind_event_name_list.erase(event_name) 66 | return world.remove_event(event_name) 67 | 68 | func get_event(event_name: StringName) -> Signal: 69 | return world.get_event(event_name) 70 | 71 | func event(event_name: StringName) -> Signal: 72 | return world.get_event(event_name) 73 | 74 | 75 | func has_command(command_name: StringName) -> bool: 76 | return world.has_command(command_name) 77 | 78 | func add_system_command(command_name: StringName, callable: Callable) -> void: 79 | if world.add_command(command_name, callable): 80 | bind_command_name_list.append(command_name) 81 | 82 | func remove_system_command(command_name: StringName) -> bool: 83 | if not bind_command_name_list.has(command_name): 84 | return false 85 | bind_command_name_list.erase(command_name) 86 | return world.remove_command(command_name) 87 | 88 | func get_command(command_name: StringName) -> Callable: 89 | return world.get_command(command_name) 90 | 91 | func command(command_name: StringName) -> Callable: 92 | return world.get_command(command_name) 93 | 94 | 95 | -------------------------------------------------------------------------------- /addons/gdscript-ecs/EcsSystemScanner.gd: -------------------------------------------------------------------------------- 1 | class_name EcsSystemScanner extends _EcsGDScriptScanner 2 | 3 | var _system_base_script: Script 4 | 5 | ## Array[ ArrayIndex, system_script:GDScript ] 6 | var _system_script_list: Array[GDScript] = [] 7 | 8 | func _init(system_base_script: Script = null) -> void: 9 | _system_base_script = EcsSystemBase if system_base_script == null else system_base_script 10 | pass 11 | 12 | func add_script(script: GDScript) -> bool: 13 | if not _is_parent_script(script, _system_base_script): 14 | return false 15 | _system_script_list.append(script) 16 | return true 17 | 18 | ## Instantiate all systems 19 | func load_system_list() -> Array[EcsSystemBase]: 20 | var system_list: Array[EcsSystemBase] = [] 21 | for system_script in _system_script_list: 22 | var system = system_script.new() 23 | system_list.append(system) 24 | return system_list 25 | -------------------------------------------------------------------------------- /addons/gdscript-ecs/EcsWorld.gd: -------------------------------------------------------------------------------- 1 | ## World 2 | ## 3 | ## # Function Call Order 4 | ## ## EcsWorld.new(...) 5 | ## ↓ - EcsWorld._init() 6 | ## ↓ for - EcsRegistrarBase._on_set_world(...) 7 | ## ↓ for - EcsRegistrarBase._on_before_ready() 8 | ## ↓ for - EcsRegistrarBase._on_ready() 9 | ## ↓ EcsWorld.init_system() 10 | ## ↓ for - EcsSystemBase._on_set_world(...) 11 | ## ↓ for - EcsRegistrarBase._on_system_before_ready() 12 | ## ↓ for - EcsSystemBase._on_before_ready(...) 13 | ## ↓ for - EcsSystemBase._on_ready(...) 14 | ## ↓ for - EcsRegistrarBase._on_system_on_ready() 15 | ## ## EcsWorld.update(...) 16 | ## ↓ for - EcsSystemBase._on_pre_update(...) 17 | ## ↓ for - EcsSystemBase._on_update(...) 18 | ## 19 | class_name EcsWorld extends RefCounted 20 | 21 | # --------------------- 22 | 23 | ## Used to delay system initialization, 24 | ## so that the system can access externally 25 | ## added commands and events during the initialization phase. 26 | var _system_not_initialized := true 27 | 28 | ## Responsible for instantiating components 29 | var _component_scanner: EcsComponentScanner 30 | 31 | var _registrar_scanner: EcsRegistrarScanner 32 | 33 | ## All system instances 34 | var _system_list: Array[EcsSystemBase] 35 | 36 | ## The expected number of cached entity_id. 37 | ## Due to the default function implementation, 38 | ## only the entity ID at the end is reclaimed, 39 | ## so the current variable is only a suggested value. 40 | ## Please refer to the function '_on_entity_unrecycled()' for details. 41 | var _entity_id_pool_capacity: int 42 | 43 | ## Dictionary[ any, any ] 44 | var _update_cache: Dictionary = {} 45 | 46 | ## Dictionary[ command_name: StringName, callable: Callable ] 47 | var _command_dcit: Dictionary = {} 48 | 49 | ## Dictionary[ event_name:StringName, event_signal: Signal ] 50 | var _event_dict: Dictionary = {} 51 | 52 | # --------------------- 53 | 54 | ## Array[ ArrayIndex, entity_id:int ] 55 | var _entity_id_pool: Array[int] = [] 56 | 57 | ## Array[ ArrayIndex, entity:EcsEntity ] 58 | var _entity_list: Array[EcsEntity] = [] 59 | 60 | ## Array[ entity_id:ArrayIndex, components:Dictionary[ component_name:StringName, component:EcsComponentBase ] ] 61 | var _entity_list_components: Array[Dictionary] = [] 62 | 63 | ## Dictionary[ component_name:StringName, entity_id_list:Array[ ArrayIndex, entity_id:int ] ] 64 | var _index__component_name_to_entity_id_list: Dictionary = {} 65 | 66 | 67 | func _init( component_scanner: EcsComponentScanner, 68 | system_scanner: EcsSystemScanner, 69 | registrar_scanner: EcsRegistrarScanner = null, 70 | entity_id_pool_capacity: int = 255) -> void: 71 | _component_scanner = component_scanner 72 | _registrar_scanner = registrar_scanner 73 | _entity_id_pool_capacity = entity_id_pool_capacity 74 | _system_list = system_scanner.load_system_list() 75 | _init_registrar_list() 76 | pass 77 | 78 | func _init_registrar_list() -> void: 79 | if _registrar_scanner == null: 80 | return 81 | var registrar_list = _registrar_scanner.get_registrar_list() 82 | # _on_set_world 83 | for registrar in registrar_list: 84 | registrar._on_set_world(self) 85 | # _on_before_ready 86 | for registrar in registrar_list: 87 | registrar._on_before_ready() 88 | # _on_ready 89 | for registrar in registrar_list: 90 | registrar._on_ready() 91 | pass 92 | 93 | ## Trigger system initialization 94 | ## 95 | ## If you did not call the 'init_system()' function, 96 | ## it will be automatically called when the 'update()' function is called. 97 | func init_system() -> void: 98 | if not _system_not_initialized: 99 | return 100 | _system_not_initialized = false 101 | # registrar_list 102 | var registrar_list: Array = [] if _registrar_scanner == null \ 103 | else _registrar_scanner.get_registrar_list() 104 | _registrar_scanner = null 105 | # Step 1. 106 | for system in _system_list: 107 | system._on_set_world(self) 108 | # Step 2. 109 | for registrar in registrar_list: 110 | registrar._on_system_before_ready() 111 | for system in _system_list: 112 | system._on_before_ready() 113 | # Step 3. 114 | for system in _system_list: 115 | system._on_ready() 116 | for registrar in registrar_list: 117 | registrar._on_system_on_ready() 118 | pass 119 | 120 | func update(data = null) -> Dictionary: 121 | if _system_not_initialized: 122 | init_system() 123 | if data == null: 124 | data = _update_cache 125 | data.clear() 126 | assert(data is Dictionary, "The 'data' parameter must be of type Dictionary") 127 | # on_pre_update 128 | for system in _system_list: 129 | data = system._on_pre_update(data) 130 | # on_update 131 | for system in _system_list: 132 | data = system._on_update(data) 133 | return data 134 | 135 | func get_entity_list() -> Array[EcsEntity]: 136 | return _entity_list 137 | 138 | func find_entity_first(component_name: StringName) -> EcsEntity: 139 | var entity_id_list = _index__component_name_to_entity_id_list.get(component_name) 140 | if entity_id_list == null or entity_id_list.is_empty(): 141 | return null 142 | var entity_id = entity_id_list[0] 143 | return _entity_list[entity_id] 144 | 145 | func find_entity_list(component_name: StringName) -> Array[EcsEntity]: 146 | var out_entity_list: Array[EcsEntity] = [] 147 | var entity_id_list = _index__component_name_to_entity_id_list.get(component_name) 148 | if entity_id_list == null: 149 | return out_entity_list 150 | for i in entity_id_list.size(): 151 | var entity_id = entity_id_list[i] 152 | var entity = _entity_list[entity_id] 153 | assert(entity != null) 154 | out_entity_list.append(entity) 155 | return out_entity_list 156 | 157 | func find_entity_list_append(component_name: StringName, out_entity_list: Array[EcsEntity]) -> void: 158 | var entity_id_list = _index__component_name_to_entity_id_list.get(component_name) 159 | if entity_id_list == null: 160 | return 161 | for i in entity_id_list.size(): 162 | var entity_id = entity_id_list[i] 163 | var entity = _entity_list[entity_id] 164 | assert(entity != null) 165 | if not out_entity_list.has(entity): 166 | out_entity_list.append(entity) 167 | pass 168 | 169 | func find_component_first(component_name: StringName) -> EcsComponentBase: 170 | var entity = find_entity_first(component_name) 171 | if entity == null: 172 | return null 173 | return entity.get_component(component_name) 174 | 175 | func create_entity() -> EcsEntity: 176 | var entity: EcsEntity 177 | if _entity_id_pool.is_empty(): 178 | var entity_id_next = _entity_list.size() 179 | entity = EcsEntity.new(self, entity_id_next) 180 | _entity_list.append(entity) 181 | _entity_list_components.append({}) 182 | else: 183 | var entity_id = _entity_id_pool.pop_back() 184 | entity = _entity_list[entity_id] 185 | pass 186 | return entity 187 | 188 | func get_entity(entity_id: int) -> EcsEntity: 189 | return _entity_list[entity_id] 190 | 191 | func _remove_entity(entity_id: int) -> void: 192 | _remove_component_all(entity_id) 193 | if not _recovery_entity(entity_id): 194 | # For entities that have not been recycled, 195 | # they will be placed in the cache for reuse 196 | var components = _entity_list_components[entity_id] 197 | components.clear() 198 | _entity_id_pool.append(entity_id) 199 | pass 200 | 201 | func _recovery_entity(entity_id: int) -> bool: 202 | if _entity_id_pool.size() <= _entity_id_pool_capacity: 203 | return false 204 | var last_entity = _entity_list.back() 205 | if last_entity._entity_id == entity_id: 206 | # Because Array.resize() has high modification performance. 207 | # For example, viewing the Array.resize() function description 208 | # So Array.pop_back() should have the best performance in removing data elements. 209 | _entity_list.pop_back() 210 | _entity_list_components.pop_back() 211 | return true 212 | else: 213 | # Unable to recycle at optimal performance 214 | return _on_entity_unrecycled(entity_id) 215 | pass 216 | 217 | ## Triggered when entities cannot be recycled at optimal performance. 218 | ## 219 | ## The default is not to attempt to recycle the entity again, 220 | ## but to force it to be stored in the buffer pool. 221 | ## 222 | ## If you need to ensure that the entity is recycled, 223 | ## implement this method and return true, 224 | ## indicating that the entity has been recycled and cannot be placed in the cache. 225 | ## [override] 226 | func _on_entity_unrecycled(entity_id: int) -> bool: 227 | return false 228 | 229 | func _on_create_component_instantiate(component_name: StringName) -> Variant: 230 | if not _component_scanner.has_component_script(component_name): 231 | assert(false, "Class not find: component_name[" + component_name + "]") 232 | return null 233 | return _component_scanner.instantiate_component(component_name) 234 | 235 | func _create_component(entity_id: int, component_name: StringName) -> EcsComponentBase: 236 | var component: EcsComponentBase = _on_create_component_instantiate(component_name) 237 | # ref 238 | var components = _get_components(entity_id) 239 | if components.has(component_name): 240 | push_error("Component are repeatedly added to the same entity: component_name[%s] entity_id[%s]" % \ 241 | [component_name, entity_id] \ 242 | ) 243 | assert(false, "Component are repeatedly added to the same entity: component_name[%s] entity_id[%s]" % \ 244 | [component_name, entity_id] \ 245 | ) 246 | # Although the data was returned, it has no association, 247 | # and even if the data is set, it will not have any effect, 248 | # just to prevent null pointers. 249 | return component 250 | components[component_name] = component 251 | component._entity_id = entity_id 252 | # index 253 | if not _index__component_name_to_entity_id_list.has(component_name): 254 | var new_index: Array[int] = [] 255 | _index__component_name_to_entity_id_list[component_name] = new_index 256 | var index: Array[int] = _index__component_name_to_entity_id_list[component_name] 257 | index.append(entity_id) 258 | return component 259 | 260 | func _get_components(entity_id: int) -> Dictionary: 261 | return _entity_list_components[entity_id] 262 | 263 | func _remove_component(entity_id: int, component_name: StringName) -> bool: 264 | # ref 265 | var components = _get_components(entity_id) 266 | var component: EcsComponentBase = components.get(component_name) 267 | if component == null: 268 | return false 269 | component._entity_id = -1 270 | components.erase(component_name) 271 | # index 272 | var index: Array[int] = _index__component_name_to_entity_id_list[component_name] 273 | index.erase(entity_id) 274 | if index.is_empty(): 275 | _index__component_name_to_entity_id_list.erase(component_name) 276 | return true 277 | 278 | func _remove_component_all(entity_id: int) -> void: 279 | var components = _get_components(entity_id) 280 | for componentName in components.keys(): 281 | _remove_component(entity_id, componentName) 282 | pass 283 | 284 | func has_event(event_name: StringName) -> bool: 285 | return _event_dict.has(event_name) 286 | 287 | func add_event(event_signal: Signal) -> bool: 288 | if has_event(event_signal.get_name()): 289 | assert(false, "Repeatedly adding event: " + str(event_signal)) 290 | return false 291 | _event_dict[event_signal.get_name()] = event_signal 292 | return true 293 | 294 | func get_event(event_name: StringName) -> Signal: 295 | var event_signal = _event_dict.get(event_name) 296 | assert(event_signal != null, "Not find event_signal: " + event_name) 297 | return event_signal 298 | 299 | func remove_event(event_name: StringName) -> bool: 300 | return _event_dict.erase(event_name) 301 | 302 | func has_command(command_name: StringName) -> bool: 303 | return _command_dcit.has(command_name) 304 | 305 | func add_command(command_name: StringName, callable: Callable) -> bool: 306 | if has_command(command_name): 307 | assert(false, "Repeatedly adding command: " + command_name) 308 | return false 309 | _command_dcit[command_name] = callable 310 | return true 311 | 312 | func remove_command(command_name: StringName) -> bool: 313 | return _command_dcit.erase(command_name) 314 | 315 | func get_command(command_name: StringName) -> Callable: 316 | return _command_dcit[command_name] 317 | 318 | func remove_system(system: EcsSystemBase) -> bool: 319 | var system_id = _system_list.find(system) 320 | if system_id == -1: 321 | return false 322 | return remove_system_by_id(system_id) 323 | 324 | func remove_system_by_id(system_id: int) -> bool: 325 | if system_id >= 0 and system_id < _system_list.size(): 326 | var system: EcsSystemBase = _system_list[system_id] 327 | _system_list.remove_at(system_id) 328 | _on_system_removed(system) 329 | return true 330 | else: 331 | return false 332 | 333 | func remove_system_all() -> void: 334 | var size = _system_list.size() 335 | for i in size: 336 | remove_system_by_id(size - 1 - i) 337 | pass 338 | 339 | func _on_system_removed(system: EcsSystemBase) -> void: 340 | system._on_removed() 341 | pass 342 | 343 | 344 | 345 | func export_dict() -> Dictionary: 346 | var data: Dictionary = {} 347 | # Entity 348 | data["entity_id_pool"] = _entity_id_pool 349 | data["entity_id_capacity"] = _entity_list.size() 350 | # Component 351 | var component_name_list := _index__component_name_to_entity_id_list.keys() 352 | data["components"] = component_name_list 353 | data["components_list"] = _entity_list_components.map( 354 | __entity_list_components_to_dictionary.bind(component_name_list) 355 | ) 356 | return data 357 | 358 | func __entity_list_components_to_dictionary(components: Dictionary, component_name_list: Array) -> Dictionary: 359 | var data: Dictionary = {}; 360 | for component_name in components: 361 | var component_name_index = component_name_list.find(component_name) 362 | data[component_name_index] = _component_scanner.component_to_dict(components[component_name]) 363 | return data 364 | 365 | func import_dict(data: Dictionary): 366 | # Entity 367 | _entity_id_pool.clear() 368 | _entity_id_pool.append_array(data["entity_id_pool"]) 369 | var entity_id_capacity: int = data["entity_id_capacity"] 370 | if entity_id_capacity < _entity_list.size(): 371 | _entity_list.resize(entity_id_capacity) 372 | pass 373 | elif entity_id_capacity > _entity_list.size(): 374 | var entity_id_next = _entity_list.size() 375 | while entity_id_next < entity_id_capacity: 376 | _entity_list.append(EcsEntity.new(self, entity_id_next)) 377 | entity_id_next += 1 378 | pass 379 | # Component 380 | var component_name_list: Array = data["components"] 381 | var components_list: Array = data["components_list"] 382 | _entity_list_components.clear() 383 | _entity_list_components.append_array( 384 | components_list.map(__dictionary_to_entity_list_components.bind(component_name_list)) 385 | ) 386 | pass 387 | 388 | func __dictionary_to_entity_list_components(data: Dictionary, component_name_list: Array) -> Dictionary: 389 | var components: Dictionary = {} 390 | var index: int 391 | var component_name: String 392 | for component_name_index in data: 393 | # component_name_index:int to component_name:String 394 | index = int(component_name_index) 395 | assert(index < component_name_list.size(), "Import data error, non-existent component name index: " + str(component_name_index)) 396 | component_name = component_name_list[index] 397 | var component_data = data[component_name_index] 398 | # instantiate component 399 | var component = _component_scanner.dict_to_component(component_name, component_data) 400 | components[component_name] = component 401 | # index 402 | if not _index__component_name_to_entity_id_list.has(component_name): 403 | _index__component_name_to_entity_id_list[component_name] = [] 404 | _index__component_name_to_entity_id_list[component_name].append(component._entity_id) 405 | 406 | return components 407 | 408 | -------------------------------------------------------------------------------- /addons/gdscript-ecs/README-zh.md: -------------------------------------------------------------------------------- 1 | # GDScript ECS 框架 2 | 3 | 4 | ## 框架特点 5 | 6 | 1. 所有的ECS组件,都支持语法提示。每个组件都是GDScript自定义类。 7 | 8 | 2. 查询实体非常快。实体ID是数组索引。 9 | 10 | 3. 添加和删除实体依然也非常快。这是因为框架使用了缓存池,实现对实体的重复使用。 11 | 12 | 4. 支持序列化,导入数据和导出数据,而不需要任何配置。并且数据非常紧凑。 13 | 14 | 5. 使用起来非常的简洁,逻辑清晰。 15 | 16 | 17 | ## 什么是ECS架构 18 | 19 | ECS全称Entity-Component-System,即实体-组件-系统。是一种软件架构,主要用于游戏开发。 20 | 21 | 22 | ## 了解框架 23 | 24 | 框架有三个主要元素组成,分别是`Entity`、`Component`和`System`。另外还有`Event`和`Command`两个元素,辅助`System`元素完成代码逻辑。 25 | 26 | - `Entity` 实体:是组件的容器,一个实体包含多个组件,实体本身不负责存储数据。 27 | - `Component` 组件:负责存储数据。其行为与Dictionary作用相同。 28 | - `System` 系统:逻辑处理。 29 | - `Event` 事件:一个人抛出事件,可以多个人(也可以没人)监听事件。抛出事件后,没有结果。其实现,就是Godot的信号。 30 | - `Command` 命令:一个人调用命令,一个人执行命令。命令允许返回结果。其实现,就是一个函数。 31 | 32 | ## 例子 33 | ```gdscript 34 | static func test(): 35 | # 添加组件脚本 36 | var component_scanner = EcsComponentScanner.new() 37 | component_scanner.add_script(Component_Namer) 38 | component_scanner.add_script(Component_Test2) 39 | # component_scanner.add_script(load("Component_xxxx.gd")) 40 | # component_scanner.scan_script("res://gdscript_ecs_test/component/", "*.gd") 41 | 42 | # 添加系统脚本 43 | var system_scanner = EcsSystemScanner.new() 44 | system_scanner.add_script(System_Test1) 45 | system_scanner.add_script(System_Test2) 46 | # system_scanner.add_script(load("System_xxxx1.gd")) 47 | # system_scanner.scan_script("res://gdscript_ecs_test/system/", "*.gd") 48 | 49 | # 初始化世界 50 | var world := EcsWorld.new(component_scanner, system_scanner) 51 | world.init_system() # 可选的 52 | 53 | world.add_command("test_world_print", func(arg): 54 | print("[world1_print]", arg) 55 | pass) 56 | 57 | # 你可以任何地方创建实体,例如在这里创建实体 58 | var entity = world.create_entity() 59 | var entity_namer = entity.create_component(Component_Namer.NAME) as Component_Namer 60 | entity_namer.display_name = "Entities created in any location" 61 | 62 | # 让所有系统处理数据 63 | for i in 15: 64 | world.update({ 65 | count = i + 1 66 | }) 67 | 68 | # 导出数据到存档 69 | var world_data = world.export_dict() 70 | var world_data_json_str = JSON.stringify(world_data, "\t") 71 | print("[世界1的存档数据] ", world_data_json_str) 72 | 73 | # 从存档导入数据到一个新的世界 74 | var world2 := EcsWorld.new(component_scanner, system_scanner) 75 | var world_data_json = JSON.parse_string(world_data_json_str) 76 | world2.import_dict(world_data_json) 77 | print("[世界2的存档数据] ", JSON.stringify(world_data_json, "\t")) 78 | 79 | # 导入数据到世界中,必须确保世界具有相同的命令和事件 80 | # 81 | # 你认为这样同步很麻烦吗? 82 | # 您可以使用`EcsRegisterScanner`类,来实现仅创建命令和事件的功能。 83 | # 84 | world2.add_command("test_world_print", func(arg): 85 | print("[world2_print]", arg) 86 | pass) 87 | 88 | # 世界已经完全恢复,你可以随心所欲地调用它。 89 | world2.update({ 90 | count = 1 91 | }) 92 | pass 93 | 94 | ## 为实体提供命名数据 95 | class Component_Namer extends EcsComponentBase: 96 | const NAME = &"Namer" 97 | ## Display Name 98 | var display_name: String = "" 99 | 100 | ## 第二个测试组件 101 | class Component_Test2 extends EcsComponentBase: 102 | const NAME = &"Test2" 103 | var count: int = -1 104 | 105 | ## 一个测试系统 106 | class System_Test1 extends EcsSystemBase: 107 | signal on_entity_created() 108 | 109 | func _on_before_ready() -> void: 110 | add_system_event(on_entity_created) 111 | pass 112 | 113 | func _on_update(downlink_data: Dictionary) -> Dictionary: 114 | if downlink_data.count == 10: 115 | # Create entities in the system 116 | var entity = world.create_entity() 117 | 118 | var namer = entity.create_component(Component_Namer.NAME) as Component_Namer 119 | namer.display_name = "Test1 Entity - " + str(downlink_data.count) 120 | 121 | var component2 = entity.create_component(Component_Test2.NAME) as Component_Test2 122 | component2.count = downlink_data.count 123 | 124 | downlink_data.count = 11111111111111 125 | on_entity_created.emit() 126 | pass 127 | return downlink_data 128 | pass 129 | 130 | ## 第二个测试系统 131 | class System_Test2 extends EcsSystemBase: 132 | 133 | func _on_ready() -> void: 134 | # 连接来自其它地方注册的信号 135 | get_event("on_entity_created").connect(__on_entity_created) 136 | pass 137 | 138 | func __on_entity_created(): 139 | print("[系统2] 收到信号,系统1创建了实体") 140 | pass 141 | 142 | func _on_update(downlink_data: Dictionary) -> Dictionary: 143 | var text = "[系统2] downlink_data.count: " + str(downlink_data.count) 144 | get_command("test_world_print").call(text) 145 | return downlink_data 146 | pass 147 | ``` 148 | -------------------------------------------------------------------------------- /addons/gdscript-ecs/README.md: -------------------------------------------------------------------------------- 1 | # GDScript ECS framework 2 | 3 | 4 | ## Framework features 5 | 6 | 1. All ECS components support syntax prompts. Each component is a GDScript custom class. 7 | 8 | 2. The entity query is very fast. The entity ID is an array index. 9 | 10 | 3. Adding and deleting entities is still very fast. This is because the framework uses cache pooling to achieve reuse of entities. 11 | 12 | 4. Supports serialization, importing and exporting data without any configuration. And the data is very compact. 13 | 14 | 5. It is very concise and logically clear to use. 15 | 16 | 17 | ## What is ECS architecture 18 | 19 | The full name of ECS is Entity Component System. It is a software architecture primarily used for game development. 20 | 21 | 22 | ## Framework concept 23 | 24 | The framework consists of three main elements, namely 'Entity', 'Component', and 'System'. 25 | In addition, there are two elements, 'Event' and 'Command', which assist the 'System' element in completing the code logic. 26 | 27 | - `Entity`: It is a container for components, where an entity contains multiple components and the entity itself is not responsible for storing data. 28 | - `Component`: Responsible for storing data. Its behavior is the same as that of a Dictionary. 29 | - `System`: Logical processing. 30 | - `Event`: One person can throw an event, and multiple people (or no one) can listen to the event. After throwing the event, there is no result. In fact, it is the signal in Godot. 31 | - `Command`: One person calls the command, one person executes the command. The command allows for the return of results. In fact, it is a function. 32 | 33 | ## Example 34 | ```gdscript 35 | static func test(): 36 | # Add component script 37 | var component_scanner = EcsComponentScanner.new() 38 | component_scanner.add_script(Component_Namer) 39 | component_scanner.add_script(Component_Test2) 40 | # component_scanner.add_script(load("Component_xxxx.gd")) 41 | # component_scanner.scan_script("res://gdscript_ecs_test/component/", "*.gd") 42 | 43 | # Add system script 44 | var system_scanner = EcsSystemScanner.new() 45 | system_scanner.add_script(System_Test1) 46 | system_scanner.add_script(System_Test2) 47 | # system_scanner.add_script(load("System_xxxx1.gd")) 48 | # system_scanner.scan_script("res://gdscript_ecs_test/system/", "*.gd") 49 | 50 | # Initialize the world 51 | var world := EcsWorld.new(component_scanner, system_scanner) 52 | world.init_system() # optional 53 | 54 | world.add_command("test_world_print", func(arg): 55 | print("[world1_print]", arg) 56 | pass) 57 | 58 | # Entities created in any location 59 | var entity = world.create_entity() 60 | var entity_namer = entity.create_component(Component_Namer.NAME) as Component_Namer 61 | entity_namer.display_name = "Entities created in any location" 62 | 63 | # Processing data 64 | for i in 15: 65 | world.update({ 66 | count = i + 1 67 | }) 68 | 69 | # Export world data for saving data. 70 | var world_data = world.export_dict() 71 | var world_data_json_str = JSON.stringify(world_data, "\t") 72 | print("[world_data] ", world_data_json_str) 73 | 74 | # Import data. 75 | var world2 := EcsWorld.new(component_scanner, system_scanner) 76 | var world_data_json = JSON.parse_string(world_data_json_str) 77 | world2.import_dict(world_data_json) 78 | print("[world_data2] ", JSON.stringify(world_data_json, "\t")) 79 | 80 | # In the world of importing data, 81 | # be sure to have the same commands and events 82 | # 83 | # Do you think synchronizing like this is troublesome? 84 | # You can use the 'EcsRegisterScanner' class to implement the function of only creating commands and events. 85 | # 86 | world2.add_command("test_world_print", func(arg): 87 | print("[world2_print]", arg) 88 | pass) 89 | 90 | # The world has been fully restored and you can call it as you please. 91 | world2.update({ 92 | count = 1 93 | }) 94 | pass 95 | 96 | ## Provide named data for entities 97 | class Component_Namer extends EcsComponentBase: 98 | const NAME = &"Namer" 99 | ## Display Name 100 | var display_name: String = "" 101 | 102 | class Component_Test2 extends EcsComponentBase: 103 | const NAME = &"Test2" 104 | var count: int = -1 105 | 106 | class System_Test1 extends EcsSystemBase: 107 | signal on_entity_created() 108 | 109 | func _on_before_ready() -> void: 110 | add_system_event(on_entity_created) 111 | pass 112 | 113 | func _on_update(downlink_data: Dictionary) -> Dictionary: 114 | if downlink_data.count == 10: 115 | # Create entities in the system 116 | var entity = world.create_entity() 117 | 118 | var namer = entity.create_component(Component_Namer.NAME) as Component_Namer 119 | namer.display_name = "Test1 Entity - " + str(downlink_data.count) 120 | 121 | var component2 = entity.create_component(Component_Test2.NAME) as Component_Test2 122 | component2.count = downlink_data.count 123 | 124 | downlink_data.count = 11111111111111 125 | on_entity_created.emit() 126 | pass 127 | return downlink_data 128 | pass 129 | 130 | class System_Test2 extends EcsSystemBase: 131 | 132 | func _on_ready() -> void: 133 | get_event("on_entity_created").connect(__on_entity_created) 134 | pass 135 | 136 | func __on_entity_created(): 137 | print("[System_Test2] __on_entity_created") 138 | pass 139 | 140 | func _on_update(downlink_data: Dictionary) -> Dictionary: 141 | var text = "[System_Test2] downlink_data.count: " + str(downlink_data.count) 142 | get_command("test_world_print").call(text) 143 | return downlink_data 144 | pass 145 | ``` 146 | -------------------------------------------------------------------------------- /addons/gdscript-ecs/_EcsGDScriptScanner.gd: -------------------------------------------------------------------------------- 1 | ## GDScript Scanner 2 | ## 3 | ## Find and load scripts from certain directories 4 | ## 5 | class_name _EcsGDScriptScanner extends RefCounted 6 | 7 | func _init() -> void: 8 | pass 9 | 10 | ## Scan all scripts in the specified directory 11 | ## 12 | ## All script files in the upper directory must be loaded before those in the subdirectories. 13 | ## Therefore, for example, by scanning system scripts, 14 | ## you can achieve sequential loading of system scripts based on the hierarchy of directories. 15 | func scan_script(path: String, file_name_expr: String = "*.gd", include_sub_paths: bool = true) -> Error: 16 | var dir = DirAccess.open(path) 17 | if dir == null: 18 | return DirAccess.get_open_error() 19 | dir.list_dir_begin() 20 | var dir_name_list: PackedStringArray = [] 21 | while true: 22 | var file_name = dir.get_next() 23 | if file_name == "": 24 | break 25 | if dir.current_is_dir(): 26 | if include_sub_paths: 27 | dir_name_list.append(file_name) 28 | else: 29 | if not file_name.match(file_name_expr): 30 | continue 31 | var script = load(path + "/" + file_name) as GDScript 32 | assert(script != null, "Unable to load script: " + path + "/" + file_name) 33 | if script != null: 34 | add_script(script) 35 | pass 36 | pass 37 | dir.list_dir_end() 38 | # Load all scripts in the directory first, then scan the subdirectories. 39 | for dir_name in dir_name_list: 40 | scan_script(path + "/" + dir_name, file_name_expr, true) 41 | return OK 42 | 43 | func add_script(_script: GDScript) -> bool: 44 | assert(false, "Please re implement this function: 'add_script'") 45 | return false 46 | 47 | func _is_parent_script(child_script: Script, parent_script: Script) -> bool: 48 | var base_script: Script = child_script.get_base_script() 49 | while base_script != null: 50 | if base_script == parent_script: 51 | return true 52 | base_script = base_script.get_base_script() 53 | pass 54 | return false 55 | 56 | func _is_expected_script_structure(script: GDScript, script_structure: Dictionary) -> bool: 57 | # instantiate 58 | if script_structure.has("instantiate"): 59 | if script.can_instantiate() != script_structure.instantiate: 60 | return false 61 | # constant name 62 | if script_structure.has("constant_name_list"): 63 | var script_constant_map := script.get_script_constant_map() 64 | for constant_name in script_structure.constant_name_list: 65 | if not script_constant_map.has(constant_name): 66 | return false 67 | # method 68 | if script_structure.has("methods"): 69 | var methods = script_structure.methods 70 | for method_structure in methods: 71 | var script_method = _find_script_method(script, method_structure.name) 72 | if script_method == null: 73 | return false 74 | if method_structure.has("type"): 75 | if method_structure.type != script_method.type: 76 | return false 77 | if method_structure.has("flags"): 78 | if (script_method.flags & method_structure.flags) == 0: 79 | return false 80 | pass 81 | pass 82 | # property 83 | if script_structure.has("properties"): 84 | var properties = script_structure.properties 85 | for property_structure in properties: 86 | var script_property = _find_script_property(script, property_structure.name) 87 | if script_property == null: 88 | return false 89 | if property_structure.has("class_name"): 90 | if property_structure.class_name != script_property.class_name: 91 | return false 92 | pass 93 | if property_structure.has("type"): 94 | if property_structure.type != script_property.type: 95 | return false 96 | if property_structure.has("usage"): 97 | if (script_property.usage & property_structure.usage) == 0: 98 | return false 99 | pass 100 | pass 101 | # signal 102 | if script_structure.has("signals"): 103 | var signals = script_structure.signals 104 | for signal_structure in signals: 105 | var script_signal = _find_script_signal(script, signal_structure.name) 106 | if script_signal == null: 107 | return false 108 | if signal_structure.has("type"): 109 | if signal_structure.type != script_signal.type: 110 | return false 111 | if signal_structure.has("flags"): 112 | if (script_signal.flags & signal_structure.flags) == 0: 113 | return false 114 | pass 115 | return true 116 | 117 | ## return null or { 118 | ## name: String, 119 | ## args: Array, 120 | ## default_args: Array, 121 | ## flags: MethodFlags, 122 | ## id: int, 123 | ## return: Dictionary 124 | ## } 125 | func _find_script_method(script: GDScript, method_name: String): 126 | var script_method_list := script.get_script_method_list() 127 | for script_method in script_method_list: 128 | if script_method.name == method_name: 129 | return script_method 130 | return null 131 | 132 | ## return null or { 133 | ## name: String, 134 | ## class_name: StringName, 135 | ## type: int(Variant.Type), 136 | ## hint: PropertyHint, 137 | ## hint_string, 138 | ## usage: PropertyUsageFlags 139 | ## } 140 | func _find_script_property(script: GDScript, property_name: String): 141 | var script_property_list := script.get_script_property_list() 142 | for script_property in script_property_list: 143 | if script_property.name == property_name: 144 | return script_property 145 | return null 146 | 147 | ## return null or { 148 | ## name: String, 149 | ## args: Array, 150 | ## default_args: Array, 151 | ## flags: MethodFlags, 152 | ## id: int, 153 | ## return: Dictionary 154 | ## } 155 | func _find_script_signal(script: GDScript, signal_name: String): 156 | var script_signal_list = script.get_script_signal_list() 157 | for script_signal in script_signal_list: 158 | if script_signal.name == signal_name: 159 | return script_signal 160 | return null 161 | -------------------------------------------------------------------------------- /addons/gdscript-ecs/plugin.cfg: -------------------------------------------------------------------------------- 1 | [plugin] 2 | 3 | name="GDScript ECS 4.x" 4 | description="This ECS framework is very suitable for use in GDScript. Separate data and logic, get rid of the hassle of class inheritance relationships." 5 | author="yihrmc" 6 | version="1.1.0" 7 | script="plugin.gd" 8 | -------------------------------------------------------------------------------- /addons/gdscript-ecs/plugin.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends EditorPlugin 3 | 4 | func _enter_tree(): 5 | pass 6 | 7 | func _exit_tree(): 8 | pass 9 | -------------------------------------------------------------------------------- /gdscript_ecs_test/ecs/components/Component_Mob.gd: -------------------------------------------------------------------------------- 1 | class_name Component_Mob extends EcsComponentBase 2 | const NAME = &"Mob" 3 | -------------------------------------------------------------------------------- /gdscript_ecs_test/ecs/components/Component_MovableNode.gd: -------------------------------------------------------------------------------- 1 | class_name Component_MovableNode extends EcsComponentBase 2 | const NAME = &"MovableNode" 3 | 4 | ## Node instances in the Godot scene tree 5 | ## Nodes must have the following methods: 6 | ## - 'System_Movement.gd' used method: movement_set_data(position: Vector2, direction: Vector2i) 7 | var node: Node = null 8 | 9 | func get_property_name_list_of_hidden() -> PackedStringArray: 10 | # Hide some properties to prevent them from being serialized. 11 | return ["node"] 12 | -------------------------------------------------------------------------------- /gdscript_ecs_test/ecs/components/Component_Player.gd: -------------------------------------------------------------------------------- 1 | class_name Component_Player extends EcsComponentBase 2 | const NAME = &"Player" 3 | 4 | # The player component has no information at all, 5 | # currently it is only used to mark the entity as a player entity 6 | -------------------------------------------------------------------------------- /gdscript_ecs_test/ecs/components/Component_Position.gd: -------------------------------------------------------------------------------- 1 | ## The position of the entity 2 | class_name Component_Position extends EcsComponentBase 3 | const NAME = &"Position" 4 | 5 | ## Coordinate position 6 | var position: Vector2 = Vector2.ZERO 7 | 8 | ## Direction, Range -1 to 1 9 | var direction: Vector2i = Vector2i.ZERO 10 | 11 | var speed = 300 12 | -------------------------------------------------------------------------------- /gdscript_ecs_test/ecs/systems/System_Movement.gd: -------------------------------------------------------------------------------- 1 | ## 2 | ## Provide mobility for all entities 3 | ## 4 | extends EcsSystemBase 5 | 6 | func _on_ready() -> void: 7 | add_system_command("is_component_position_hits_wall", is_component_position_hits_wall) 8 | pass 9 | 10 | func is_component_position_hits_wall(component_position: Component_Position, screen_size) -> Vector2i: 11 | var wall: Vector2i = Vector2i.ZERO 12 | if component_position.position.x == 0: 13 | wall.x = -1 14 | elif component_position.position.x == screen_size.x: 15 | wall.x = 1 16 | if component_position.position.y == 0: 17 | wall.y = -1 18 | elif component_position.position.y == screen_size.y: 19 | wall.y = 1 20 | return wall 21 | 22 | func _on_update(downlink_data: Dictionary) -> Dictionary: 23 | var process_delta = downlink_data.process_delta as float 24 | if process_delta == null: 25 | return downlink_data 26 | var entity_list = world.find_entity_list(Component_Position.NAME) 27 | for entity in entity_list: 28 | # Update Position 29 | var position = entity.get_component(Component_Position.NAME) as Component_Position 30 | assert(position != null) 31 | var velocity = Vector2(position.direction) 32 | if velocity.length() > 0: 33 | velocity = velocity.normalized() * position.speed 34 | position.position += velocity * process_delta 35 | position.position = position.position.clamp(Vector2.ZERO, downlink_data.screen_size) 36 | # Assign the calculated data to the Godot node 37 | var movable_node = entity.get_component(Component_MovableNode.NAME) as Component_MovableNode 38 | if movable_node != null: 39 | movable_node.node.movement_set_data(position.position, position.direction) 40 | pass 41 | pass 42 | return downlink_data 43 | -------------------------------------------------------------------------------- /gdscript_ecs_test/ecs/systems/subsystem/System_MobAI.gd: -------------------------------------------------------------------------------- 1 | extends EcsSystemBase 2 | 3 | func _on_pre_update(uplink_data: Dictionary) -> Dictionary: 4 | var mob_entity_list = world.find_entity_list(Component_Mob.NAME) 5 | for mob_entity in mob_entity_list: 6 | var component_position = mob_entity.get_component(Component_Position.NAME) as Component_Position 7 | if component_position.direction == Vector2i.ZERO: 8 | component_position.direction = _random_direction() 9 | elif command("is_component_position_hits_wall").call( 10 | component_position, 11 | uplink_data.screen_size) != Vector2i.ZERO: 12 | component_position.direction = _random_direction() 13 | pass 14 | return uplink_data 15 | 16 | func _random_direction() -> Vector2i: 17 | return Vector2i(randi_range(-1, 1), randi_range(-1, 1)) 18 | -------------------------------------------------------------------------------- /gdscript_ecs_test/ecs/systems/subsystem/System_MobSpawner.gd: -------------------------------------------------------------------------------- 1 | extends EcsSystemBase 2 | 3 | func _on_before_ready() -> void: 4 | add_system_command("create_mob_entity", create_mob_entity) 5 | pass 6 | 7 | func create_mob_entity() -> EcsEntity: 8 | var mob_entity = world.create_entity() 9 | 10 | mob_entity.create_component(Component_Mob.NAME) 11 | 12 | var component_position = mob_entity.create_component(Component_Position.NAME) as Component_Position 13 | component_position.position = Vector2(50, 50) 14 | 15 | var component_movablenode = mob_entity.create_component(Component_MovableNode.NAME) as Component_MovableNode 16 | component_movablenode.node = command("create_mob_node").call() 17 | 18 | return mob_entity 19 | -------------------------------------------------------------------------------- /gdscript_ecs_test/ecs/systems/subsystem/System_PlayerController.gd: -------------------------------------------------------------------------------- 1 | ## 2 | ## Control player movement and other behaviors 3 | ## 4 | extends EcsSystemBase 5 | 6 | func _on_before_ready() -> void: 7 | add_system_command("create_player_entity", create_player_entity) 8 | add_system_command("set_player_direction", set_player_direction) 9 | restore_nodes_from_archive() 10 | pass 11 | 12 | func _on_pre_update(uplink_data: Dictionary) -> Dictionary: 13 | var player_entity = world.find_entity_first(Component_Player.NAME) 14 | if player_entity == null: 15 | return uplink_data 16 | var component_position = player_entity.get_component(Component_Position.NAME) as Component_Position 17 | var direction = Vector2i.ZERO # The player's movement vector. 18 | if Input.is_action_pressed("move_right"): 19 | direction.x += 1 20 | if Input.is_action_pressed("move_left"): 21 | direction.x -= 1 22 | if Input.is_action_pressed("move_down"): 23 | direction.y += 1 24 | if Input.is_action_pressed("move_up"): 25 | direction.y -= 1 26 | component_position.direction = direction 27 | return uplink_data 28 | 29 | func create_player_entity() -> EcsEntity: 30 | var player_entity = world.create_entity() 31 | 32 | player_entity.create_component(Component_Player.NAME) 33 | 34 | var component_position = player_entity.create_component(Component_Position.NAME) as Component_Position 35 | component_position.position = Vector2(200, 200) 36 | component_position.speed = 380 37 | 38 | var component_movablenode = player_entity.create_component(Component_MovableNode.NAME) as Component_MovableNode 39 | component_movablenode.node = get_command("create_player_node").call() 40 | 41 | return player_entity 42 | 43 | func set_player_direction(direction: Vector2i) -> void: 44 | assert(direction.x >= -1 and direction.x <= 1) 45 | assert(direction.y >= -1 and direction.y <= 1) 46 | var player_entity = world.find_entity_first(Component_Player.NAME) 47 | if player_entity == null: 48 | return 49 | var position = player_entity.get_component(Component_Position.NAME) as Component_Position 50 | assert(position != null) 51 | position.direction = direction 52 | pass 53 | 54 | func restore_nodes_from_archive() -> void: 55 | var player_entity = world.find_entity_first(Component_Player.NAME) 56 | if player_entity == null: 57 | return 58 | var movable_node = player_entity.get_component(Component_MovableNode.NAME) as Component_MovableNode 59 | assert(movable_node != null) 60 | if movable_node.node == null: 61 | var player_node = get_command("create_player_node").call() 62 | assert(player_node != null) 63 | movable_node.node = player_node 64 | pass 65 | -------------------------------------------------------------------------------- /gdscript_ecs_test/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /gdscript_ecs_test/icon.svg.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://bnq8wbfviveyr" 6 | path="res://.godot/imported/icon.svg-71387a6f9487126bb710bf67762b285b.ctex" 7 | metadata={ 8 | "vram_texture": false 9 | } 10 | 11 | [deps] 12 | 13 | source_file="res://gdscript_ecs_test/icon.svg" 14 | dest_files=["res://.godot/imported/icon.svg-71387a6f9487126bb710bf67762b285b.ctex"] 15 | 16 | [params] 17 | 18 | compress/mode=0 19 | compress/high_quality=false 20 | compress/lossy_quality=0.7 21 | compress/hdr_compression=1 22 | compress/normal_map=0 23 | compress/channel_pack=0 24 | mipmaps/generate=false 25 | mipmaps/limit=-1 26 | roughness/mode=0 27 | roughness/src_normal="" 28 | process/fix_alpha_border=true 29 | process/premult_alpha=false 30 | process/normal_map_invert_y=false 31 | process/hdr_as_srgb=false 32 | process/hdr_clamp_exposure=false 33 | process/size_limit=0 34 | detect_3d/compress_to=1 35 | svg/scale=1.0 36 | editor/scale_with_editor_scale=false 37 | editor/convert_colors_with_editor_theme=false 38 | -------------------------------------------------------------------------------- /gdscript_ecs_test/main.gd: -------------------------------------------------------------------------------- 1 | ## 2 | ## Main scene, the game starts from here 3 | ## 4 | extends Node 5 | 6 | ## The logical world of games 7 | var _world: EcsWorld 8 | 9 | ## Size of the game window. 10 | var _screen_size 11 | 12 | func _ready() -> void: 13 | _screen_size = $WorldContainer.get_viewport_rect().size 14 | _init_ecs() 15 | pass 16 | 17 | func _init_ecs() -> void: 18 | # Register all components 19 | var component_scanner = EcsComponentScanner.new() 20 | component_scanner.scan_script("res://gdscript_ecs_test/ecs/components/", "*.gd") 21 | # Register all systems 22 | var system_scanner = EcsSystemScanner.new() 23 | system_scanner.scan_script("res://gdscript_ecs_test/ecs/systems/", "*.gd") 24 | # Register events and commands 25 | var registrar_scanner = EcsRegistrarScanner.new() 26 | #registrar_scanner.add_registrar_node($ECSBinder_Player) 27 | # ECS World 28 | _world = EcsWorld.new(component_scanner, system_scanner, registrar_scanner) 29 | # Bind with Godot node 30 | _world.add_command("create_player_node", create_player_node) 31 | _world.add_command("create_mob_node", create_mob_node) 32 | # ECS Init all systems 33 | _world.init_system() 34 | 35 | # Test 36 | _world.get_command("create_player_entity").call() 37 | for i in 10: 38 | _world.get_command("create_mob_entity").call() 39 | pass 40 | 41 | func _process(delta: float) -> void: 42 | _world.update({ 43 | process_delta = delta, 44 | screen_size = _screen_size, 45 | }) 46 | 47 | func create_player_node(): 48 | var Scene_Player = load("res://gdscript_ecs_test/node/Player.tscn") 49 | var player_node = Scene_Player.instantiate() 50 | player_node._world = _world 51 | $WorldContainer.add_child(player_node) 52 | return player_node 53 | 54 | func create_mob_node(): 55 | var Scene_Mob = load("res://gdscript_ecs_test/node/Mob.tscn") 56 | var mob_node = Scene_Mob.instantiate() 57 | mob_node._world = _world 58 | $WorldContainer.add_child(mob_node) 59 | return mob_node 60 | -------------------------------------------------------------------------------- /gdscript_ecs_test/main.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=2 format=3 uid="uid://34vy80v084lp"] 2 | 3 | [ext_resource type="Script" path="res://gdscript_ecs_test/main.gd" id="1_vu71a"] 4 | 5 | [node name="Main" type="Node"] 6 | script = ExtResource("1_vu71a") 7 | 8 | [node name="WorldContainer" type="Node2D" parent="."] 9 | -------------------------------------------------------------------------------- /gdscript_ecs_test/node/Mob.gd: -------------------------------------------------------------------------------- 1 | extends RigidBody2D 2 | 3 | var _ref_world: WeakRef = null 4 | var _world: EcsWorld: 5 | get: 6 | if _ref_world == null: 7 | return null 8 | return _ref_world.get_ref() 9 | set(world): 10 | _ref_world = weakref(world) 11 | 12 | ## The caller is: System_Movement.gd::_on_update() 13 | func movement_set_data(data_position: Vector2, _direction: Vector2i) -> void: 14 | # Set the node's own position 15 | position = data_position 16 | -------------------------------------------------------------------------------- /gdscript_ecs_test/node/Mob.gdshader: -------------------------------------------------------------------------------- 1 | shader_type canvas_item; 2 | 3 | uniform int pixelSize = 4; 4 | 5 | void fragment() 6 | { 7 | 8 | ivec2 size = textureSize(TEXTURE, 0); 9 | 10 | int xRes = size.x; 11 | int yRes = size.y; 12 | 13 | float xFactor = float(xRes) / float(pixelSize); 14 | float yFactor = float(yRes) / float(pixelSize); 15 | 16 | float grid_uv_x = round(UV.x * xFactor) / xFactor; 17 | float grid_uv_y = round(UV.y * yFactor) / yFactor; 18 | 19 | vec4 text = texture(TEXTURE, vec2(grid_uv_x, grid_uv_y)); 20 | 21 | COLOR = text; 22 | } 23 | -------------------------------------------------------------------------------- /gdscript_ecs_test/node/Mob.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=6 format=3 uid="uid://bnos07563muio"] 2 | 3 | [ext_resource type="Shader" path="res://gdscript_ecs_test/node/Mob.gdshader" id="1_0ojmp"] 4 | [ext_resource type="Script" path="res://gdscript_ecs_test/node/Mob.gd" id="1_67gcy"] 5 | [ext_resource type="Texture2D" uid="uid://bnq8wbfviveyr" path="res://gdscript_ecs_test/icon.svg" id="1_ylkhh"] 6 | 7 | [sub_resource type="ShaderMaterial" id="ShaderMaterial_63vox"] 8 | shader = ExtResource("1_0ojmp") 9 | shader_parameter/pixelSize = 5 10 | 11 | [sub_resource type="RectangleShape2D" id="RectangleShape2D_qsg6n"] 12 | size = Vector2(128, 128) 13 | 14 | [node name="Mob" type="RigidBody2D"] 15 | collision_mask = 0 16 | gravity_scale = 0.0 17 | script = ExtResource("1_67gcy") 18 | 19 | [node name="Icon" type="Sprite2D" parent="."] 20 | material = SubResource("ShaderMaterial_63vox") 21 | texture = ExtResource("1_ylkhh") 22 | 23 | [node name="CollisionShape2D" type="CollisionShape2D" parent="."] 24 | visible = false 25 | shape = SubResource("RectangleShape2D_qsg6n") 26 | -------------------------------------------------------------------------------- /gdscript_ecs_test/node/Player.gd: -------------------------------------------------------------------------------- 1 | extends Area2D 2 | 3 | @onready var icon: Sprite2D = $Icon 4 | 5 | ## Size of the game window. 6 | var _screen_size 7 | 8 | var _ref_world: WeakRef = null 9 | var _world: EcsWorld: 10 | get: 11 | if _ref_world == null: 12 | return null 13 | return _ref_world.get_ref() 14 | set(world): 15 | _ref_world = weakref(world) 16 | 17 | ## The caller is: System_Movement.gd::_on_update() 18 | func movement_set_data(data_position: Vector2, direction: Vector2i) -> void: 19 | # Set the node's own position 20 | position = data_position 21 | # Using shaders to change the direction of the image 22 | var shader := icon.material as ShaderMaterial 23 | shader.set_shader_parameter("pitch", direction.x * 0.3) 24 | shader.set_shader_parameter("roll", -direction.y * 0.3) 25 | pass 26 | 27 | func get_player_direction() -> Vector2i: 28 | var direction = Vector2i.ZERO 29 | if Input.is_action_pressed("move_right"): 30 | direction.x += 1 31 | if Input.is_action_pressed("move_left"): 32 | direction.x -= 1 33 | if Input.is_action_pressed("move_down"): 34 | direction.y += 1 35 | if Input.is_action_pressed("move_up"): 36 | direction.y -= 1 37 | return direction 38 | -------------------------------------------------------------------------------- /gdscript_ecs_test/node/Player.gdshader: -------------------------------------------------------------------------------- 1 | // Hey this is Hei! This shader "fakes" a 3D-camera perspective on CanvasItems. 2 | // Known limitations: 3 | // - Yaw doesn't work as intended with non-square images. 4 | // License: MIT 5 | 6 | 7 | shader_type canvas_item; 8 | 9 | // Camera FOV (half) in radians 10 | // You can also consider pre-calculating "0.5 / tan(fov)" 11 | uniform float fov = 0.785398; 12 | // How far the image plane is from camera 13 | uniform float plane_distance = 1.0; 14 | 15 | uniform float yaw : hint_range(-1.57, 1.57) = 0.0; 16 | uniform float pitch : hint_range(-1.57, 1.57) = 0.0; 17 | uniform float roll : hint_range(-1.57, 1.57) = 0.0; 18 | 19 | // Consider changing this to a uniform and change it from code 20 | varying mat3 rotmat; 21 | 22 | // Creates rotation matrix 23 | void vertex(){ 24 | float cos_a = cos(yaw); 25 | float sin_a = sin(yaw); 26 | float sin_b = sin(pitch); 27 | float cos_b = cos(pitch); 28 | float sin_c = sin(roll); 29 | float cos_c = cos(roll); 30 | 31 | rotmat[0][0] = cos_a * cos_b; 32 | rotmat[0][1] = cos_a * sin_b * sin_c - sin_a * cos_c; 33 | rotmat[0][2] = cos_a * sin_b * cos_c + sin_a * sin_c; 34 | 35 | rotmat[1][0] = sin_a * cos_b; 36 | rotmat[1][1] = sin_a * sin_b * sin_c + cos_a * cos_c; 37 | rotmat[1][2] = sin_a * sin_b * cos_c - cos_a * sin_c; 38 | 39 | rotmat[2][0] = -sin_b; 40 | rotmat[2][1] = cos_b * sin_c; 41 | rotmat[2][2] = cos_b * cos_c; 42 | } 43 | 44 | // Projects UV to an imaginary plane; I barely have clue myself how it works. 45 | void fragment(){ 46 | vec3 from = vec3(0, 0, -plane_distance); 47 | vec3 normal = rotmat * vec3(0.0, 0.0, 1.0); 48 | vec3 segment = normalize(vec3(UV - 0.5, 0.5 / tan(fov))); 49 | float dist = -dot(normal, from) / dot(normal, segment); 50 | vec3 intersection = from + segment * dist; 51 | vec2 uv = (inverse(rotmat) * intersection).xy + 0.5; 52 | if (any(greaterThan(abs(uv - 0.5), vec2(0.5)))) discard; 53 | COLOR = texture(TEXTURE, uv); 54 | } 55 | -------------------------------------------------------------------------------- /gdscript_ecs_test/node/Player.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=6 format=3 uid="uid://byhjva5t6lguq"] 2 | 3 | [ext_resource type="Texture2D" uid="uid://bnq8wbfviveyr" path="res://gdscript_ecs_test/icon.svg" id="1_6bjee"] 4 | [ext_resource type="Script" path="res://gdscript_ecs_test/node/Player.gd" id="1_ds0vu"] 5 | [ext_resource type="Shader" path="res://gdscript_ecs_test/node/Player.gdshader" id="3_5k36o"] 6 | 7 | [sub_resource type="ShaderMaterial" id="ShaderMaterial_5hc38"] 8 | shader = ExtResource("3_5k36o") 9 | shader_parameter/fov = 10.0 10 | shader_parameter/plane_distance = 1.0 11 | shader_parameter/yaw = 0.0 12 | shader_parameter/pitch = 0.0 13 | shader_parameter/roll = 2.21229e-08 14 | 15 | [sub_resource type="RectangleShape2D" id="RectangleShape2D_lgndw"] 16 | size = Vector2(100, 100) 17 | 18 | [node name="Player" type="Area2D"] 19 | script = ExtResource("1_ds0vu") 20 | 21 | [node name="Icon" type="Sprite2D" parent="."] 22 | material = SubResource("ShaderMaterial_5hc38") 23 | texture = ExtResource("1_6bjee") 24 | metadata/_edit_lock_ = true 25 | 26 | [node name="CollisionShape2D" type="CollisionShape2D" parent="."] 27 | shape = SubResource("RectangleShape2D_lgndw") 28 | metadata/_edit_lock_ = true 29 | --------------------------------------------------------------------------------