chione 0.0.2

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.
@@ -0,0 +1,64 @@
1
+ # -*- ruby -*-
2
+ #encoding: utf-8
3
+
4
+ require 'loggability'
5
+
6
+ require 'chione' unless defined?( Chione )
7
+
8
+
9
+ # An Assemblage mixin for defining factories for commmon entity configurations.
10
+ module Chione::Assemblage
11
+ extend Loggability
12
+
13
+ # Loggability API -- log to the chione logger
14
+ log_to :chione
15
+
16
+
17
+ ### Extension callback -- add assemblage functionality to an extended +object+.
18
+ def self::extended( object )
19
+ super
20
+ object.extend( Loggability )
21
+ object.log_to( :chione )
22
+ object.components ||= {}
23
+ end
24
+
25
+
26
+ ### Inclusion callback -- add the components from this assemblage to those in
27
+ ### the specified +mod+.
28
+ def included( mod )
29
+ super
30
+ self.log.debug "Including %d components in %p" % [ self.components.length, mod ]
31
+ self.components.each do |component_type, args|
32
+ self.log.debug "Adding %p to %p from %p" % [ component_type, mod, self ]
33
+ mod.add( component_type, *args )
34
+ end
35
+ end
36
+
37
+
38
+ ##
39
+ # The Hash of component types and initialization values to add to entities
40
+ # constructed by this Assemblage.
41
+ attr_accessor :components
42
+
43
+
44
+ ### Add a +component_type+ to the list used when constructing a new entity from
45
+ ### the current Assemblage. The component will be instantiated using the specified
46
+ ### +init_args+.
47
+ def add( component_type, *init_args )
48
+ self.components[ component_type ] = init_args
49
+ end
50
+
51
+
52
+ ### Construct a new entity for the specified +world+ with all of the assemblage's
53
+ ### components.
54
+ def construct_for( world )
55
+ entity = world.create_entity
56
+ self.components.each do |component_type, args|
57
+ component = component_type.new( *args )
58
+ entity.add_component( component )
59
+ end
60
+
61
+ return entity
62
+ end
63
+
64
+ end # module Chione::Assemblage
@@ -0,0 +1,31 @@
1
+ # -*- ruby -*-
2
+ #encoding: utf-8
3
+
4
+ require 'rspec'
5
+ require 'chione'
6
+
7
+
8
+ RSpec.shared_examples "a Chione Component" do |args|
9
+
10
+ if args.key?( :with_fields )
11
+
12
+ args[:with_fields].each do |field|
13
+
14
+ it "declares a #{field} field" do
15
+ expect( described_class.fields ).to include( field.to_sym )
16
+ end
17
+
18
+ end
19
+
20
+ else
21
+
22
+ it "declares one or more fields" do
23
+ expect( described_class.fields ).to_not be_empty
24
+ end
25
+
26
+ end
27
+
28
+ end
29
+
30
+
31
+
@@ -0,0 +1,43 @@
1
+ # -*- ruby -*-
2
+ #encoding: utf-8
3
+
4
+ require 'chione' unless defined?( Chione )
5
+
6
+ # The Component (data) class
7
+ class Chione::Component
8
+
9
+ # The Hash of fields implemented by the component
10
+ class << self
11
+ attr_accessor :fields
12
+ end
13
+
14
+
15
+ ### Declare a field for the component named +name+, with a default value of
16
+ ### +default+.
17
+ def self::field( name, default: nil )
18
+ self.fields ||= {}
19
+ self.fields[ name ] = default
20
+ attr_accessor( name )
21
+ end
22
+
23
+
24
+ ### Create a new component with the specified +values+.
25
+ def initialize( values={} )
26
+ if self.class.fields
27
+ self.class.fields.each do |name, default|
28
+ self.method( "#{name}=" ).call( values[name] || deep_copy(default) )
29
+ end
30
+ end
31
+ end
32
+
33
+
34
+ #######
35
+ private
36
+ #######
37
+
38
+ ### Make a deep copy of the specified +value+.
39
+ def deep_copy( value )
40
+ Marshal.load( Marshal.dump(value) )
41
+ end
42
+
43
+ end # class Chione::Component
@@ -0,0 +1,80 @@
1
+ # -*- ruby -*-
2
+ #encoding: utf-8
3
+
4
+ require 'loggability'
5
+ require 'securerandom'
6
+
7
+ require 'chione' unless defined?( Chione )
8
+
9
+ # The Entity (identity) class
10
+ class Chione::Entity
11
+ extend Loggability
12
+
13
+ # Loggability API -- send logs to the Chione logger
14
+ log_to :chione
15
+
16
+
17
+ ### Return an ID for an Entity.
18
+ def self::make_new_id
19
+ return Chione.uuid.generate
20
+ end
21
+
22
+
23
+ ### Create a new Entity for the specified +world+, and use the specified +id+ if
24
+ ### given. If no +id+ is given, one will be automatically generated.
25
+ def initialize( world, id=nil )
26
+ @world = world
27
+ @id = id || self.class.make_new_id
28
+ @components = {}
29
+ end
30
+
31
+
32
+ ######
33
+ public
34
+ ######
35
+
36
+ # The Entity's ID
37
+ attr_reader :id
38
+
39
+ # The World the Entity belongs to
40
+ attr_reader :world
41
+
42
+ ##
43
+ # The Hash of this entity's Components
44
+ attr_reader :components
45
+
46
+
47
+ ### Add the specified +component+ to the entity. It will replace any existing component
48
+ ### of the same type.
49
+ def add_component( component )
50
+ self.components[ component.class ] = component
51
+ self.world.add_component_for( self, component )
52
+ end
53
+
54
+
55
+ ### Fetch the first Component of the specified +types+ that belongs to the entity. If the
56
+ ### Entity doesn't have any of the specified types of Component, raises a KeyError.
57
+ def get_component( *types )
58
+ found_type = types.find {|type| self.components[type] } or
59
+ raise KeyError, "entity %s doesn't have any of %p" % [ self.id, types ]
60
+ return self.components[ found_type ]
61
+ end
62
+
63
+
64
+ ### Returns +true+ if this entity has the specified +component+.
65
+ def has_component?( component )
66
+ return self.components.key?( component )
67
+ end
68
+
69
+
70
+ ### Return the Entity as a human-readable string suitable for debugging.
71
+ def inspect
72
+ return "#<%p:%0#x ID=%s (%s)>" % [
73
+ self.class,
74
+ self.object_id * 2,
75
+ self.id,
76
+ self.components.keys.map( &:name ).sort.join( '+' )
77
+ ]
78
+ end
79
+
80
+ end # class Chione::Entity
@@ -0,0 +1,44 @@
1
+ # -*- ruby -*-
2
+ #encoding: utf-8
3
+
4
+ require 'loggability'
5
+
6
+ require 'chione' unless defined?( Chione )
7
+
8
+
9
+ # The Manager class
10
+ class Chione::Manager
11
+ extend Loggability
12
+
13
+ # Loggability API -- send logs to the Chione logger
14
+ log_to :chione
15
+
16
+
17
+ ### Create a new Chione::Manager for the specified +world+.
18
+ def initialize( world, * )
19
+ @world = world
20
+ end
21
+
22
+
23
+ ######
24
+ public
25
+ ######
26
+
27
+ # The World which the Manager belongs to
28
+ attr_reader :world
29
+
30
+
31
+ ### Start the Manager as the world is starting. Derivatives must implement this
32
+ ### method.
33
+ def start
34
+ raise NotImplementedError, "%p does not implement required method #start" % [ self.class ]
35
+ end
36
+
37
+
38
+ ### Stop the Manager as the world is stopping. Derivatives must implement this
39
+ ### method.
40
+ def stop
41
+ raise NotImplementedError, "%p does not implement required method #stop" % [ self.class ]
42
+ end
43
+
44
+ end # class Chione::Manager
@@ -0,0 +1,91 @@
1
+ # -*- ruby -*-
2
+ #encoding: utf-8
3
+
4
+ require 'chione' unless defined?( Chione )
5
+
6
+
7
+ module Chione
8
+
9
+ # A collection of methods for declaring other methods.
10
+ #
11
+ # class MyClass
12
+ # extend Chione::MethodUtilities
13
+ #
14
+ # singleton_attr_accessor :types
15
+ # singleton_method_alias :kinds, :types
16
+ # end
17
+ #
18
+ # MyClass.types = [ :pheno, :proto, :stereo ]
19
+ # MyClass.kinds # => [:pheno, :proto, :stereo]
20
+ #
21
+ module MethodUtilities
22
+
23
+ ### Creates instance variables and corresponding methods that return their
24
+ ### values for each of the specified +symbols+ in the singleton of the
25
+ ### declaring object (e.g., class instance variables and methods if declared
26
+ ### in a Class).
27
+ def singleton_attr_reader( *symbols )
28
+ singleton_class.instance_exec( symbols ) do |attrs|
29
+ attr_reader( *attrs )
30
+ end
31
+ end
32
+
33
+ ### Create instance variables and corresponding methods that return
34
+ ### true or false values for each of the specified +symbols+ in the singleton
35
+ ### of the declaring object.
36
+ def singleton_predicate_reader( *symbols )
37
+ singleton_class.extend( Chione::MethodUtilities )
38
+ singleton_class.attr_predicate( *symbols )
39
+ end
40
+
41
+ ### Creates methods that allow assignment to the attributes of the singleton
42
+ ### of the declaring object that correspond to the specified +symbols+.
43
+ def singleton_attr_writer( *symbols )
44
+ singleton_class.instance_exec( symbols ) do |attrs|
45
+ attr_writer( *attrs )
46
+ end
47
+ end
48
+
49
+ ### Creates readers and writers that allow assignment to the attributes of
50
+ ### the singleton of the declaring object that correspond to the specified
51
+ ### +symbols+.
52
+ def singleton_attr_accessor( *symbols )
53
+ symbols.each do |sym|
54
+ singleton_class.__send__( :attr_accessor, sym )
55
+ end
56
+ end
57
+
58
+ ### Create predicate methods and writers that allow assignment to the attributes
59
+ ### of the singleton of the declaring object that correspond to the specified
60
+ ### +symbols+.
61
+ def singleton_predicate_accessor( *symbols )
62
+ singleton_class.extend( Chione::MethodUtilities )
63
+ singleton_class.attr_predicate_accessor( *symbols )
64
+ end
65
+
66
+ ### Creates an alias for the +original+ method named +newname+.
67
+ def singleton_method_alias( newname, original )
68
+ singleton_class.__send__( :alias_method, newname, original )
69
+ end
70
+
71
+
72
+ ### Create a reader in the form of a predicate for the given +attrname+.
73
+ def attr_predicate( attrname )
74
+ attrname = attrname.to_s.chomp( '?' )
75
+ define_method( "#{attrname}?" ) do
76
+ instance_variable_get( "@#{attrname}" ) ? true : false
77
+ end
78
+ end
79
+
80
+
81
+ ### Create a reader in the form of a predicate for the given +attrname+
82
+ ### as well as a regular writer method.
83
+ def attr_predicate_accessor( attrname )
84
+ attrname = attrname.to_s.chomp( '?' )
85
+ attr_writer( attrname )
86
+ attr_predicate( attrname )
87
+ end
88
+
89
+ end # module MethodUtilities
90
+
91
+ end # module Chione
@@ -0,0 +1,69 @@
1
+ # -*- ruby -*-
2
+ #encoding: utf-8
3
+
4
+ require 'loggability'
5
+
6
+ require 'chione' unless defined?( Chione )
7
+ require 'chione/mixins'
8
+ require 'chione/aspect'
9
+
10
+
11
+ # The System (behavior) class
12
+ class Chione::System
13
+ extend Loggability,
14
+ Chione::MethodUtilities
15
+
16
+ # Loggability API -- send logs to the Chione logger
17
+ log_to :chione
18
+
19
+
20
+ ### Add the specified +component_types+ to the Aspect of this System as being
21
+ ### required in any entities it processes.
22
+ def self::aspect( all_of: nil, one_of: nil, none_of: nil )
23
+ @aspect ||= Chione::Aspect.new
24
+
25
+ @aspect = @aspect.with_all_of( all_of ) if all_of
26
+ @aspect = @aspect.with_one_of( one_of ) if one_of
27
+ @aspect = @aspect.with_none_of( none_of ) if none_of
28
+
29
+ return @aspect
30
+ end
31
+ singleton_method_alias :for_entities_that_have, :aspect
32
+
33
+
34
+ ### Create a new Chione::System for the specified +world+.
35
+ def initialize( world, * )
36
+ @world = world
37
+ @thread = nil
38
+ end
39
+
40
+
41
+ ######
42
+ public
43
+ ######
44
+
45
+ # The World which the System belongs to
46
+ attr_reader :world
47
+
48
+
49
+ ### Start the system.
50
+ def start
51
+ self.log.info "Starting the %p" % [ self.class ]
52
+ @thread = Thread.new( &self.method(:process_loop) )
53
+ end
54
+
55
+
56
+ ### Stop the system.
57
+ def stop
58
+ @thread.kill
59
+ end
60
+
61
+
62
+ ### The main loop of the system -- process entities that this system is
63
+ ### interested in at an appropriate interval.
64
+ def process_loop
65
+ raise NotImplementedError, "%p does not implement required method #process_loop" %
66
+ [ self.class ]
67
+ end
68
+
69
+ end # class Chione::System
@@ -0,0 +1,358 @@
1
+ # -*- ruby -*-
2
+ #encoding: utf-8
3
+
4
+ require 'set'
5
+ require 'loggability'
6
+ require 'configurability'
7
+
8
+ require 'chione' unless defined?( Chione )
9
+ require 'chione/mixins'
10
+
11
+
12
+ # The main ECS container
13
+ class Chione::World
14
+ extend Loggability,
15
+ Configurability,
16
+ Chione::MethodUtilities
17
+
18
+ # Loggability API -- send logs to the Chione logger
19
+ log_to :chione
20
+
21
+ # Configurability API -- use the 'gameworld' section of the config
22
+ config_key :gameworld
23
+
24
+
25
+ # Default config tunables
26
+ CONFIG_DEFAULTS = {
27
+ max_stop_wait: 5,
28
+ timing_event_interval: 1,
29
+ }
30
+
31
+
32
+ ##
33
+ # :singleton-method:
34
+ # Configurable: The maximum number of seconds to wait for any one System
35
+ # or Manager thread to exit before killing it when shutting down.
36
+ singleton_attr_accessor :max_stop_wait
37
+ @max_stop_wait = CONFIG_DEFAULTS[:max_stop_wait]
38
+
39
+ ##
40
+ # :singleton-method:
41
+ # Configurable: The number of seconds between timing events.
42
+ singleton_attr_accessor :timing_event_interval
43
+ @timing_event_interval = CONFIG_DEFAULTS[ :timing_event_interval ]
44
+
45
+
46
+ ### Configurability API -- configure the GameWorld.
47
+ def self::configure( config=nil )
48
+ config = self.defaults.merge( config || {} )
49
+
50
+ self.max_stop_wait = config[:max_stop_wait]
51
+ self.timing_event_interval = config[:timing_event_interval]
52
+ end
53
+
54
+
55
+
56
+
57
+ ### Create a new Chione::World
58
+ def initialize
59
+ @entities = {}
60
+ @systems = {}
61
+ @managers = {}
62
+
63
+ @subscriptions = Hash.new do |h,k|
64
+ h[ k ] = Set.new
65
+ end
66
+
67
+ @main_thread = nil
68
+ @world_threads = ThreadGroup.new
69
+
70
+ @entities_by_component = Hash.new {|h,k| h[k] = Set.new }
71
+
72
+ @timing_event_count = 0
73
+ end
74
+
75
+
76
+ ######
77
+ public
78
+ ######
79
+
80
+ # The number of times the event loop has executed.
81
+ attr_reader :timing_event_count
82
+
83
+ # The Hash of all Entities in the World, keyed by ID
84
+ attr_reader :entities
85
+
86
+ # The Hash of all Systems currently in the World, keyed by class.
87
+ attr_reader :systems
88
+
89
+ # The Hash of all Managers currently in the World, keyed by class.
90
+ attr_reader :managers
91
+
92
+ # The ThreadGroup that contains all Threads managed by the World.
93
+ attr_reader :world_threads
94
+
95
+ # The Thread object running the World's IO reactor loop
96
+ attr_reader :io_thread
97
+
98
+ # The Hash of event subscription callbacks registered with the world, keyed by
99
+ # event pattern.
100
+ attr_reader :subscriptions
101
+
102
+
103
+ ### Start the world; returns the Thread in which the world is running.
104
+ def start
105
+ @main_thread = Thread.new do
106
+ Thread.current.abort_on_exception = true
107
+ self.log.info "Main thread (%s) started." % [ Thread.current ]
108
+ @world_threads.add( Thread.current )
109
+ @world_threads.enclose
110
+
111
+ self.start_managers
112
+ self.start_systems
113
+
114
+ self.timing_loop
115
+ end
116
+
117
+ self.log.info "Started main World thread: %p" % [ @main_thread ]
118
+ return @main_thread
119
+ end
120
+
121
+
122
+ ### Start any Managers registered with the world.
123
+ def start_managers
124
+ self.log.info "Starting %d Managers" % [ self.managers.length ]
125
+ self.managers.each do |manager_class, mgr|
126
+ self.log.debug " starting %p" % [ manager_class ]
127
+ start = Time.now
128
+ mgr.start
129
+ finish = Time.now
130
+ self.log.debug " started in %0.5fs" % [ finish - start ]
131
+ end
132
+ end
133
+
134
+
135
+ ### Start any Systems registered with the world.
136
+ def start_systems
137
+ self.log.info "Starting %d Systems" % [ self.systems.length ]
138
+ self.systems.each do |system_class, sys|
139
+ self.log.debug " starting %p" % [ system_class ]
140
+ start = Time.now
141
+ sys.start
142
+ finish = Time.now
143
+ self.log.debug " started in %0.5fs" % [ finish - start ]
144
+ end
145
+ end
146
+
147
+
148
+ ### Returns +true+ if the World has been started (but is not necessarily running yet).
149
+ def started?
150
+ return @main_thread && @main_thread.alive?
151
+ end
152
+
153
+
154
+ ### Returns +true+ if the World is running (i.e., if #start has been called)
155
+ def running?
156
+ return self.started? && self.timing_event_count.nonzero?
157
+ end
158
+
159
+
160
+ ### Stop the world.
161
+ def stop
162
+ self.systems.each {|_, sys| sys.stop }
163
+ self.managers.each {|_, mgr| mgr.stop }
164
+
165
+ self.world_threads.list.each do |thr|
166
+ next if thr == @main_thread
167
+ thr.join( self.class.max_stop_wait )
168
+ end
169
+
170
+ self.stop_timing_loop
171
+ end
172
+
173
+
174
+ ### Halt the main timing loop. By default, this just kills the world's main thread.
175
+ def stop_timing_loop
176
+ @main_thread.kill
177
+ end
178
+
179
+
180
+ ### Subscribe to events with the specified +event_name+. Returns the callback object
181
+ ### for later unsubscribe calls.
182
+ def subscribe( event_name, callback=nil )
183
+ callback = Proc.new if !callback && block_given?
184
+
185
+ raise LocalJumpError, "no callback given" unless callback
186
+ raise ArgumentError, "callback is not callable" unless callback.respond_to?( :call )
187
+ raise ArgumentError, "callback has wrong arity" unless
188
+ callback.arity >= 2 || callback.arity < 0
189
+
190
+ @subscriptions[ event_name ].add( callback )
191
+
192
+ return callback
193
+ end
194
+
195
+
196
+ ### Unsubscribe from events that publish to the specified +callback+.
197
+ def unsubscribe( callback )
198
+ @subscriptions.values.each {|cbset| cbset.delete(callback) }
199
+ end
200
+
201
+
202
+ ### Publish an event with the specified +event_name+, calling any subscribers with
203
+ ### the specified +payload+.
204
+ def publish( event_name, *payload )
205
+ self.log.debug "Publishing a %p event: %p" % [ event_name, payload ]
206
+ @subscriptions.each do |pattern, callbacks|
207
+ next unless File.fnmatch?( pattern, event_name, File::FNM_EXTGLOB|File::FNM_PATHNAME )
208
+
209
+ callbacks.each do |callback|
210
+ begin
211
+ callback.call( event_name, payload )
212
+ rescue => err
213
+ self.log.error "%p while calling %p for a %p event: %s" %
214
+ [ err.class, callback, event_name, err.message ]
215
+ callbacks.delete( callback )
216
+ end
217
+ end
218
+ end
219
+ end
220
+
221
+
222
+ ### Return a new Chione::Entity for the receiving World.
223
+ def create_entity( assemblage=nil )
224
+ entity = if assemblage
225
+ assemblage.construct_for( self )
226
+ else
227
+ Chione::Entity.new( self )
228
+ end
229
+
230
+ @entities[ entity.id ] = entity
231
+
232
+ self.publish( 'entity/created', entity )
233
+ return entity
234
+ end
235
+
236
+
237
+ ### Destroy the specified entity and remove it from any registered
238
+ ### systems/managers.
239
+ def destroy_entity( entity )
240
+ raise ArgumentError, "%p does not contain entity %p" % [ self, entity ] unless
241
+ self.has_entity?( entity )
242
+
243
+ self.publish( 'entity/destroyed', entity )
244
+ @entities_by_component.each_value {|set| set.delete(entity) }
245
+ @entities.delete( entity.id )
246
+ end
247
+
248
+
249
+ ### Returns +true+ if the world contains the specified +entity+ or an entity
250
+ ### with +entity+ as the ID.
251
+ def has_entity?( entity )
252
+ if entity.respond_to?( :id )
253
+ return @entities.key?( entity.id )
254
+ else
255
+ return @entities.key?( entity )
256
+ end
257
+ end
258
+
259
+
260
+ ### Register the specified +component+ as having been added to the specified
261
+ ### +entity+.
262
+ def add_component_for( entity, component )
263
+ @entities_by_component[ component.class ].add( entity )
264
+ end
265
+
266
+
267
+ ### Return the Entities that have a Component composition that is compatible with
268
+ ### the specified +system+'s aspect.
269
+ def entities_for( system )
270
+ system = system.class unless system.is_a?( Class )
271
+ return self.entities_with( system.aspect )
272
+ end
273
+
274
+
275
+ ### Return an Array of all entities that match the specified +aspect+.
276
+ def entities_with( aspect )
277
+ initial_set = if aspect.one_of.empty?
278
+ @entities_by_component.values
279
+ else
280
+ @entities_by_component.values_at( *aspect.one_of )
281
+ end
282
+
283
+ with_one = initial_set.reduce( :| )
284
+ with_all = @entities_by_component.values_at( *aspect.all_of ).reduce( with_one, :& )
285
+ without_any = @entities_by_component.values_at( *aspect.none_of ).reduce( with_all, :- )
286
+
287
+ return without_any
288
+ end
289
+
290
+
291
+ ### Add an instance of the specified +system_type+ to the world and return it.
292
+ ### It will replace any existing system of the same type.
293
+ def add_system( system_type, *args )
294
+ system_obj = system_type.new( self, *args )
295
+ @systems[ system_type ] = system_obj
296
+
297
+ if self.running?
298
+ self.log.info "Starting %p added to running world." % [ system_type ]
299
+ system_obj.start
300
+ end
301
+
302
+ self.publish( 'system/added', system_type )
303
+ return system_obj
304
+ end
305
+
306
+
307
+ ### Add an instance of the specified +manager_type+ to the world and return it.
308
+ ### It will replace any existing manager of the same type.
309
+ def add_manager( manager_type, *args )
310
+ manager_obj = manager_type.new( self, *args )
311
+ @managers[ manager_type ] = manager_obj
312
+
313
+ if self.running?
314
+ self.log.info "Starting %p added to running world." % [ manager_type ]
315
+ manager_obj.start
316
+ end
317
+
318
+ self.publish( 'manager/added', manager_type )
319
+ return manager_obj
320
+ end
321
+
322
+
323
+ #########
324
+ protected
325
+ #########
326
+
327
+ ### The loop the main thread executes after the world is started. The default
328
+ ### implementation just broadcasts the +timing+ event, so will likely want to
329
+ ### override this if the main thread should do something else.
330
+ def timing_loop
331
+ self.log.info "Starting timing loop."
332
+ last_timing_event = Time.now
333
+ interval = self.class.timing_event_interval
334
+ @timing_event_count = 0
335
+
336
+ loop do
337
+ previous_time, last_timing_event = last_timing_event, Time.now
338
+
339
+ self.publish( 'timing', last_timing_event - previous_time, @timing_event_count )
340
+
341
+ @timing_event_count += 1
342
+ remaining_time = interval - (Time.now - last_timing_event)
343
+
344
+ if remaining_time > 0
345
+ sleep( remaining_time )
346
+ else
347
+ self.log.warn "Timing loop %d exceeded `timing_event_interval` (by %0.6fs)" %
348
+ [ @timing_event_count, remaining_time.abs ]
349
+ end
350
+ end
351
+
352
+ ensure
353
+ self.log.info "Exiting timing loop."
354
+ end
355
+
356
+
357
+ end # class Chione::World
358
+