Chione::

World class

The main ECS container

Attributes

components_by_entity R

The Hash of Hashes of Components which have been added to an Entity, keyed by the Entity’s ID and the Component class.

deferred_events R

The queue of events that have not yet been sent to subscribers.

entities R

The Hash of all Entities in the World, keyed by ID

entities_by_component R

The Hash of Sets of Entities which have a particular component, keyed by Component class.

main_thread R

The Thread object running the World’s IO reactor loop

managers R

The Hash of all Managers currently in the World, keyed by class.

subscriptions R

The Hash of event subscription callbacks registered with the world, keyed by event pattern.

systems R

The Hash of all Systems currently in the World, keyed by class.

tick_count RW

The number of times the event loop has executed.

world_threads R

The ThreadGroup that contains all Threads managed by the World.

Public Class Methods

new()

Create a new Chione::World

   # File lib/chione/world.rb
42 def initialize
43     @entities      = {}
44     @systems       = {}
45     @managers      = {}
46 
47     @subscriptions = Hash.new {|h,k| h[k] = Set.new }
48     @defer_events  = true
49     @deferred_events = []
50 
51     @main_thread   = nil
52     @world_threads = ThreadGroup.new
53 
54     @entities_by_component = Hash.new {|h,k| h[k] = Set.new }
55     @components_by_entity = Hash.new {|h, k| h[k] = {} }
56 
57     @tick_count = 0
58 end

Public Instance Methods

call_subscription_callback( callback, event_name, payload )

Call the specified callback with the provided event_name and payload, returning true if the callback executed without error.

    # File lib/chione/world.rb
301 def call_subscription_callback( callback, event_name, payload )
302     callback.call( event_name, payload )
303     return true
304 rescue => err
305     self.log.error "%p while calling %p for a %p event: %s" %
306         [ err.class, callback, event_name, err.message ]
307     self.log.debug "  %s" % [ err.backtrace.join("\n  ") ]
308     return false
309 end
call_subscription_callbacks( event_name, payload )

Call the callbacks of any subscriptions matching the specified event_name with the given payload.

    # File lib/chione/world.rb
285 def call_subscription_callbacks( event_name, payload )
286     self.subscriptions.each do |pattern, callbacks|
287         next unless File.fnmatch?( pattern, event_name, File::FNM_EXTGLOB|File::FNM_PATHNAME )
288 
289         callbacks.each do |callback|
290             unless self.call_subscription_callback( callback, event_name, payload )
291                 self.log.debug "Callback failed; removing it from the subscription."
292                 self.unsubscribe( callback )
293             end
294         end
295     end
296 end
defer_events()

Whether or not to queue published events instead of sending them to subscribers immediately.

    # File lib/chione/world.rb
107 attr_predicate_accessor :defer_events
kill_world_threads()

Kill the threads other than the main thread in the world’s thread list.

    # File lib/chione/world.rb
210 def kill_world_threads
211     self.log.info "Killing child threads."
212     self.world_threads.list.each do |thr|
213         next if thr == @main_thread
214         self.log.debug "  killing: %p" % [ thr ]
215         thr.join( Chione::World.max_stop_wait )
216     end
217 end
publish( event_name, *payload )

Publish an event with the specified event_name and payload.

    # File lib/chione/world.rb
263 def publish( event_name, *payload )
264     # self.log.debug "Publishing a %p event: %p" % [ event_name, payload ]
265     if self.defer_events?
266         self.deferred_events.push( [event_name, payload] )
267     else
268         self.call_subscription_callbacks( event_name, payload )
269     end
270 end
publish_deferred_events()

Send any deferred events to subscribers.

    # File lib/chione/world.rb
274 def publish_deferred_events
275     self.log.debug "Publishing %d deferred events" % [ self.deferred_events.length ] unless
276         self.deferred_events.empty?
277     while event = self.deferred_events.shift
278         self.call_subscription_callbacks( *event )
279     end
280 end
running?()

