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.
- checksums.yaml +7 -0
- checksums.yaml.gz.sig +0 -0
- data.tar.gz.sig +0 -0
- data/.gemtest +0 -0
- data/.rdoc_options +22 -0
- data/.simplecov +14 -0
- data/ChangeLog +114 -0
- data/History.rdoc +9 -0
- data/Manifest.txt +27 -0
- data/README.rdoc +96 -0
- data/Rakefile +92 -0
- data/lib/chione.rb +37 -0
- data/lib/chione/aspect.rb +163 -0
- data/lib/chione/assemblage.rb +64 -0
- data/lib/chione/behaviors.rb +31 -0
- data/lib/chione/component.rb +43 -0
- data/lib/chione/entity.rb +80 -0
- data/lib/chione/manager.rb +44 -0
- data/lib/chione/mixins.rb +91 -0
- data/lib/chione/system.rb +69 -0
- data/lib/chione/world.rb +358 -0
- data/spec/chione/aspect_spec.rb +143 -0
- data/spec/chione/assemblage_spec.rb +60 -0
- data/spec/chione/component_spec.rb +54 -0
- data/spec/chione/entity_spec.rb +109 -0
- data/spec/chione/manager_spec.rb +39 -0
- data/spec/chione/mixins_spec.rb +94 -0
- data/spec/chione/system_spec.rb +72 -0
- data/spec/chione/world_spec.rb +451 -0
- data/spec/chione_spec.rb +11 -0
- data/spec/spec_helper.rb +34 -0
- metadata +250 -0
- metadata.gz.sig +0 -0
@@ -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
|
data/lib/chione/world.rb
ADDED
@@ -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
|
+
|