class Chione::World

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() click to toggle source

Create a new Chione::World

# File lib/chione/world.rb, line 42
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_subscription_callback( callback, event_name, payload ) click to toggle source

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 301
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_subscription_callbacks( event_name, payload ) click to toggle source

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

# File lib/chione/world.rb, line 285
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
defer_events() click to toggle source

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

# File lib/chione/world.rb, line 107
attr_predicate_accessor :defer_events
kill_world_threads() click to toggle source

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

# File lib/chione/world.rb, line 210
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( event_name, *payload ) click to toggle source

Publish an event with the specified event_name and payload.

# File lib/chione/world.rb, line 263
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
publish_deferred_events() click to toggle source

Send any deferred events to subscribers.

# File lib/chione/world.rb, line 274
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
running?() click to toggle source

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

# File lib/chione/world.rb, line 204
def running?
        return self.started? && self.tick_count.nonzero?
end
start() click to toggle source

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

# File lib/chione/world.rb, line 127
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_managers() click to toggle source

Start any Managers registered with the world.

# File lib/chione/world.rb, line 156
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_systems() click to toggle source

Start any Systems registered with the world.

# File lib/chione/world.rb, line 176
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
started?() click to toggle source

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

# File lib/chione/world.rb, line 198
def started?
        return @main_thread && @main_thread.alive?
end
status() click to toggle source

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

# File lib/chione/world.rb, line 116
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() click to toggle source

Stop the world.

# File lib/chione/world.rb, line 221
def stop
        self.stop_systems
        self.stop_managers
        self.kill_world_threads
        self.stop_timing_loop
end
stop_managers() click to toggle source

Stop any Managers running in the world.

# File lib/chione/world.rb, line 169
def stop_managers
        self.log.info "Stopping managers."
        self.managers.each {|_, mgr| mgr.stop }
end
stop_systems() click to toggle source

Stop any Systems running in the world.

# File lib/chione/world.rb, line 191
def stop_systems
        self.log.info "Stopping systems."
        self.systems.each {|_, sys| sys.stop }
end
stop_timing_loop() click to toggle source

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

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

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

# File lib/chione/world.rb, line 238
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
tick( delta_seconds=1.0/60.0 ) click to toggle source

Step the world delta_seconds into the future.

# File lib/chione/world.rb, line 147
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( callback ) click to toggle source

Unsubscribe from events that publish to the specified callback.

# File lib/chione/world.rb, line 253
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

make_injection_hash_for( system_class ) click to toggle source

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

# File lib/chione/world.rb, line 521
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
timing_loop() click to toggle source

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 547
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_entity_caches( entity, components ) click to toggle source

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

# File lib/chione/world.rb, line 535
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

↑ top

Public Instance Methods

add_component_for( entity, component, **init_values )
Alias for: add_component_to
add_component_to( entity, component, **init_values ) click to toggle source

Add the specified component to the specified entity.

# File lib/chione/world.rb, line 369
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
Also aliased as: add_component_for
components_for( entity ) click to toggle source

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

# File lib/chione/world.rb, line 386
def components_for( entity )
        entity = entity.id if entity.respond_to?( :id )
        return self.components_by_entity[ entity ].dup
end
get_component_for( entity, component_class ) click to toggle source

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

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 423
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_component_for( entity, component )
remove_component_from( entity, component ) click to toggle source

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 404
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
Also aliased as: remove_component_for

Entity API

↑ top

Public Instance Methods

create_blank_entity() click to toggle source

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

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 318
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_entity( entity ) click to toggle source

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

# File lib/chione/world.rb, line 342
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
has_entity?( entity ) click to toggle source

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

# File lib/chione/world.rb, line 355
def has_entity?( entity )
        if entity.respond_to?( :id )
                return @entities.key?( entity.id )
        else
                return @entities.key?( entity )
        end
end

Manager API

↑ top

Public Instance Methods

add_manager( manager_type, *args ) click to toggle source

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 482
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_manager( manager_type ) click to toggle source

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 499
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

↑ top

Public Instance Methods

add_system( system_type, *args ) click to toggle source

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 439
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
entities_with( aspect ) click to toggle source

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

# File lib/chione/world.rb, line 471
def entities_with( aspect )
        return aspect.matching_entities( self.entities_by_component )
end
remove_system( system_type ) click to toggle source

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 456
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