Returns true if the World is running (i.e., if #start has been called)

    # File lib/chione/world.rb
204 def running?
205     return self.started? && self.tick_count.nonzero?
206 end
start()

Start the world; returns the Thread in which the world is running.

    # File lib/chione/world.rb
127 def start
128     @main_thread = Thread.new do
129         Thread.current.abort_on_exception = true
130         Thread.current.name = "Main World"
131         self.log.info "Main thread (%p) started." % [ Thread.current ]
132         @world_threads.add( Thread.current )
133         @world_threads.enclose
134 
135         self.start_managers
136         self.start_systems
137 
138         self.timing_loop
139     end
140 
141     self.log.info "Started main World thread: %p" % [ @main_thread ]
142     return @main_thread
143 end
start_managers()

Start any Managers registered with the world.

    # File lib/chione/world.rb
156 def start_managers
157     self.log.info "Starting %d Managers" % [ self.managers.length ]
158     self.managers.each do |manager_class, mgr|
159         self.log.debug "  starting %p" % [ manager_class ]
160         start = Time.now
161         mgr.start
162         finish = Time.now
163         self.log.debug "  started in %0.5fs" % [ finish - start ]
164     end
165 end
start_systems()

Start any Systems registered with the world.

    # File lib/chione/world.rb
176 def start_systems
177     self.log.info "Starting %d Systems" % [ self.systems.length ]
178     self.systems.each do |system_class, sys|
179         injections = self.make_injection_hash_for( system_class )
180 
181         self.log.debug "  starting %p" % [ system_class ]
182         start = Time.now
183         sys.start( **injections )
184         finish = Time.now
185         self.log.debug "  started in %0.5fs" % [ finish - start ]
186     end
187 end
started?()

Returns true if the World has been started (but is not necessarily running yet).

    # File lib/chione/world.rb
198 def started?
199     return @main_thread && @main_thread.alive?
200 end
status()

Return a Hash of information about the world suitable for display in tools.

    # File lib/chione/world.rb
116 def status
117     return {
118         versions: { chione: Chione::VERSION },
119         tick: self.tick_count,
120         systems: self.systems.keys.map( &:name ),
121         managers: self.managers.keys.map( &:name )
122     }
123 end
stop()

Stop the world.

    # File lib/chione/world.rb
221 def stop
222     self.stop_systems
223     self.stop_managers
224     self.kill_world_threads
225     self.stop_timing_loop
226 end
stop_managers()

Stop any Managers running in the world.

    # File lib/chione/world.rb
169 def stop_managers
170     self.log.info "Stopping managers."
171     self.managers.each {|_, mgr| mgr.stop }
172 end
stop_systems()

Stop any Systems running in the world.

    # File lib/chione/world.rb
191 def stop_systems
192     self.log.info "Stopping systems."
193     self.systems.each {|_, sys| sys.stop }
194 end
stop_timing_loop()

Halt the main timing loop. By default, this just kills the world’s main thread.

    # File lib/chione/world.rb
230 def stop_timing_loop
231     self.log.info "Stopping the timing loop."
232     @main_thread.kill
233 end
subscribe( event_name, callback=nil, &block )

Subscribe to events with the specified event_name. Returns the callback object for later unsubscribe calls.

    # File lib/chione/world.rb
238 def subscribe( event_name, callback=nil, &block )
239     callback ||= block
240 
241     raise LocalJumpError, "no callback given" unless callback
242     raise ArgumentError, "callback is not callable" unless callback.respond_to?( :call )
243     raise ArgumentError, "callback has wrong arity" unless
244         callback.arity >= 2 || callback.arity < 0
245 
246     self.subscriptions[ event_name ].add( callback )
247 
248     return callback
249 end
tick( delta_seconds=1.0/60.0 )

Step the world delta_seconds into the future.

    # File lib/chione/world.rb
147 def tick( delta_seconds=1.0/60.0 )
148     self.publish( 'timing', delta_seconds, self.tick_count )
149     self.publish_deferred_events
150 
151     self.tick_count += 1
152 end
unsubscribe( callback )

Unsubscribe from events that publish to the specified callback.

    # File lib/chione/world.rb
253 def unsubscribe( callback )
254     self.subscriptions.keys.each do |pattern|
255         cbset = self.subscriptions[ pattern ]
256         cbset.delete( callback )
257         self.subscriptions.delete( pattern ) if cbset.empty?
258     end
259 end

Protected Instance Methods

make_injection_hash_for( system_class )

Return a Hash of the loaded Chione::Systems that system_class has requested be injected into it.

    # File lib/chione/world.rb
521 def make_injection_hash_for( system_class )
522     self.log.debug "Injecting %d other system/s into %p" %
523         [ system_class.injected_systems.length, system_class ]
524     return system_class.injected_systems.each_with_object({}) do |(name, injected_class), hash|
525         self.log.debug "  inject %p: %p" % [ name, injected_class ]
526         system = self.systems[ injected_class ] or
527             raise "Can't inject %p into %p: not configured to run it" %
528                 [ injected_class, system_class]
529         hash[ name ] = system
530     end
531 end
timing_loop()

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
547 def timing_loop
548     last_timing_event = Time.now
549     interval = Chione::World.timing_event_interval
550     self.defer_events = false
551     self.tick_count = 0
552 
553     self.log.info "Starting timing loop with interval = %0.3fs." % [ interval ]
554     loop do
555         previous_time, last_timing_event = last_timing_event, Time.now
556         self.tick( last_timing_event - previous_time )
557         remaining_time = interval - (Time.now - last_timing_event)
558 
559         if remaining_time > 0
560             sleep( remaining_time )
561         else
562             self.log.warn "Timing loop %d exceeded `timing_event_interval` (by %0.6fs)" %
563                 [ self.tick_count, remaining_time.abs ]
564         end
565     end
566 
567 ensure
568     self.log.info "Exiting timing loop."
569 end
update_entity_caches( entity, components )

Update any entity caches in the system when an entity has its components hash changed.

    # File lib/chione/world.rb
535 def update_entity_caches( entity, components )
536     entity = entity.id if entity.respond_to?( :id )
537     self.log.debug "  updating entity cache for %p" % [ entity ]
538     self.systems.each_value do |sys|
539         sys.entity_components_updated( entity, components )
540     end
541 end

Component API

↑ top

Public Instance Methods

add_component_for( entity, component, **init_values )
Alias for: add_component_to
add_component_to( entity, component, **init_values )

Add the specified component to the specified entity.

    # File lib/chione/world.rb
369 def add_component_to( entity, component, **init_values )
370     entity = entity.id if entity.respond_to?( :id )
371     component = Chione::Component( component, init_values )
372     component.entity_id = entity
373 
374     self.log.debug "Adding %p for %p" % [ component.class, entity ]
375     self.entities_by_component[ component.class ].add( entity )
376     component_hash = self.components_by_entity[ entity ]
377     component_hash[ component.class ] = component
378 
379     self.update_entity_caches( entity, component_hash )
380 end
Also aliased as: add_component_for
components_for( entity )

Return a Hash of the Component instances associated with entity, keyed by their class.

    # File lib/chione/world.rb
386 def components_for( entity )
387     entity = entity.id if entity.respond_to?( :id )
388     return self.components_by_entity[ entity ].dup
389 end
get_component_for( entity, component_class )

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
394 def get_component_for( entity, component_class )
395     entity = entity.id if entity.respond_to?( :id )
396     return self.components_by_entity[ entity ][ component_class ]
397 end
has_component_for?( entity, component )

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
423 def has_component_for?( entity, component )
424     entity = entity.id if entity.respond_to?( :id )
425     if component.is_a?( Class )
426         return self.components_by_entity[ entity ].key?( component )
427     else
428         return self.components_by_entity[ entity ][ component.class ] == component
429     end
430 end
remove_component_for( entity, component )
remove_component_from( entity, component )

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
404 def remove_component_from( entity, component )
405     entity = entity.id if entity.respond_to?( :id )
406     if component.is_a?( Class )
407         self.entities_by_component[ component ].delete( entity )
408         component_hash = self.components_by_entity[ entity ]
409         component_hash.delete( component )
410         self.update_entity_caches( entity, component_hash )
411     else
412         self.remove_component_from( entity, component.class ) if
413             self.has_component_for?( entity, component )
414     end
415 end
Also aliased as: remove_component_for

Entity API

↑ top

Public Instance Methods

create_blank_entity()

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
335 def create_blank_entity
336     return Chione::Entity.new( self )
337 end
create_entity( archetype=nil )

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
318 def create_entity( archetype=nil )
319     entity = if archetype
320             archetype.construct_for( self )
321         else
322             self.create_blank_entity
323         end
324 
325     @entities[ entity.id ] = entity
326 
327     self.publish( 'entity/created', entity.id )
328     return entity
329 end
destroy_entity( entity )

Destroy the specified entity and remove it from any registered systems/managers.

    # File lib/chione/world.rb
342 def destroy_entity( entity )
343     raise ArgumentError, "%p does not contain entity %p" % [ self, entity ] unless
344         self.has_entity?( entity )
345 
346     self.publish( 'entity/destroyed', entity )
347     self.entities_by_component.each_value {|set| set.delete(entity.id) }
348     self.components_by_entity.delete( entity.id )
349     @entities.delete( entity.id )
350 end
has_entity?( entity )

Returns true if the world contains the specified entity or an entity with entity as the ID.

    # File lib/chione/world.rb
355 def has_entity?( entity )
356     if entity.respond_to?( :id )
357         return @entities.key?( entity.id )
358     else
359         return @entities.key?( entity )
360     end
361 end

Manager API

↑ top

Public Instance Methods

add_manager( manager_type, *args )

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
482 def add_manager( manager_type, *args )
483     manager_obj = manager_type.new( self, *args )
484     self.managers[ manager_type ] = manager_obj
485 
486     if self.running?
487         self.log.info "Starting %p added to running world." % [ manager_type ]
488         manager_obj.start
489     end
490 
491     self.publish( 'manager/added', manager_obj )
492     return manager_obj
493 end
remove_manager( manager_type )

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
499 def remove_manager( manager_type )
500     manager_obj = self.managers.delete( manager_type ) or return nil
501     self.publish( 'manager/removed', manager_obj )
502 
503     if self.running?
504         self.log.info "Stopping %p removed from running world." % [ manager_type ]
505         manager_obj.stop
506     end
507 
508     return manager_obj
509 end

System API

↑ top

Public Instance Methods

add_system( system_type, *args )

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
439 def add_system( system_type, *args )
440     system_obj = system_type.new( self, *args )
441     self.systems[ system_type ] = system_obj
442 
443     if self.running?
444         self.log.info "Starting %p added to running world." % [ system_type ]
445         system_obj.start
446     end
447 
448     self.publish( 'system/added', system_obj )
449     return system_obj
450 end
entities_with( aspect )

Return an Array of all entities that match the specified aspect.

    # File lib/chione/world.rb
471 def entities_with( aspect )
472     return aspect.matching_entities( self.entities_by_component )
473 end
remove_system( system_type )

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
456 def remove_system( system_type )
457     system_obj = self.systems.delete( system_type ) or return nil
458 
459     self.publish( 'system/removed', system_obj )
460 
461     if self.running?
462         self.log.info "Stopping %p before being removed from runnning world." % [ system_type ]
463         system_obj.stop
464     end
465 
466     return system_obj
467 end