Class: Chione::World

Inherits:
Object
  • Object
show all
Extended by:
MethodUtilities, Configurability, Loggability
Defined in:
lib/chione/world.rb

Overview

The main ECS container

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods included from MethodUtilities

attr_predicate, attr_predicate_accessor, singleton_attr_accessor, singleton_attr_reader, singleton_attr_writer, singleton_method_alias, singleton_predicate_accessor, singleton_predicate_reader

Constructor Details

#initializeWorld

Create a new Chione::World



43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
# 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

Instance Attribute Details

#components_by_entityObject (readonly)

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



98
99
100
# File 'lib/chione/world.rb', line 98

def components_by_entity
  @components_by_entity
end

#deferred_eventsObject (readonly)

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



113
114
115
# File 'lib/chione/world.rb', line 113

def deferred_events
  @deferred_events
end

#entitiesObject (readonly)

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



72
73
74
# File 'lib/chione/world.rb', line 72

def entities
  @entities
end

#entities_by_componentObject (readonly)

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



93
94
95
# File 'lib/chione/world.rb', line 93

def entities_by_component
  @entities_by_component
end

#main_threadObject (readonly)

The Thread object running the World’s IO reactor loop



88
89
90
# File 'lib/chione/world.rb', line 88

def main_thread
  @main_thread
end

#managersObject (readonly)

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



80
81
82
# File 'lib/chione/world.rb', line 80

def managers
  @managers
end

#subscriptionsObject (readonly)

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



103
104
105
# File 'lib/chione/world.rb', line 103

def subscriptions
  @subscriptions
end

#systemsObject (readonly)

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



76
77
78
# File 'lib/chione/world.rb', line 76

def systems
  @systems
end

#tick_countObject

The number of times the event loop has executed.



68
69
70
# File 'lib/chione/world.rb', line 68

def tick_count
  @tick_count
end

#world_threadsObject (readonly)

The ThreadGroup that contains all Threads managed by the World.



84
85
86
# File 'lib/chione/world.rb', line 84

def world_threads
  @world_threads
end

Instance Method Details

#add_component_to(entity, component, **init_values) ⇒ Object Also known as: add_component_for

Add the specified component to the specified entity.



370
371
372
373
374
375
376
377
378
379
380
381
# 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

#add_manager(manager_type, *args) ⇒ Object

Add an instance of the specified manager_type to the world and return it. It will replace any existing manager of the same type.



483
484
485
486
487
488
489
490
491
492
493
494
# 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

#add_system(system_type, *args) ⇒ Object

Add an instance of the specified system_type to the world and return it. It will replace any existing system of the same type.



440
441
442
443
444
445
446
447
448
449
450
451
# 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

#call_subscription_callback(callback, event_name, payload) ⇒ Object

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



302
303
304
305
306
307
308
309
310
# 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_subscription_callbacks(event_name, payload) ⇒ Object

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



286
287
288
289
290
291
292
293
294
295
296
297
# 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

#components_for(entity) ⇒ Object

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



387
388
389
390
# 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

#create_blank_entityObject

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.



336
337
338
# File 'lib/chione/world.rb', line 336

def create_blank_entity
  return Chione::Entity.new( self )
end

#create_entity(archetype = nil) ⇒ Object

Return a new Chione::Entity for the receiving World, using the optional archetype to populate it with components if it’s specified.



319
320
321
322
323
324
325
326
327
328
329
330
# 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

#defer_eventsObject

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



108
# File 'lib/chione/world.rb', line 108

attr_predicate_accessor :defer_events

#destroy_entity(entity) ⇒ Object

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

Raises:

  • (ArgumentError)


343
344
345
346
347
348
349
350
351
# 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

#entities_with(aspect) ⇒ Object

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



472
473
474
# File 'lib/chione/world.rb', line 472

def entities_with( aspect )
  return aspect.matching_entities( self.entities_by_component )
end

#get_component_for(entity, component_class) ⇒ Object

Return the Component instance of the specified component_class that’s associated with the given entity, if it has one.



395
396
397
398
# 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

#has_component_for?(entity, component) ⇒ Boolean

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.

Returns:

  • (Boolean)


424
425
426
427
428
429
430
431
# 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

#has_entity?(entity) ⇒ Boolean

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

Returns:

  • (Boolean)


356
357
358
359
360
361
362
# 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

#kill_world_threadsObject

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



211
212
213
214
215
216
217
218
# 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(event_name, *payload) ⇒ Object

Publish an event with the specified event_name and payload.



264
265
266
267
268
269
270
271
# 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

#publish_deferred_eventsObject

Send any deferred events to subscribers.



275
276
277
278
279
280
281
# 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

#remove_component_from(entity, component) ⇒ Object Also known as: remove_component_for

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.



405
406
407
408
409
410
411
412
413
414
415
416
# 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

#remove_manager(manager_type) ⇒ Object

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.



500
501
502
503
504
505
506
507
508
509
510
# 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

#remove_system(system_type) ⇒ Object

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.



457
458
459
460
461
462
463
464
465
466
467
468
# 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

#running?Boolean

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

Returns:

  • (Boolean)


205
206
207
# File 'lib/chione/world.rb', line 205

def running?
  return self.started? && self.tick_count.nonzero?
end

#startObject

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



128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
# 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_managersObject

Start any Managers registered with the world.



157
158
159
160
161
162
163
164
165
166
# 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_systemsObject

Start any Systems registered with the world.



177
178
179
180
181
182
183
184
185
186
187
188
# 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

#started?Boolean

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

Returns:

  • (Boolean)


199
200
201
# File 'lib/chione/world.rb', line 199

def started?
  return @main_thread && @main_thread.alive?
end

#statusObject

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



117
118
119
120
121
122
123
124
# 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

#stopObject

Stop the world.



222
223
224
225
226
227
# 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_managersObject

Stop any Managers running in the world.



170
171
172
173
# File 'lib/chione/world.rb', line 170

def stop_managers
  self.log.info "Stopping managers."
  self.managers.each {|_, mgr| mgr.stop }
end

#stop_systemsObject

Stop any Systems running in the world.



192
193
194
195
# File 'lib/chione/world.rb', line 192

def stop_systems
  self.log.info "Stopping systems."
  self.systems.each {|_, sys| sys.stop }
end

#stop_timing_loopObject

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



231
232
233
234
# File 'lib/chione/world.rb', line 231

def stop_timing_loop
  self.log.info "Stopping the timing loop."
  @main_thread.kill
end

#subscribe(event_name, callback = nil, &block) ⇒ Object

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

Raises:

  • (LocalJumpError)


239
240
241
242
243
244
245
246
247
248
249
250
# 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

#tick(delta_seconds = 1.0/60.0) ⇒ Object

Step the world delta_seconds into the future.



148
149
150
151
152
153
# 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(callback) ⇒ Object

Unsubscribe from events that publish to the specified callback.



254
255
256
257
258
259
260
# 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