diff options
Diffstat (limited to 'lib/felecs/component_manager.rb')
| -rw-r--r-- | lib/felecs/component_manager.rb | 277 |
1 files changed, 277 insertions, 0 deletions
diff --git a/lib/felecs/component_manager.rb b/lib/felecs/component_manager.rb new file mode 100644 index 0000000..36916cb --- /dev/null +++ b/lib/felecs/component_manager.rb @@ -0,0 +1,277 @@ +# frozen_string_literal: true + +module FelECS + module Components + @component_map = [] + class << self + # Creates a new {FelECS::ComponentManager component manager}. + # + # @example + # # Here color is set to default to red + # # while max and current are nil until set. + # # When you make a new component using this component manager + # # these are the values and accessors it will have. + # FelECS::Component.new('Health', :max, :current, color: 'red') + # + # @param component_name [String] Name of your new component manager. Must be stylized in the format of constants in Ruby + # @param attrs [:Symbols] New components made with this manager will include these symbols as accessors, the values of these accessors will default to nil + # @param attrs_with_defaults [Keyword: DefaultValue] New components made with this manager will include these keywords as accessors, their defaults set to the values given to the keywords + # @return [ComponentManager] + def new(component_name, *attrs, **attrs_with_defaults) + if FelECS::Components.const_defined?(component_name) + raise(NameError.new, "Component Manager '#{component_name}' is already defined") + end + + const_set(component_name, Class.new(FelECS::ComponentManager) {}) + update_const_cache + + attrs.each do |attr| + if FelECS::Components.const_get(component_name).method_defined?(attr.to_s) || FelECS::Components.const_get(component_name).method_defined?("#{attr}=") + raise NameError, "The attribute name \"#{attr}\" is already a method" + end + + FelECS::Components.const_get(component_name).attr_accessor attr + end + attrs_with_defaults.each do |attr, _default| + attrs_with_defaults[attr] = _default.dup + FelECS::Components.const_get(component_name).attr_reader attr + FelECS::Components.const_get(component_name).define_method("#{attr}=") do |value| + attr_changed_trigger_systems(attr) unless value.equal? send(attr) + instance_variable_set("@#{attr}", value) + end + end + FelECS::Components.const_get(component_name).define_method(:set_defaults) do + attrs_with_defaults.each do |attr, default| + instance_variable_set("@#{attr}", default.dup) + end + end + FelECS::Components.const_get(component_name) + end + + # Stores the components managers in {FelECS::Components}. This + # is needed because calling `FelECS::Components.constants` + # will not let you iterate over the value of the constants + # but will instead give you an array of symbols. This caches + # the convertion of those symbols to the actual value of the + # constants + # @!visibility private + def const_cache + @const_cache || update_const_cache + end + + # Updates the array that stores the constants. + # Used internally by FelECS + # @!visibility private + def update_const_cache + @const_cache = constants.map do |constant| + const_get constant + end + end + + # Forwards undefined methods to the array of constants + # if the array can handle the request. Otherwise tells + # the programmer their code errored + # @!visibility private + def respond_to_missing?(method, *) + if const_cache.respond_to? method + true + else + super + end + end + + # Makes component module behave like arrays with additional + # methods for managing the array + # @!visibility private + def method_missing(method, *args, **kwargs, &block) + if const_cache.respond_to? method + const_cache.send(method, *args, **kwargs, &block) + else + super + end + end + end + end + + # Component Managers are what is used to create individual components which can be attached to entities. + # When a Component is created from a Component Manager that has accessors given to it, you can set or get the values of those accessors using standard ruby message sending (e.g [email protected] = 5+), or by using the {#to_h} and {#update_attrs} methods instead. + class ComponentManager + # Allows overwriting the storage of triggers, such as for clearing. + # This method should generally only need to be used internally and + # not by a game developer. + # @!visibility private + attr_writer :addition_triggers, :removal_triggers, :attr_triggers + + # Stores references to systems that should be triggered when a + # component from this manager is added. + # Do not edit this array as it is managed by FelECS automatically. + # @return [Array<System>] + def addition_triggers + @addition_triggers ||= [] + end + + # Stores references to systems that should be triggered when a + # component from this manager is removed. + # Do not edit this array as it is managed by FelECS automatically. + # @return [Array<System>] + def removal_triggers + @removal_triggers ||= [] + end + + # Stores references to systems that should be triggered when an + # attribute from this manager is changed. + # Do not edit this hash as it is managed by FelECS automatically. + # @return [Hash<Symbol, Array<System>>] + def attr_triggers + @attr_triggers ||= {} + end + + # Creates a new component and sets the values of the attributes given to it. If an attritbute is not passed then it will remain as the default. + # @param attrs [Keyword: Value] You can pass any number of Keyword-Value pairs + # @return [Component] + def initialize(**attrs) + # Prepare the object + # (this is a function created with metaprogramming + # in FelECS::Components) + set_defaults + + # Fill params + attrs.each do |key, value| + send "#{key}=", value + end + + # Save Component + self.class.push self + end + + class << self + # Makes component managers behave like arrays with additional + # methods for managing the array + # @!visibility private + def respond_to_missing?(method, *) + if _data.respond_to? method + true + else + super + end + end + + # Makes component managers behave like arrays with additional + # methods for managing the array + # @!visibility private + def method_missing(method, *args, **kwargs, &block) + if _data.respond_to? method + _data.send(method, *args, **kwargs, &block) + else + super + end + end + + # Allows overwriting the storage of triggers, such as for clearing. + # This method should generally only need to be used internally and + # not by a game developer. + # @!visibility private + attr_writer :addition_triggers, :removal_triggers, :attr_triggers + + # Stores references to systems that should be triggered when this + # component is added to an enitity. + # Do not edit this array as it is managed by FelECS automatically. + # @return [Array<System>] + def addition_triggers + @addition_triggers ||= [] + end + + # Stores references to systems that should be triggered when this + # component is removed from an enitity. + # Do not edit this array as it is managed by FelECS automatically. + # @return [Array<System>] + def removal_triggers + @removal_triggers ||= [] + end + + # Stores references to systems that should be triggered when an + # attribute from this component changed. + # Do not edit this hash as it is managed by FelECS automatically. + # @return [Hash<Symbol, System>] + def attr_triggers + @attr_triggers ||= {} + end + + # @return [Array<Component>] Array of all Components that belong to a given component manager + # @!visibility private + def _data + @data ||= [] + end + end + + # Entities that have this component + # @return [Array<Component>] + def entities + @entities ||= [] + end + + # A single entity. Use this if you expect the component to only belong to one entity and you want to access it. + # @return [Component] + def entity + if entities.empty? + Warning.warn("This component belongs to NO entities but you called the method that is intended for components belonging to a single entity.\nYou may have a bug in your logic.") + elsif entities.length > 1 + Warning.warn("This component belongs to MANY entities but you called the method that is intended for components belonging to a single entity.\nYou may have a bug in your logic.") + end + entities.first + end + + # Update attribute values using a hash or keywords. + # @return [Hash<Symbol, Value>] Hash of updated attributes + def update_attrs(**opts) + opts.each do |key, value| + send "#{key}=", value + end + end + + # Execute systems that have been added to execute on variable change + # @return [Boolean] +true+ + # @!visibility private + def attr_changed_trigger_systems(attr) + systems_to_execute = self.class.attr_triggers[attr] + systems_to_execute = [] if systems_to_execute.nil? + + systems_to_execute |= attr_triggers[attr] unless attr_triggers[attr].nil? + + systems_to_execute.sort_by(&:priority).reverse_each(&:call) + true + end + + # Removes this component from the list and purges all references to this Component from other Entities, as well as its data. + # @return [Boolean] +true+. + def delete + addition_triggers.each do |system| + system.clear_triggers component_or_manager: self + end + entities.reverse_each do |entity| + entity.remove self + end + self.class._data.delete self + instance_variables.each do |var| + instance_variable_set(var, nil) + end + true + end + + # @return [Hash<Symbol, Value>] A hash, where all the keys are attributes storing their respective values. + def to_h + return_hash = instance_variables.each_with_object({}) do |key, final| + final[key.to_s.delete_prefix('@').to_sym] = instance_variable_get(key) + end + return_hash.delete(:attr_triggers) + return_hash + end + + # Export all data into a JSON String, which could then later be loaded or saved to a file + # TODO: This function is not yet complete + # @return [String] a JSON formatted String + # def to_json + # # should return a json or hash of all data in this component + # end + end +end |
