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