class Chione::World
The main ECS container
Attributes
The Hash of Hashes of Components which have been added to an Entity, keyed by the Entity's ID and the Component class.
The queue of events that have not yet been sent to subscribers.
The Hash of all Entities in the World
, keyed by ID
The Hash of Sets of Entities which have a particular component, keyed by Component class.
The Thread object running the World's IO reactor loop
The Hash of all Managers currently in the World
, keyed by class.
The Hash of event subscription callbacks registered with the world, keyed by event pattern.
The Hash of all Systems currently in the World
, keyed by class.
The number of times the event loop has executed.
The ThreadGroup that contains all Threads managed by the World
.
Public Class Methods
Create a new Chione::World
# File lib/chione/world.rb, line 43 def initialize @entities = {} @systems = {} @managers = {} @subscriptions = Hash.new {|h,k| h[k] = Set.new } @defer_events = true @deferred_events = [] @main_thread = nil @world_threads = ThreadGroup.new @entities_by_component = Hash.new {|h,k| h[k] = Set.new } @components_by_entity = Hash.new {|h, k| h[k] = {} } @tick_count = 0 end
Public Instance Methods
Call the specified callback
with the provided event_name
and payload
, returning true
if the callback executed without error.
# File lib/chione/world.rb, line 302 def call_subscription_callback( callback, event_name, payload ) callback.call( event_name, payload ) return true rescue => err self.log.error "%p while calling %p for a %p event: %s" % [ err.class, callback, event_name, err.message ] self.log.debug " %s" % [ err.backtrace.join("\n ") ] return false end
Call the callbacks of any subscriptions matching the specified event_name
with the given payload
.
# File lib/chione/world.rb, line 286 def call_subscription_callbacks( event_name, payload ) self.subscriptions.each do |pattern, callbacks| next unless File.fnmatch?( pattern, event_name, File::FNM_EXTGLOB|File::FNM_PATHNAME ) callbacks.each do |callback| unless self.call_subscription_callback( callback, event_name, payload ) self.log.debug "Callback failed; removing it from the subscription." self.unsubscribe( callback ) end end end end
Whether or not to queue published events instead of sending them to subscribers immediately.
# File lib/chione/world.rb, line 108 attr_predicate_accessor :defer_events
Kill the threads other than the main thread in the world's thread list.
# File lib/chione/world.rb, line 211 def kill_world_threads self.log.info "Killing child threads." self.world_threads.list.each do |thr| next if thr == @main_thread self.log.debug " killing: %p" % [ thr ] thr.join( Chione::World.max_stop_wait ) end end
Publish an event with the specified event_name
and payload
.
# File lib/chione/world.rb, line 264 def publish( event_name, *payload ) # self.log.debug "Publishing a %p event: %p" % [ event_name, payload ] if self.defer_events? self.deferred_events.push( [event_name, payload] ) else self.call_subscription_callbacks( event_name, payload ) end end
Send any deferred events to subscribers.
# File lib/chione/world.rb, line 275 def publish_deferred_events self.log.debug "Publishing %d deferred events" % [ self.deferred_events.length ] unless self.deferred_events.empty? while event = self.deferred_events.shift self.call_subscription_callbacks( *event ) end end
Start the world; returns the Thread in which the world is running.
# File lib/chione/world.rb, line 128 def start @main_thread = Thread.new do Thread.current.abort_on_exception = true Thread.current.name = "Main World" self.log.info "Main thread (%p) started." % [ Thread.current ] @world_threads.add( Thread.current ) @world_threads.enclose self.start_managers self.start_systems self.timing_loop end self.log.info "Started main World thread: %p" % [ @main_thread ] return @main_thread end
Start any Managers registered with the world.
# File lib/chione/world.rb, line 157 def start_managers self.log.info "Starting %d Managers" % [ self.managers.length ] self.managers.each do |manager_class, mgr| self.log.debug " starting %p" % [ manager_class ] start = Time.now mgr.start finish = Time.now self.log.debug " started in %0.5fs" % [ finish - start ] end end
Start any Systems registered with the world.
# File lib/chione/world.rb, line 177 def start_systems self.log.info "Starting %d Systems" % [ self.systems.length ] self.systems.each do |system_class, sys| injections = self.make_injection_hash_for( system_class ) self.log.debug " starting %p" % [ system_class ] start = Time.now sys.start( **injections ) finish = Time.now self.log.debug " started in %0.5fs" % [ finish - start ] end end
Returns true
if the World
has been started (but is not necessarily running yet).
# File lib/chione/world.rb, line 199 def started? return @main_thread && @main_thread.alive? end
Return a Hash of information about the world suitable for display in tools.
# File lib/chione/world.rb, line 117 def status return { versions: { chione: Chione::VERSION }, tick: self.tick_count, systems: self.systems.keys.map( &:name ), managers: self.managers.keys.map( &:name ) } end
Stop the world.
# File lib/chione/world.rb, line 222 def stop self.stop_systems self.stop_managers self.kill_world_threads self.stop_timing_loop end
Stop any Managers running in the world.
# File lib/chione/world.rb, line 170 def stop_managers self.log.info "Stopping managers." self.managers.each {|_, mgr| mgr.stop } end
Stop any Systems running in the world.
# File lib/chione/world.rb, line 192 def stop_systems self.log.info "Stopping systems." self.systems.each {|_, sys| sys.stop } end
Halt the main timing loop. By default, this just kills the world's main thread.
# File lib/chione/world.rb, line 231 def stop_timing_loop self.log.info "Stopping the timing loop." @main_thread.kill end
Subscribe to events with the specified event_name
. Returns the callback object for later unsubscribe calls.
# File lib/chione/world.rb, line 239 def subscribe( event_name, callback=nil, &block ) callback ||= block raise LocalJumpError, "no callback given" unless callback raise ArgumentError, "callback is not callable" unless callback.respond_to?( :call ) raise ArgumentError, "callback has wrong arity" unless callback.arity >= 2 || callback.arity < 0 self.subscriptions[ event_name ].add( callback ) return callback end
Step the world delta_seconds
into the future.
# File lib/chione/world.rb, line 148 def tick( delta_seconds=1.0/60.0 ) self.publish( 'timing', delta_seconds, self.tick_count ) self.publish_deferred_events self.tick_count += 1 end
Unsubscribe from events that publish to the specified callback
.
# File lib/chione/world.rb, line 254 def unsubscribe( callback ) self.subscriptions.keys.each do |pattern| cbset = self.subscriptions[ pattern ] cbset.delete( callback ) self.subscriptions.delete( pattern ) if cbset.empty? end end
Protected Instance Methods
Return a Hash of the loaded Chione::Systems that system_class
has requested be injected into it.
# File lib/chione/world.rb, line 522 def make_injection_hash_for( system_class ) self.log.debug "Injecting %d other system/s into %p" % [ system_class.injected_systems.length, system_class ] return system_class.injected_systems.each_with_object({}) do |(name, injected_class), hash| self.log.debug " inject %p: %p" % [ name, injected_class ] system = self.systems[ injected_class ] or raise "Can't inject %p into %p: not configured to run it" % [ injected_class, system_class] hash[ name ] = system end end
The loop the main thread executes after the world is started. The default implementation just broadcasts the timing
event, so you will likely want to override this if the main thread should do something else.
# File lib/chione/world.rb, line 548 def timing_loop last_timing_event = Time.now interval = Chione::World.timing_event_interval self.defer_events = false self.tick_count = 0 self.log.info "Starting timing loop with interval = %0.3fs." % [ interval ] loop do previous_time, last_timing_event = last_timing_event, Time.now self.tick( last_timing_event - previous_time ) remaining_time = interval - (Time.now - last_timing_event) if remaining_time > 0 sleep( remaining_time ) else self.log.warn "Timing loop %d exceeded `timing_event_interval` (by %0.6fs)" % [ self.tick_count, remaining_time.abs ] end end ensure self.log.info "Exiting timing loop." end
Update any entity caches in the system when an entity
has its components
hash changed.
# File lib/chione/world.rb, line 536 def update_entity_caches( entity, components ) entity = entity.id if entity.respond_to?( :id ) self.log.debug " updating entity cache for %p" % [ entity ] self.systems.each_value do |sys| sys.entity_components_updated( entity, components ) end end
Component API
↑ topPublic Instance Methods
Add the specified component
to the specified entity
.
# File lib/chione/world.rb, line 370 def add_component_to( entity, component, **init_values ) entity = entity.id if entity.respond_to?( :id ) component = Chione::Component( component, init_values ) component.entity_id = entity self.log.debug "Adding %p for %p" % [ component.class, entity ] self.entities_by_component[ component.class ].add( entity ) component_hash = self.components_by_entity[ entity ] component_hash[ component.class ] = component self.update_entity_caches( entity, component_hash ) end
Return a Hash of the Component instances associated with entity
, keyed by their class.
# File lib/chione/world.rb, line 387 def components_for( entity ) entity = entity.id if entity.respond_to?( :id ) return self.components_by_entity[ entity ].dup end
Return the Component instance of the specified component_class
that's associated with the given entity
, if it has one.
# File lib/chione/world.rb, line 395 def get_component_for( entity, component_class ) entity = entity.id if entity.respond_to?( :id ) return self.components_by_entity[ entity ][ component_class ] end
Return true
if the specified entity
has the given component
. If component
is a Component subclass, any instance of it will test true
. If component
is a Component instance, it will only test true
if the entity
is associated with that particular instance.
# File lib/chione/world.rb, line 424 def has_component_for?( entity, component ) entity = entity.id if entity.respond_to?( :id ) if component.is_a?( Class ) return self.components_by_entity[ entity ].key?( component ) else return self.components_by_entity[ entity ][ component.class ] == component end end
Remove the specified component
from the given entity
. If component
is a Component subclass, any instance of it will be removed. If it's a Component instance, it will be removed iff it is the same instance associated with the given entity
.
# File lib/chione/world.rb, line 405 def remove_component_from( entity, component ) entity = entity.id if entity.respond_to?( :id ) if component.is_a?( Class ) self.entities_by_component[ component ].delete( entity ) component_hash = self.components_by_entity[ entity ] component_hash.delete( component ) self.update_entity_caches( entity, component_hash ) else self.remove_component_from( entity, component.class ) if self.has_component_for?( entity, component ) end end
Entity API
↑ topPublic Instance Methods
Return a new Chione::Entity
with no components for the receiving world. Override this if you wish to use a class other than Chione::Entity
for your world.
# File lib/chione/world.rb, line 336 def create_blank_entity return Chione::Entity.new( self ) end
Return a new Chione::Entity
for the receiving World
, using the optional archetype
to populate it with components if it's specified.
# File lib/chione/world.rb, line 319 def create_entity( archetype=nil ) entity = if archetype archetype.construct_for( self ) else self.create_blank_entity end @entities[ entity.id ] = entity self.publish( 'entity/created', entity.id ) return entity end
Destroy the specified entity and remove it from any registered systems/managers.
# File lib/chione/world.rb, line 343 def destroy_entity( entity ) raise ArgumentError, "%p does not contain entity %p" % [ self, entity ] unless self.has_entity?( entity ) self.publish( 'entity/destroyed', entity ) self.entities_by_component.each_value {|set| set.delete(entity.id) } self.components_by_entity.delete( entity.id ) @entities.delete( entity.id ) end
Returns true
if the world contains the specified entity
or an entity with entity
as the ID.
# File lib/chione/world.rb, line 356 def has_entity?( entity ) if entity.respond_to?( :id ) return @entities.key?( entity.id ) else return @entities.key?( entity ) end end
Manager API
↑ topPublic Instance Methods
Add an instance of the specified manager_type
to the world and return it. It will replace any existing manager of the same type.
# File lib/chione/world.rb, line 483 def add_manager( manager_type, *args ) manager_obj = manager_type.new( self, *args ) self.managers[ manager_type ] = manager_obj if self.running? self.log.info "Starting %p added to running world." % [ manager_type ] manager_obj.start end self.publish( 'manager/added', manager_obj ) return manager_obj end
Remove the instance of the specified manager_type
from the world and return it if it's been added. Returns nil
if no instance of the specified manager_type
was added.
# File lib/chione/world.rb, line 500 def remove_manager( manager_type ) manager_obj = self.managers.delete( manager_type ) or return nil self.publish( 'manager/removed', manager_obj ) if self.running? self.log.info "Stopping %p removed from running world." % [ manager_type ] manager_obj.stop end return manager_obj end
System API
↑ topPublic Instance Methods
Add an instance of the specified system_type
to the world and return it. It will replace any existing system of the same type.
# File lib/chione/world.rb, line 440 def add_system( system_type, *args ) system_obj = system_type.new( self, *args ) self.systems[ system_type ] = system_obj if self.running? self.log.info "Starting %p added to running world." % [ system_type ] system_obj.start end self.publish( 'system/added', system_obj ) return system_obj end
Return an Array of all entities that match the specified aspect
.
# File lib/chione/world.rb, line 472 def entities_with( aspect ) return aspect.matching_entities( self.entities_by_component ) end
Remove the instance of the specified system_type
from the world and return it if it's been added. Returns nil
if no instance of the specified system_type
was added.
# File lib/chione/world.rb, line 457 def remove_system( system_type ) system_obj = self.systems.delete( system_type ) or return nil self.publish( 'system/removed', system_obj ) if self.running? self.log.info "Stopping %p before being removed from runnning world." % [ system_type ] system_obj.stop end return system_obj end