chione 0.3.0 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -6,12 +6,15 @@ require 'loggability'
6
6
  require 'securerandom'
7
7
 
8
8
  require 'chione' unless defined?( Chione )
9
+ require 'chione/mixins'
9
10
 
10
11
  # The Entity (identity) class
11
12
  class Chione::Entity
12
13
  extend Loggability,
13
14
  Deprecatable
14
15
 
16
+ include Chione::Inspection
17
+
15
18
  # Loggability API -- send logs to the Chione logger
16
19
  log_to :chione
17
20
 
@@ -25,9 +28,8 @@ class Chione::Entity
25
28
  ### Create a new Entity for the specified +world+, and use the specified +id+ if
26
29
  ### given. If no +id+ is given, one will be automatically generated.
27
30
  def initialize( world, id=nil )
28
- @world = world
29
- @id = id || self.class.make_new_id
30
- @components = {}
31
+ @world = world
32
+ @id = id || self.class.make_new_id
31
33
  end
32
34
 
33
35
 
@@ -41,42 +43,50 @@ class Chione::Entity
41
43
  # The World the Entity belongs to
42
44
  attr_reader :world
43
45
 
44
- ##
45
- # The Hash of this entity's Components
46
- attr_reader :components
46
+
47
+ ### Return the components that the entity's World has registered for it as a Hash
48
+ ### keyed by the Component class.
49
+ def components
50
+ return self.world.components_for( self )
51
+ end
47
52
 
48
53
 
49
- ### Add the specified +component+ to the entity. It will replace any existing component
50
- ### of the same type.
51
- def add_component( component )
52
- component = Chione::Component( component )
53
- self.components[ component.class ] = component
54
- self.world.add_component_for( self, component )
54
+ ### Add the specified +component+ to the entity. The +component+ can be a
55
+ ### subclass of Chione::Component, an instance of such a subclass, or the name
56
+ ### of a subclass. It will replace any existing component of the same type.
57
+ def add_component( component, **init_values )
58
+ self.world.add_component_to( self, component, init_values )
55
59
  end
56
60
 
57
61
 
58
- ### Fetch the first Component of the specified +types+ that belongs to the entity. If the
59
- ### Entity doesn't have any of the specified types of Component, raises a KeyError.
60
- def find_component( *types )
61
- found_type = types.find {|type| self.components[type] } or
62
- raise KeyError, "entity %s doesn't have any of %p" % [ self.id, types ]
63
- return self.components[ found_type ]
62
+ ### Fetch the component of the specified +component_class+ that corresponds with the
63
+ ### receiving entity. Returns +nil+ if so much component exists.
64
+ def get_component( component_class )
65
+ return self.world.get_component_for( self, component_class )
64
66
  end
65
- alias_method :get_component, :find_component
66
- deprecate :get_component, message: 'Use #find_component instead', removal_version: '1.0'
67
67
 
68
68
 
69
- ### Returns +true+ if this entity has the specified +component+.
70
- def has_component?( component )
71
- return self.components.key?( component )
69
+ ### Remove the component of the specified +component_class+ that corresponds with the
70
+ ### receiving entity. Returns the component instance if it was removed, or +nil+ if no
71
+ ### Component of the specified type was registered to the entity.
72
+ def remove_component( component_class )
73
+ return self.world.remove_component_from( self, component_class )
72
74
  end
73
75
 
74
76
 
