chione 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
+