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
43 def initialize
44     @entities      = {}
45     @systems       = {}
46     @managers      = {}
47 
48     @subscriptions = Hash.new {|h,k| h[k] = Set.new }
49     @defer_events  = true
50     @deferred_events = []
51 
52     @main_thread   = nil
53     @world_threads = ThreadGroup.new
54 
55     @entities_by_component = Hash.new {|h,k| h[k] = Set.new }
56     @components_by_entity = Hash.new {|h, k| h[k] = {} }
57 
58     @tick_count = 0
59 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
302 def call_subscription_callback( callback, event_name, payload )
303     callback.call( event_name, payload )
304     return true
305 rescue => err
306     self.log.error "%p while calling %p for a %p event: %s" %
307         [ err.class, callback, event_name, err.message ]
308     self.log.debug "  %s" % [ err.backtrace.join("\n  ") ]
309     return false
310 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
286 def call_subscription_callbacks( event_name, payload )
287     self.subscriptions.each do |pattern, callbacks|
288         next unless File.fnmatch?( pattern, event_name, File::FNM_EXTGLOB|File::FNM_PATHNAME )
289 
290         callbacks.each do |callback|
291             unless self.call_subscription_callback( callback, event_name, payload )
292                 self.log.debug "Callback failed; removing it from the subscription."
293                 self.unsubscribe( callback )
294             end
295         end
296     end
297 end
defer_events()

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

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

Publish an event with the specified event_name and payload.

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

Send any deferred events to subscribers.

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

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

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

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

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

Start any Managers registered with the world.

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

Start any Systems registered with the world.

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

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

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

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

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

Stop the world.

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

Stop any Managers running in the world.

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

Stop any Systems running in the world.

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

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

    # File lib/chione/world.rb
231 def stop_timing_loop
232     self.log.info "Stopping the timing loop."
233     @main_thread.kill
234 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
239 def subscribe( event_name, callback=nil, &block )
240     callback ||= block
241 
242     raise LocalJumpError, "no callback given" unless callback
243     raise ArgumentError, "callback is not callable" unless callback.respond_to?( :call )
244     raise ArgumentError, "callback has wrong arity" unless
245         callback.arity >= 2 || callback.arity < 0
246 
247     self.subscriptions[ event_name ].add( callback )
248 
249     return callback
250 end
tick( delta_seconds=1.0/60.0 )

Step the world delta_seconds into the future.

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

Unsubscribe from events that publish to the specified callback.

    # File lib/chione/world.rb