75
- ### Return the Entity as a human-readable string suitable for debugging.
76
- def inspect
77
- return "#<%p:%0#x ID=%s (%s)>" % [
78
- self.class,
79
- self.object_id * 2,
77
+ ### Returns +true+ if this entity has a component of the specified +component_class+.
78
+ def has_component?( component_class )
79
+ return self.world.has_component_for?( self, component_class )
80
+ end
81
+
82
+
83
+ #########
84
+ protected
85
+ #########
86
+
87
+ ### Return the detailed part of the Entity's #inspect output
88
+ def inspect_details
89
+ return "ID=%s (%s)" % [
80
90
  self.id,
81
91
  self.components.keys.map( &:name ).sort.join( '+' )
82
92
  ]
@@ -0,0 +1,18 @@
1
+ # -*- ruby -*-
2
+ # encoding: utf-8
3
+ # frozen_string_literal: true
4
+
5
+ require 'faker'
6
+ require 'fluent_fixtures'
7
+
8
+ require 'chione' unless defined?( Chione )
9
+
10
+
11
+ module Chione::Fixtures
12
+ extend FluentFixtures::Collection
13
+
14
+ # Set the path to use when finding fixtures for this collection
15
+ fixture_path_prefix 'chione/fixtures'
16
+
17
+ end # module Chione
18
+
@@ -0,0 +1,32 @@
1
+ # -*- ruby -*-
2
+ #encoding: utf-8
3
+
4
+ require 'faker'
5
+
6
+ require 'chione/fixtures' unless defined?( Chione::Fixtures )
7
+ require 'chione/entity'
8
+
9
+
10
+ # Entity fixtures
11
+ module Chione::Fixtures::Entities
12
+ extend Chione::Fixtures
13
+
14
+ fixtured_class Chione::Entity
15
+
16
+ base :entity
17
+
18
+ decorator :with_id do |id|
19
+ @id = id
20
+ end
21
+
22
+
23
+ decorator :with_components do |*components|
24
+ components.each do |comp|
25
+ self.add_component( comp )
26
+ end
27
+ end
28
+
29
+ end # module Chione::Fixtures::Entities
30
+
31
+
32
+
@@ -0,0 +1,35 @@
1
+ # -*- ruby -*-
2
+ #encoding: utf-8
3
+
4
+ require 'chione'
5
+ require 'chione/system'
6
+
7
+
8
+
9
+ # Process all entities matching an aspect iteratively for every World timing
10
+ # loop iteration.
11
+ class Chione::IteratingSystem < Chione::System
12
+
13
+
14
+ every_tick do |*|
15
+ world = self.world
16
+
17
+ self.class.aspects.each do |name, aspect|
18
+ self.log.debug "Iterating over entities with '%s'" % [ name ]
19
+ world.entities_with( aspect ).each do |entity|
20
+ self.process( name, entity, world.components_for(entity) )
21
+ end
22
+ end
23
+
24
+ end
25
+
26
+
27
+ ### Process the given +components+ (which match the system's Aspect) for the
28
+ ### specified +entity_id+. Concrete subclasses are required to override this.
29
+ def process( aspect_name, entity_id, components )
30
+ raise NotImplementedError, "%p does not implement #%s" % [ self.class, __method__ ]
31
+ end
32
+
33
+
34
+ end # class Chione::IteratingSystem
35
+
@@ -5,6 +5,7 @@ require 'pluggability'
5
5
  require 'loggability'
6
6
 
7
7
  require 'chione' unless defined?( Chione )
8
+ require 'chione/mixins'
8
9
 
9
10
 
10
11
  # The Manager class
@@ -12,6 +13,9 @@ class Chione::Manager
12
13
  extend Loggability,
13
14
  Pluggability
14
15
 
16
+ include Chione::Inspection
17
+
18
+
15
19
  # Loggability API -- send logs to the Chione logger
16
20
  log_to :chione
17
21
 
@@ -46,4 +50,15 @@ class Chione::Manager
46
50
  raise NotImplementedError, "%p does not implement required method #stop" % [ self.class ]
47
51
  end
48
52
 
53
+
54
+ #########
55
+ protected
56
+ #########
57
+
58
+ ### Return the detail part of the inspection string.
59
+ def inspect_details
60
+ return "for %p:%#016x" % [ self.world.class, self.world.object_id * 2 ]
61
+ end
62
+
63
+
49
64
  end # class Chione::Manager
@@ -88,4 +88,26 @@ module Chione
88
88
 
89
89
  end # module MethodUtilities
90
90
 
91
+
92
+ # An extensible #inspect for Chione objects.
93
+ module Inspection
94
+
95
+ ### Return a human-readable representation of the object suitable for debugging.
96
+ def inspect
97
+ return "#<%p:%#016x %s>" % [
98
+ self.class,
99
+ self.object_id * 2,
100
+ self.inspect_details,
101
+ ]
102
+ end
103
+
104
+
105
+ ### Return the detail portion of the inspect output for this object.
106
+ def inspect_details
107
+ return ''
108
+ end
109
+
110
+ end # module Inspection
111
+
112
+
91
113
  end # module Chione
@@ -15,6 +15,19 @@ class Chione::System
15
15
  Pluggability,
16
16
  Chione::MethodUtilities
17
17
 
18
+ include Chione::Inspection
19
+
20
+
21
+ # A Hash that auto-vivifies only its :default key
22
+ DEFAULT_ASPECT_HASH = Hash.new do |h,k|
23
+ if k == :default
24
+ h[k] = Chione::Aspect.new
25
+ else
26
+ nil
27
+ end
28
+ end
29
+
30
+
18
31
  # Loggability API -- send logs to the Chione logger
19
32
  log_to :chione
20
33
 
@@ -22,25 +35,73 @@ class Chione::System
22
35
  plugin_prefixes 'chione/system'
23
36
 
24
37
 
38
+ ##
39
+ # The Hash of Chione::Aspects that describe entities this system is interested
40
+ # in, keyed by name (a Symbol). A System which declares no aspects will have a
41
+ # +:default+ Aspect which matches all entities.
42
+ singleton_attr_reader :aspects
43
+
44
+ ##
45
+ # Event handler tuples (event name, callback) that should be registered when the
46
+ # System is started.
47
+ singleton_attr_reader :event_handlers
48
+
49
+
25
50
  ### Add the specified +component_types+ to the Aspect of this System as being
26
51
  ### required in any entities it processes.
27
- def self::aspect( *required, all_of: nil, one_of: nil, none_of: nil )
28
- @aspect ||= Chione::Aspect.new
52
+ def self::aspect( name, *required, all_of: nil, one_of: nil, none_of: nil )
53
+ aspect = Chione::Aspect.new
29
54
 
30
55
  all_of = required + Array( all_of )
31
56
 
32
- @aspect = @aspect.with_all_of( all_of )
33
- @aspect = @aspect.with_one_of( one_of ) if one_of
34
- @aspect = @aspect.with_none_of( none_of ) if none_of
57
+ aspect = aspect.with_all_of( all_of )
58
+ aspect = aspect.with_one_of( one_of ) if one_of
59
+ aspect = aspect.with_none_of( none_of ) if none_of
35
60
 
36
- return @aspect
61
+ self.aspects[ name ] = aspect
62
+ end
63
+
64
+
65
+ ### Declare a block that is called once whenever an event matching +event_name+ is
66
+ ### broadcast to the World.
67
+ def self::on( event_name, &block )
68
+ raise LocalJumpError, "no block given" unless block
69
+ raise ArgumentError, "callback has wrong arity" unless block.arity >= 2 || block.arity < 0
70
+
71
+ method_name = "on_%s_event" % [ event_name.tr('/', '_') ]
72
+ self.log.debug "Making handler method #%s for %s events out of %p" %
73
+ [ method_name, event_name, block ]
74
+ define_method( method_name, &block )
75
+
76
+ self.event_handlers << [ event_name, method_name ]
77
+ end
78
+
79
+
80
+ ### Declare a block that is called once every tick for each entity that matches the given
81
+ ### +aspect+.
82
+ def self::every_tick( &block )
83
+ return self.on( 'timing' ) do |event_name, payload|
84
+ self.instance_exec( *payload, &block )
85
+ end
86
+ end
87
+
88
+
89
+ ### Add some per-subclass data structures to inheriting +subclass+es.
90
+ def self::inherited( subclass )
91
+ super
92
+ subclass.instance_variable_set( :@aspects, DEFAULT_ASPECT_HASH.clone )
93
+ subclass.instance_variable_set( :@event_handlers, self.event_handlers&.dup || [] )
37
94
  end
38
- singleton_method_alias :for_entities_that_have, :aspect
39
95
 
40
96
 
41
97
  ### Create a new Chione::System for the specified +world+.
42
98
  def initialize( world, * )
43
99
  @world = world
100
+ @aspect_entities = self.class.aspects.each_with_object( {} ) do |(aspect_name, aspect), hash|
101
+ matching_set = world.entities_with( aspect )
102
+ self.log.debug "Initial Set with the %s aspect: %p" % [ aspect_name, matching_set]
103
+ hash[ aspect_name ] = matching_set
104
+ end
44
105
  end
45
106
 
46
107
 
@@ -48,25 +109,89 @@ class Chione::System
48
109
  public
49
110
  ######
50
111
 
112
+ ##
51
113
  # The World which the System belongs to
52
114
  attr_reader :world
53
115
 
116
+ ##
117
+ # The Hash of Sets of entity IDs which match the System's aspects, keyed by aspect name.
118
+ attr_reader :aspect_entities
119
+
54
120
 
55
121
  ### Start the system.
56
122
  def start
57
- self.log.info "Starting the %p" % [ self.class ]
123
+ self.log.info "Starting the %p system; %d event handlers to register" %
124
+ [ self.class, self.class.event_handlers.length ]
125
+ self.class.event_handlers.each do |event_name, method_name|
126
+ callback = self.method( method_name )
127
+ self.log.info "Registering %p as a callback for '%s' events." % [ callback, event_name ]
128
+ self.world.subscribe( event_name, callback )
129
+ end
58
130
  end
59
131
 
60
132
 
61
133
  ### Stop the system.
62
134
  def stop
135
+ self.class.event_handlers.each do |_, method_name|
136
+ callback = self.method( method_name )
137
+ self.log.info "Unregistering subscription for %p." % [ callback ]
138
+ self.world.unsubscribe( callback )
139
+ end
140
+ end
141
+
142
+
143
+ ### Return an Enumerator that yields the entities which match the given +aspect_name+.
144
+ def entities( aspect_name=:default )
145
+ return self.aspect_entities[ aspect_name ].to_enum( :each )
146
+ end
147
+
148
+
149
+ ### Entity callback -- called whenever an entity has a component added to it or
150
+ ### removed from it. Calls the appropriate callback (#inserted or #removed) if
151
+ ### the component change caused it to belong to or stop belonging to one of the
152
+ ### system's aspects.
153
+ def entity_components_updated( entity_id, components_hash )
154
+ self.class.aspects.each do |aspect_name, aspect|
155
+ entity_ids = self.aspect_entities[ aspect_name ]
156
+
157
+ if aspect.matches?( components_hash )
158
+ self.inserted( aspect_name, entity_id, components_hash ) if
159
+ entity_ids.add?( entity_id )
160
+ else
161
+ self.removed( aspect_name, entity_id, components_hash ) if
162
+ entity_ids.delete?( entity_id )
163
+ end
164
+ end
165
+ end
166
+
167
+
168
+ ### Entity callback -- called whenever an entity has a component added to it
169
+ ### that makes it start matching an aspect of the receiving System. The
170
+ ### +aspect_name+ is the name of the Aspect it now matches, and the +components+
171
+ ### are a Hash of the entity's components keyed by Class. By default this is a
172
+ ### no-op.
173
+ def inserted( aspect_name, entity_id, components )
174
+ self.log.debug "Entity %s now matches the %s aspect." % [ entity_id, aspect_name ]
63
175
  end
64
176
 
65
177
 
66
- ### Return an Enumerator that yields the entities which this system operators
67
- ### over.
68
- def entities
69
- return self.world.entities_for( self )
178
+ ### Entity callback -- called whenever an entity has a component removed from it
179
+ ### that makes it stop matching an aspect of the receiving System. The
180
+ ### +aspect_name+ is the name of the Aspect it no longer matches, and the
181
+ ### +components+ are a Hash of the entity's components keyed by Class. By
182
+ ### default this is a no-op.
183
+ def removed( aspect_name, entity_id, components )
184
+ self.log.debug "Entity %s no longer matches the %s aspect." % [ entity_id, aspect_name ]
185
+ end
186
+
187
+
188
+ #########
189
+ protected
190
+ #########
191
+
192
+ ### Return the detail part of the inspection string.
193
+ def inspect_details
194
+ return "for %p:%#016x" % [ self.world.class, self.world.object_id * 2 ]
70
195
  end
71
196
 
72
197
  end # class Chione::System
@@ -42,9 +42,7 @@ class Chione::World
42
42
  @systems = {}
43
43
  @managers = {}
44
44
 
45
- @subscriptions = Hash.new do |h,k|
46
- h[ k ] = Set.new
47
- end
45
+ @subscriptions = Hash.new {|h,k| h[k] = Set.new }
48
46
  @defer_events = true
49
47
  @deferred_events = []
50
48
 
@@ -52,8 +50,9 @@ class Chione::World
52
50
  @world_threads = ThreadGroup.new
53
51
 
54
52
  @entities_by_component = Hash.new {|h,k| h[k] = Set.new }
53
+ @components_by_entity = Hash.new {|h, k| h[k] = {} }
55
54
 
56
- @timing_event_count = 0
55
+ @tick_count = 0
57
56
  end
58
57
 
59
58
 
@@ -63,7 +62,7 @@ class Chione::World
63
62
 
64
63
  ##
65
64
  # The number of times the event loop has executed.
66
- attr_reader :timing_event_count
65
+ attr_accessor :tick_count
67
66
 
68
67
  ##
69
68
  # The Hash of all Entities in the World, keyed by ID
@@ -85,6 +84,16 @@ class Chione::World
85
84
  # The Thread object running the World's IO reactor loop
86
85
  attr_reader :main_thread
87
86
 
87
+ ##
88
+ # The Hash of Sets of Entities which have a particular component, keyed by
89
+ # Component class.
90
+ attr_reader :entities_by_component
91
+
92
+ ##
93
+ # The Hash of Hashes of Components which have been added to an Entity, keyed by
94
+ # the Entity's ID and the Component class.
95
+ attr_reader :components_by_entity
96
+
88
97
  ##
89
98
  # The Hash of event subscription callbacks registered with the world, keyed by
90
99
  # event pattern.
@@ -120,6 +129,15 @@ class Chione::World
120
129
  end
121
130
 
122
131
 
132
+ ### Step the world +delta_seconds+ into the future.
133
+ def tick( delta_seconds=1.0/60.0 )
134
+ self.publish( 'timing', delta_seconds, self.tick_count )
135
+ self.publish_deferred_events
136
+
137
+ self.tick_count += 1
138
+ end
139
+
140
+
123
141
  ### Start any Managers registered with the world.
124
142
  def start_managers
125
143
  self.log.info "Starting %d Managers" % [ self.managers.length ]
@@ -168,7 +186,7 @@ class Chione::World
168
186
 
169
187
  ### Returns +true+ if the World is running (i.e., if #start has been called)
170
188
  def running?
171
- return self.started? && self.timing_event_count.nonzero?
189
+ return self.started? && self.tick_count.nonzero?
172
190
  end
173
191
 
174
192
 
@@ -238,6 +256,7 @@ class Chione::World
238
256
 
239
257
  ### Send any deferred events to subscribers.
240
258
  def publish_deferred_events
259
+ self.log.debug "Publishing %d deferred events" % [ self.deferred_events.length ]
241
260
  while event = self.deferred_events.shift
242
261
  self.call_subscription_callbacks( *event )
243
262
  end
@@ -273,6 +292,10 @@ class Chione::World
273
292
  end
274
293
 
275
294
 
295
+ #
296
+ # :section: Entity API
297
+ #
298
+
276
299
  ### Return a new Chione::Entity for the receiving World, using the optional
277
300
  ### +archetype+ to populate it with components if it's specified.
278
301
  def create_entity( archetype=nil )
@@ -304,7 +327,8 @@ class Chione::World
304
327
  self.has_entity?( entity )
305
328
 
306
329
  self.publish( 'entity/destroyed', entity )
307
- @entities_by_component.each_value {|set| set.delete(entity) }
330
+ self.entities_by_component.each_value {|set| set.delete(entity.id) }
331
+ self.components_by_entity.delete( entity.id )
308
332
  @entities.delete( entity.id )
309
333
  end
310
334
 
@@ -320,74 +344,171 @@ class Chione::World
320
344
  end
321
345
 
322
346
 
323
- ### Register the specified +component+ as having been added to the specified
324
- ### +entity+.
325
- def add_component_for( entity, component )
347
+ #
348
+ # :section: Component API
349
+ #
350
+
351
+ ### Add the specified +component+ to the specified +entity+.
352
+ def add_component_to( entity, component, **init_values )
353
+ entity = entity.id if entity.respond_to?( :id )
354
+ component = Chione::Component( component, init_values )
355
+ component.entity_id = entity
356
+
326
357
  self.log.debug "Adding %p for %p" % [ component.class, entity ]
327
- @entities_by_component[ component.class ].add( entity )
358
+ self.entities_by_component[ component.class ].add( entity )
359
+ component_hash = self.components_by_entity[ entity ]
360
+ component_hash[ component.class ] = component
361
+
362
+ self.update_entity_caches( entity, component_hash )
328
363
  end
364
+ alias_method :add_component_for, :add_component_to
329
365
 
330
366
 
331
- ### Return an Enumerator of the Entities that have a Component composition that
332
- ### is compatible with the specified +system+'s aspect.
333
- def entities_for( system )
334
- system = system.class unless system.is_a?( Class )
335
- return self.entities_with( system.aspect ).to_enum
367
+ ### Return a Hash of the Component instances associated with +entity+, keyed by
368
+ ### their class.
369
+ def components_for( entity )
370
+ entity = entity.id if entity.respond_to?( :id )
371
+ return self.components_by_entity[ entity ].dup
336
372
  end
337
373
 
338
374
 
339
- ### Return an Array of all entities that match the specified +aspect+.
340
- def entities_with( aspect )
341
- initial_set = if aspect.one_of.empty?
342
- @entities_by_component.values
343
- else
344
- @entities_by_component.values_at( *aspect.one_of )
345
- end
375
+ ### Return the Component instance of the specified +component_class+ that's
376
+ ### associated with the given +entity+, if it has one.
377
+ def get_component_for( entity, component_class )
378
+ entity = entity.id if entity.respond_to?( :id )
379
+ return self.components_by_entity[ entity ][ component_class ]
380
+ end
381
+
382
+
383
+ ### Remove the specified +component+ from the given +entity+. If +component+ is
384
+ ### a Component subclass, any instance of it will be removed. If it's a
385
+ ### Component instance, it will be removed iff it is the same instance associated
386
+ ### with the given +entity+.
387
+ def remove_component_from( entity, component )
388
+ entity = entity.id if entity.respond_to?( :id )
389
+ if component.is_a?( Class )
390
+ self.entities_by_component[ component ].delete( entity )
391
+ component_hash = self.components_by_entity[ entity ]
392
+ component_hash.delete( component )
393
+ self.update_entity_caches( entity, component_hash )
394
+ else
395
+ self.remove_component_from( entity, component.class ) if
396
+ self.has_component_for?( entity, component )
397
+ end
398
+ end
399
+ alias_method :remove_component_for, :remove_component_from
346
400
 
347
- with_one = initial_set.reduce( :| )
348
- with_all = @entities_by_component.values_at( *aspect.all_of ).reduce( with_one, :& )
349
- without_any = @entities_by_component.values_at( *aspect.none_of ).reduce( with_all, :- )
350
401
 
351
- return without_any
402
+ ### Return +true+ if the specified +entity+ has the given +component+. If
403
+ ### +component+ is a Component subclass, any instance of it will test +true+. If
404
+ ### +component+ is a Component instance, it will only test +true+ if the
405
+ ### +entity+ is associated with that particular instance.
406
+ def has_component_for?( entity, component )
407
+ entity = entity.id if entity.respond_to?( :id )
408
+ if component.is_a?( Class )
409
+ return self.components_by_entity[ entity ].key?( component )
410
+ else
411
+ return self.components_by_entity[ entity ][ component.class ] == component
412
+ end
352
413
  end
353
414
 
354
415
 
416
+ #
417
+ # :section: System API
418
+ #
419
+
355
420
  ### Add an instance of the specified +system_type+ to the world and return it.
356
421
  ### It will replace any existing system of the same type.
357
422
  def add_system( system_type, *args )
358
423
  system_obj = system_type.new( self, *args )
359
- @systems[ system_type ] = system_obj
424
+ self.systems[ system_type ] = system_obj
360
425
 
361
426
  if self.running?
362
427
  self.log.info "Starting %p added to running world." % [ system_type ]
363
428
  system_obj.start
364
429
  end
365
430
 
366
- self.publish( 'system/added', system_type.name )
431
+ self.publish( 'system/added', system_obj )
367
432
  return system_obj
368
433
  end
369
434
 
370
435
 
436
+ ### Remove the instance of the specified +system_type+ from the world and return
437
+ ### it if it's been added. Returns +nil+ if no instance of the specified
438
+ ### +system_type+ was added.
439
+ def remove_system( system_type )
440
+ system_obj = self.systems.delete( system_type ) or return nil
441
+
442
+ self.publish( 'system/removed', system_obj )
443
+
444
+ if self.running?
445
+ self.log.info "Stopping %p before being removed from runnning world." % [ system_type ]
446
+ system_obj.stop
447
+ end
448
+
449
+ return system_obj
450
+ end
451
+
452
+
453
+ ### Return an Array of all entities that match the specified +aspect+.
454
+ def entities_with( aspect )
455
+ return aspect.matching_entities( self.entities_by_component )
456
+ end
457
+
458
+
459
+ #
460
+ # :section: Manager API
461
+ #
462
+
371
463
  ### Add an instance of the specified +manager_type+ to the world and return it.
372
464
  ### It will replace any existing manager of the same type.
373
465
  def add_manager( manager_type, *args )
374
466
  manager_obj = manager_type.new( self, *args )
375
- @managers[ manager_type ] = manager_obj
467
+ self.managers[ manager_type ] = manager_obj
376
468
 
377
469
  if self.running?
378
470
  self.log.info "Starting %p added to running world." % [ manager_type ]
379
471
  manager_obj.start
380
472
  end
381
473
 
382
- self.publish( 'manager/added', manager_type.name )
474
+ self.publish( 'manager/added', manager_obj )
475
+ return manager_obj
476
+ end
477
+
478
+
479
+ ### Remove the instance of the specified +manager_type+ from the world and
480
+ ### return it if it's been added. Returns +nil+ if no instance of the specified
481
+ ### +manager_type+ was added.
482
+ def remove_manager( manager_type )
483
+ manager_obj = self.managers.delete( manager_type ) or return nil
484
+ self.publish( 'manager/removed', manager_obj )
485
+
486
+ if self.running?
487
+ self.log.info "Stopping %p removed from running world." % [ manager_type ]
488
+ manager_obj.stop
489
+ end
490
+
383
491
  return manager_obj
384
492
  end
385
493
 
386
494
 
495
+ # :section:
496
+
497
+
387
498
  #########
388
499
  protected
389
500
  #########
390
501
 
502
+ ### Update any entity caches in the system when an +entity+ has its +components+ hash changed.
503
+ def update_entity_caches( entity, components )
504
+ entity = entity.id if entity.respond_to?( :id )
505
+ self.log.debug " updating entity cache for %p" % [ entity ]
506
+ self.systems.each_value do |sys|
507
+ sys.entity_components_updated( entity, components )
508
+ end
509
+ end
510
+
511
+
391
512
  ### The loop the main thread executes after the world is started. The default
392
513
  ### implementation just broadcasts the +timing+ event, so you will likely want to
393
514
  ### override this if the main thread should do something else.
@@ -396,22 +517,18 @@ class Chione::World
396
517
  last_timing_event = Time.now
397
518
  interval = self.class.timing_event_interval
398
519
  self.defer_events = false
399
- @timing_event_count = 0
520
+ self.tick_count = 0
400
521
 
401
522
  loop do
402
523
  previous_time, last_timing_event = last_timing_event, Time.now
403
-
404
- self.publish( 'timing', last_timing_event - previous_time, @timing_event_count )
405
- self.publish_deferred_events
406
-
407
- @timing_event_count += 1
524
+ self.tick( last_timing_event - previous_time )
408
525
  remaining_time = interval - (Time.now - last_timing_event)
409
526
 
410
527
  if remaining_time > 0
411
528
  sleep( remaining_time )
412
529
  else
413
530
  self.log.warn "Timing loop %d exceeded `timing_event_interval` (by %0.6fs)" %
414
- [ @timing_event_count, remaining_time.abs ]
531
+ [ self.tick_count, remaining_time.abs ]
415
532
  end
416
533
  end
417
534