lib/felecs/component_manager.rb
# 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|
unless value.equal? send(attr)
instance_variable_set("@#{attr}", value)
attr_changed_trigger_systems(attr)
end
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 +@component.var = 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