254 def unsubscribe( callback )
255     self.subscriptions.keys.each do |pattern|
256         cbset = self.subscriptions[ pattern ]
257         cbset.delete( callback )
258         self.subscriptions.delete( pattern ) if cbset.empty?
259     end
260 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
522 def make_injection_hash_for( system_class )
523     self.log.debug "Injecting %d other system/s into %p" %
524         [ system_class.injected_systems.length, system_class ]
525     return system_class.injected_systems.each_with_object({}) do |(name, injected_class), hash|
526         self.log.debug "  inject %p: %p" % [ name, injected_class ]
527         system = self.systems[ injected_class ] or
528             raise "Can't inject %p into %p: not configured to run it" %
529                 [ injected_class, system_class]
530         hash[ name ] = system
531     end
532 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
548 def timing_loop
549     last_timing_event = Time.now
550     interval = Chione::World.timing_event_interval
551     self.defer_events = false
552     self.tick_count = 0
553 
554     self.log.info "Starting timing loop with interval = %0.3fs." % [ interval ]
555     loop do
556         previous_time, last_timing_event = last_timing_event, Time.now
557         self.tick( last_timing_event - previous_time )
558         remaining_time = interval - (Time.now - last_timing_event)
559 
560         if remaining_time > 0
561             sleep( remaining_time )
562         else
563             self.log.warn "Timing loop %d exceeded `timing_event_interval` (by %0.6fs)" %
564                 [ self.tick_count, remaining_time.abs ]
565         end
566     end
567 
568 ensure
569     self.log.info "Exiting timing loop."
570 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
536 def update_entity_caches( entity, components )
537     entity = entity.id if entity.respond_to?( :id )
538     self.log.debug "  updating entity cache for %p" % [ entity ]
539     self.systems.each_value do |sys|
540         sys.entity_components_updated( entity, components )
541     end
542 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
370 def add_component_to( entity, component, **init_values )
371     entity = entity.id if entity.respond_to?( :id )
372     component = Chione::Component( component, init_values )
373     component.entity_id = entity
374 
375     self.log.debug "Adding %p for %p" % [ component.class, entity ]
376     self.entities_by_component[ component.class ].add( entity )
377     component_hash = self.components_by_entity[ entity ]
378     component_hash[ component.class ] = component
379 
380     self.update_entity_caches( entity, component_hash )
381 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
387 def components_for( entity )
388     entity = entity.id if entity.respond_to?( :id )
389     return self.components_by_entity[ entity ].dup
390 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
395 def get_component_for( entity, component_class )
396     entity = entity.id if entity.respond_to?( :id )
397     return self.components_by_entity[ entity ][ component_class ]
398 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
424 def has_component_for?( entity, component )
425     entity = entity.id if entity.respond_to?( :id )
426     if component.is_a?( Class )
427         return self.components_by_entity[ entity ].key?( component )
428     else
429         return self.components_by_entity[ entity ][ component.class ] == component
430     end
431 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
405 def remove_component_from( entity, component )
406     entity = entity.id if entity.respond_to?( :id )
407     if component.is_a?( Class )
408         self.entities_by_component[ component ].delete( entity )
409         component_hash = self.components_by_entity[ entity ]
410         component_hash.delete( component )
411         self.update_entity_caches( entity, component_hash )
412     else
413         self.remove_component_from( entity, component.class ) if
414             self.has_component_for?( entity, component )
415     end
416 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
336 def create_blank_entity
337     return Chione::Entity.new( self )
338 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
319 def create_entity( archetype=nil )
320     entity = if archetype
321             archetype.construct_for( self )
322         else
323             self.create_blank_entity
324         end
325 
326     @entities[ entity.id ] = entity
327 
328     self.publish( 'entity/created', entity.id )
329     return entity
330 end
destroy_entity( entity )

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

    # File lib/chione/world.rb
343 def destroy_entity( entity )
344     raise ArgumentError, "%p does not contain entity %p" % [ self, entity ] unless
345         self.has_entity?( entity )
346 
347     self.publish( 'entity/destroyed', entity )
348     self.entities_by_component.each_value {|set| set.delete(entity.id) }
349     self.components_by_entity.delete( entity.id )
350     @entities.delete( entity.id )
351 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
356 def has_entity?( entity )
357     if entity.respond_to?( :id )
358         return @entities.key?( entity.id )
359     else
360         return @entities.key?( entity )
361     end
362 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
483 def add_manager( manager_type, *args )
484     manager_obj = manager_type.new( self, *args )
485     self.managers[ manager_type ] = manager_obj
486 
487     if self.running?
488         self.log.info "Starting %p added to running world." % [ manager_type ]
489         manager_obj.start
490     end
491 
492     self.publish( 'manager/added', manager_obj )
493     return manager_obj
494 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
500 def remove_manager( manager_type )
501     manager_obj = self.managers.delete( manager_type ) or return nil
502     self.publish( 'manager/removed', manager_obj )
503 
504     if self.running?
505         self.log.info "Stopping %p removed from running world." % [ manager_type ]
506         manager_obj.stop
507     end
508 
509     return manager_obj
510 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
440 def add_system( system_type, *args )
441     system_obj = system_type.new( self, *args )
442     self.systems[ system_type ] = system_obj
443 
444     if self.running?
445         self.log.info "Starting %p added to running world." % [ system_type ]
446         system_obj.start
447     end
448 
449     self.publish( 'system/added', system_obj )
450     return system_obj
451 end
entities_with( aspect )

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

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