chione 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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