chione 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +5 -5
- checksums.yaml.gz.sig +0 -0
- data.tar.gz.sig +0 -0
- data/ChangeLog +16 -2
- data/History.md +31 -0
- data/Manifest.txt +4 -0
- data/README.md +18 -11
- data/Rakefile +2 -0
- data/lib/chione.rb +7 -4
- data/lib/chione/archetype.rb +49 -3
- data/lib/chione/aspect.rb +52 -10
- data/lib/chione/component.rb +43 -8
- data/lib/chione/entity.rb +38 -28
- data/lib/chione/fixtures.rb +18 -0
- data/lib/chione/fixtures/entities.rb +32 -0
- data/lib/chione/iterating_system.rb +35 -0
- data/lib/chione/manager.rb +15 -0
- data/lib/chione/mixins.rb +22 -0
- data/lib/chione/system.rb +137 -12
- data/lib/chione/world.rb +155 -38
- data/spec/chione/archetype_spec.rb +14 -0
- data/spec/chione/aspect_spec.rb +90 -0
- data/spec/chione/component_spec.rb +37 -0
- data/spec/chione/entity_spec.rb +17 -24
- data/spec/chione/iterating_system_spec.rb +135 -0
- data/spec/chione/system_spec.rb +224 -27
- data/spec/chione/world_spec.rb +122 -59
- data/spec/spec_helper.rb +3 -0
- metadata +46 -14
- metadata.gz.sig +0 -0
data/lib/chione/entity.rb
CHANGED
@@ -6,12 +6,15 @@ require 'loggability'
|
|
6
6
|
require 'securerandom'
|
7
7
|
|
8
8
|
require 'chione' unless defined?( Chione )
|
9
|
+
require 'chione/mixins'
|
9
10
|
|
10
11
|
# The Entity (identity) class
|
11
12
|
class Chione::Entity
|
12
13
|
extend Loggability,
|
13
14
|
Deprecatable
|
14
15
|
|
16
|
+
include Chione::Inspection
|
17
|
+
|
15
18
|
# Loggability API -- send logs to the Chione logger
|
16
19
|
log_to :chione
|
17
20
|
|
@@ -25,9 +28,8 @@ class Chione::Entity
|
|
25
28
|
### Create a new Entity for the specified +world+, and use the specified +id+ if
|
26
29
|
### given. If no +id+ is given, one will be automatically generated.
|
27
30
|
def initialize( world, id=nil )
|
28
|
-
@world
|
29
|
-
@id
|
30
|
-
@components = {}
|
31
|
+
@world = world
|
32
|
+
@id = id || self.class.make_new_id
|
31
33
|
end
|
32
34
|
|
33
35
|
|
@@ -41,42 +43,50 @@ class Chione::Entity
|
|
41
43
|
# The World the Entity belongs to
|
42
44
|
attr_reader :world
|
43
45
|
|
44
|
-
|
45
|
-
|
46
|
-
|
46
|
+
|
47
|
+
### Return the components that the entity's World has registered for it as a Hash
|
48
|
+
### keyed by the Component class.
|
49
|
+
def components
|
50
|
+
return self.world.components_for( self )
|
51
|
+
end
|
47
52
|
|
48
53
|
|
49
|
-
### Add the specified +component+ to the entity.
|
50
|
-
### of the
|
51
|
-
|
52
|
-
|
53
|
-
self.
|
54
|
-
self.world.add_component_for( self, component )
|
54
|
+
### Add the specified +component+ to the entity. The +component+ can be a
|
55
|
+
### subclass of Chione::Component, an instance of such a subclass, or the name
|
56
|
+
### of a subclass. It will replace any existing component of the same type.
|
57
|
+
def add_component( component, **init_values )
|
58
|
+
self.world.add_component_to( self, component, init_values )
|
55
59
|
end
|
56
60
|
|
57
61
|
|
58
|
-
### Fetch the
|
59
|
-
###
|
60
|
-
def
|
61
|
-
|
62
|
-
raise KeyError, "entity %s doesn't have any of %p" % [ self.id, types ]
|
63
|
-
return self.components[ found_type ]
|
62
|
+
### Fetch the component of the specified +component_class+ that corresponds with the
|
63
|
+
### receiving entity. Returns +nil+ if so much component exists.
|
64
|
+
def get_component( component_class )
|
65
|
+
return self.world.get_component_for( self, component_class )
|
64
66
|
end
|
65
|
-
alias_method :get_component, :find_component
|
66
|
-
deprecate :get_component, message: 'Use #find_component instead', removal_version: '1.0'
|
67
67
|
|
68
68
|
|
69
|
-
###
|
70
|
-
|
71
|
-
|
69
|
+
### Remove the component of the specified +component_class+ that corresponds with the
|
70
|
+
### receiving entity. Returns the component instance if it was removed, or +nil+ if no
|
71
|
+
### Component of the specified type was registered to the entity.
|
72
|
+
def remove_component( component_class )
|
73
|
+
return self.world.remove_component_from( self, component_class )
|
72
74
|
end
|
73
75
|
|
74
76
|
|
75
|
-
###
|
76
|
-
def
|
77
|
-
return
|
78
|
-
|
79
|
-
|
77
|
+
### Returns +true+ if this entity has a component of the specified +component_class+.
|
78
|
+
def has_component?( component_class )
|
79
|
+
return self.world.has_component_for?( self, component_class )
|
80
|
+
end
|
81
|
+
|
82
|
+
|
83
|
+
#########
|
84
|
+
protected
|
85
|
+
#########
|
86
|
+
|
87
|
+
### Return the detailed part of the Entity's #inspect output
|
88
|
+
def inspect_details
|
89
|
+
return "ID=%s (%s)" % [
|
80
90
|
self.id,
|
81
91
|
self.components.keys.map( &:name ).sort.join( '+' )
|
82
92
|
]
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# -*- ruby -*-
|
2
|
+
# encoding: utf-8
|
3
|
+
# frozen_string_literal: true
|
4
|
+
|
5
|
+
require 'faker'
|
6
|
+
require 'fluent_fixtures'
|
7
|
+
|
8
|
+
require 'chione' unless defined?( Chione )
|
9
|
+
|
10
|
+
|
11
|
+
module Chione::Fixtures
|
12
|
+
extend FluentFixtures::Collection
|
13
|
+
|
14
|
+
# Set the path to use when finding fixtures for this collection
|
15
|
+
fixture_path_prefix 'chione/fixtures'
|
16
|
+
|
17
|
+
end # module Chione
|
18
|
+
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# -*- ruby -*-
|
2
|
+
#encoding: utf-8
|
3
|
+
|
4
|
+
require 'faker'
|
5
|
+
|
6
|
+
require 'chione/fixtures' unless defined?( Chione::Fixtures )
|
7
|
+
require 'chione/entity'
|
8
|
+
|
9
|
+
|
10
|
+
# Entity fixtures
|
11
|
+
module Chione::Fixtures::Entities
|
12
|
+
extend Chione::Fixtures
|
13
|
+
|
14
|
+
fixtured_class Chione::Entity
|
15
|
+
|
16
|
+
base :entity
|
17
|
+
|
18
|
+
decorator :with_id do |id|
|
19
|
+
@id = id
|
20
|
+
end
|
21
|
+
|
22
|
+
|
23
|
+
decorator :with_components do |*components|
|
24
|
+
components.each do |comp|
|
25
|
+
self.add_component( comp )
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
end # module Chione::Fixtures::Entities
|
30
|
+
|
31
|
+
|
32
|
+
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# -*- ruby -*-
|
2
|
+
#encoding: utf-8
|
3
|
+
|
4
|
+
require 'chione'
|
5
|
+
require 'chione/system'
|
6
|
+
|
7
|
+
|
8
|
+
|
9
|
+
# Process all entities matching an aspect iteratively for every World timing
|
10
|
+
# loop iteration.
|
11
|
+
class Chione::IteratingSystem < Chione::System
|
12
|
+
|
13
|
+
|
14
|
+
every_tick do |*|
|
15
|
+
world = self.world
|
16
|
+
|
17
|
+
self.class.aspects.each do |name, aspect|
|
18
|
+
self.log.debug "Iterating over entities with '%s'" % [ name ]
|
19
|
+
world.entities_with( aspect ).each do |entity|
|
20
|
+
self.process( name, entity, world.components_for(entity) )
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
|
26
|
+
|
27
|
+
### Process the given +components+ (which match the system's Aspect) for the
|
28
|
+
### specified +entity_id+. Concrete subclasses are required to override this.
|
29
|
+
def process( aspect_name, entity_id, components )
|
30
|
+
raise NotImplementedError, "%p does not implement #%s" % [ self.class, __method__ ]
|
31
|
+
end
|
32
|
+
|
33
|
+
|
34
|
+
end # class Chione::IteratingSystem
|
35
|
+
|
data/lib/chione/manager.rb
CHANGED
@@ -5,6 +5,7 @@ require 'pluggability'
|
|
5
5
|
require 'loggability'
|
6
6
|
|
7
7
|
require 'chione' unless defined?( Chione )
|
8
|
+
require 'chione/mixins'
|
8
9
|
|
9
10
|
|
10
11
|
# The Manager class
|
@@ -12,6 +13,9 @@ class Chione::Manager
|
|
12
13
|
extend Loggability,
|
13
14
|
Pluggability
|
14
15
|
|
16
|
+
include Chione::Inspection
|
17
|
+
|
18
|
+
|
15
19
|
# Loggability API -- send logs to the Chione logger
|
16
20
|
log_to :chione
|
17
21
|
|
@@ -46,4 +50,15 @@ class Chione::Manager
|
|
46
50
|
raise NotImplementedError, "%p does not implement required method #stop" % [ self.class ]
|
47
51
|
end
|
48
52
|
|
53
|
+
|
54
|
+
#########
|
55
|
+
protected
|
56
|
+
#########
|
57
|
+
|
58
|
+
### Return the detail part of the inspection string.
|
59
|
+
def inspect_details
|
60
|
+
return "for %p:%#016x" % [ self.world.class, self.world.object_id * 2 ]
|
61
|
+
end
|
62
|
+
|
63
|
+
|
49
64
|
end # class Chione::Manager
|
data/lib/chione/mixins.rb
CHANGED
@@ -88,4 +88,26 @@ module Chione
|
|
88
88
|
|
89
89
|
end # module MethodUtilities
|
90
90
|
|
91
|
+
|
92
|
+
# An extensible #inspect for Chione objects.
|
93
|
+
module Inspection
|
94
|
+
|
95
|
+
### Return a human-readable representation of the object suitable for debugging.
|
96
|
+
def inspect
|
97
|
+
return "#<%p:%#016x %s>" % [
|
98
|
+
self.class,
|
99
|
+
self.object_id * 2,
|
100
|
+
self.inspect_details,
|
101
|
+
]
|
102
|
+
end
|
103
|
+
|
104
|
+
|
105
|
+
### Return the detail portion of the inspect output for this object.
|
106
|
+
def inspect_details
|
107
|
+
return ''
|
108
|
+
end
|
109
|
+
|
110
|
+
end # module Inspection
|
111
|
+
|
112
|
+
|
91
113
|
end # module Chione
|
data/lib/chione/system.rb
CHANGED
@@ -15,6 +15,19 @@ class Chione::System
|
|
15
15
|
Pluggability,
|
16
16
|
Chione::MethodUtilities
|
17
17
|
|
18
|
+
include Chione::Inspection
|
19
|
+
|
20
|
+
|
21
|
+
# A Hash that auto-vivifies only its :default key
|
22
|
+
DEFAULT_ASPECT_HASH = Hash.new do |h,k|
|
23
|
+
if k == :default
|
24
|
+
h[k] = Chione::Aspect.new
|
25
|
+
else
|
26
|
+
nil
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
|
18
31
|
# Loggability API -- send logs to the Chione logger
|
19
32
|
log_to :chione
|
20
33
|
|
@@ -22,25 +35,73 @@ class Chione::System
|
|
22
35
|
plugin_prefixes 'chione/system'
|
23
36
|
|
24
37
|
|
38
|
+
##
|
39
|
+
# The Hash of Chione::Aspects that describe entities this system is interested
|
40
|
+
# in, keyed by name (a Symbol). A System which declares no aspects will have a
|
41
|
+
# +:default+ Aspect which matches all entities.
|
42
|
+
singleton_attr_reader :aspects
|
43
|
+
|
44
|
+
##
|
45
|
+
# Event handler tuples (event name, callback) that should be registered when the
|
46
|
+
# System is started.
|
47
|
+
singleton_attr_reader :event_handlers
|
48
|
+
|
49
|
+
|
25
50
|
### Add the specified +component_types+ to the Aspect of this System as being
|
26
51
|
### required in any entities it processes.
|
27
|
-
def self::aspect( *required, all_of: nil, one_of: nil, none_of: nil )
|
28
|
-
|
52
|
+
def self::aspect( name, *required, all_of: nil, one_of: nil, none_of: nil )
|
53
|
+
aspect = Chione::Aspect.new
|
29
54
|
|
30
55
|
all_of = required + Array( all_of )
|
31
56
|
|
32
|
-
|
33
|
-
|
34
|
-
|
57
|
+
aspect = aspect.with_all_of( all_of )
|
58
|
+
aspect = aspect.with_one_of( one_of ) if one_of
|
59
|
+
aspect = aspect.with_none_of( none_of ) if none_of
|
35
60
|
|
36
|
-
|
61
|
+
self.aspects[ name ] = aspect
|
62
|
+
end
|
63
|
+
|
64
|
+
|
65
|
+
### Declare a block that is called once whenever an event matching +event_name+ is
|
66
|
+
### broadcast to the World.
|
67
|
+
def self::on( event_name, &block )
|
68
|
+
raise LocalJumpError, "no block given" unless block
|
69
|
+
raise ArgumentError, "callback has wrong arity" unless block.arity >= 2 || block.arity < 0
|
70
|
+
|
71
|
+
method_name = "on_%s_event" % [ event_name.tr('/', '_') ]
|
72
|
+
self.log.debug "Making handler method #%s for %s events out of %p" %
|
73
|
+
[ method_name, event_name, block ]
|
74
|
+
define_method( method_name, &block )
|
75
|
+
|
76
|
+
self.event_handlers << [ event_name, method_name ]
|
77
|
+
end
|
78
|
+
|
79
|
+
|
80
|
+
### Declare a block that is called once every tick for each entity that matches the given
|
81
|
+
### +aspect+.
|
82
|
+
def self::every_tick( &block )
|
83
|
+
return self.on( 'timing' ) do |event_name, payload|
|
84
|
+
self.instance_exec( *payload, &block )
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
|
89
|
+
### Add some per-subclass data structures to inheriting +subclass+es.
|
90
|
+
def self::inherited( subclass )
|
91
|
+
super
|
92
|
+
subclass.instance_variable_set( :@aspects, DEFAULT_ASPECT_HASH.clone )
|
93
|
+
subclass.instance_variable_set( :@event_handlers, self.event_handlers&.dup || [] )
|
37
94
|
end
|
38
|
-
singleton_method_alias :for_entities_that_have, :aspect
|
39
95
|
|
40
96
|
|
41
97
|
### Create a new Chione::System for the specified +world+.
|
42
98
|
def initialize( world, * )
|
43
99
|
@world = world
|
100
|
+
@aspect_entities = self.class.aspects.each_with_object( {} ) do |(aspect_name, aspect), hash|
|
101
|
+
matching_set = world.entities_with( aspect )
|
102
|
+
self.log.debug "Initial Set with the %s aspect: %p" % [ aspect_name, matching_set]
|
103
|
+
hash[ aspect_name ] = matching_set
|
104
|
+
end
|
44
105
|
end
|
45
106
|
|
46
107
|
|
@@ -48,25 +109,89 @@ class Chione::System
|
|
48
109
|
public
|
49
110
|
######
|
50
111
|
|
112
|
+
##
|
51
113
|
# The World which the System belongs to
|
52
114
|
attr_reader :world
|
53
115
|
|
116
|
+
##
|
117
|
+
# The Hash of Sets of entity IDs which match the System's aspects, keyed by aspect name.
|
118
|
+
attr_reader :aspect_entities
|
119
|
+
|
54
120
|
|
55
121
|
### Start the system.
|
56
122
|
def start
|
57
|
-
self.log.info "Starting the %p
|
123
|
+
self.log.info "Starting the %p system; %d event handlers to register" %
|
124
|
+
[ self.class, self.class.event_handlers.length ]
|
125
|
+
self.class.event_handlers.each do |event_name, method_name|
|
126
|
+
callback = self.method( method_name )
|
127
|
+
self.log.info "Registering %p as a callback for '%s' events." % [ callback, event_name ]
|
128
|
+
self.world.subscribe( event_name, callback )
|
129
|
+
end
|
58
130
|
end
|
59
131
|
|
60
132
|
|
61
133
|
### Stop the system.
|
62
134
|
def stop
|
135
|
+
self.class.event_handlers.each do |_, method_name|
|
136
|
+
callback = self.method( method_name )
|
137
|
+
self.log.info "Unregistering subscription for %p." % [ callback ]
|
138
|
+
self.world.unsubscribe( callback )
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
|
143
|
+
### Return an Enumerator that yields the entities which match the given +aspect_name+.
|
144
|
+
def entities( aspect_name=:default )
|
145
|
+
return self.aspect_entities[ aspect_name ].to_enum( :each )
|
146
|
+
end
|
147
|
+
|
148
|
+
|
149
|
+
### Entity callback -- called whenever an entity has a component added to it or
|
150
|
+
### removed from it. Calls the appropriate callback (#inserted or #removed) if
|
151
|
+
### the component change caused it to belong to or stop belonging to one of the
|
152
|
+
### system's aspects.
|
153
|
+
def entity_components_updated( entity_id, components_hash )
|
154
|
+
self.class.aspects.each do |aspect_name, aspect|
|
155
|
+
entity_ids = self.aspect_entities[ aspect_name ]
|
156
|
+
|
157
|
+
if aspect.matches?( components_hash )
|
158
|
+
self.inserted( aspect_name, entity_id, components_hash ) if
|
159
|
+
entity_ids.add?( entity_id )
|
160
|
+
else
|
161
|
+
self.removed( aspect_name, entity_id, components_hash ) if
|
162
|
+
entity_ids.delete?( entity_id )
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
|
168
|
+
### Entity callback -- called whenever an entity has a component added to it
|
169
|
+
### that makes it start matching an aspect of the receiving System. The
|
170
|
+
### +aspect_name+ is the name of the Aspect it now matches, and the +components+
|
171
|
+
### are a Hash of the entity's components keyed by Class. By default this is a
|
172
|
+
### no-op.
|
173
|
+
def inserted( aspect_name, entity_id, components )
|
174
|
+
self.log.debug "Entity %s now matches the %s aspect." % [ entity_id, aspect_name ]
|
63
175
|
end
|
64
176
|
|
65
177
|
|
66
|
-
###
|
67
|
-
###
|
68
|
-
|
69
|
-
|
178
|
+
### Entity callback -- called whenever an entity has a component removed from it
|
179
|
+
### that makes it stop matching an aspect of the receiving System. The
|
180
|
+
### +aspect_name+ is the name of the Aspect it no longer matches, and the
|
181
|
+
### +components+ are a Hash of the entity's components keyed by Class. By
|
182
|
+
### default this is a no-op.
|
183
|
+
def removed( aspect_name, entity_id, components )
|
184
|
+
self.log.debug "Entity %s no longer matches the %s aspect." % [ entity_id, aspect_name ]
|
185
|
+
end
|
186
|
+
|
187
|
+
|
188
|
+
#########
|
189
|
+
protected
|
190
|
+
#########
|
191
|
+
|
192
|
+
### Return the detail part of the inspection string.
|
193
|
+
def inspect_details
|
194
|
+
return "for %p:%#016x" % [ self.world.class, self.world.object_id * 2 ]
|
70
195
|
end
|
71
196
|
|
72
197
|
end # class Chione::System
|
data/lib/chione/world.rb
CHANGED
@@ -42,9 +42,7 @@ class Chione::World
|
|
42
42
|
@systems = {}
|
43
43
|
@managers = {}
|
44
44
|
|
45
|
-
@subscriptions = Hash.new
|
46
|
-
h[ k ] = Set.new
|
47
|
-
end
|
45
|
+
@subscriptions = Hash.new {|h,k| h[k] = Set.new }
|
48
46
|
@defer_events = true
|
49
47
|
@deferred_events = []
|
50
48
|
|
@@ -52,8 +50,9 @@ class Chione::World
|
|
52
50
|
@world_threads = ThreadGroup.new
|
53
51
|
|
54
52
|
@entities_by_component = Hash.new {|h,k| h[k] = Set.new }
|
53
|
+
@components_by_entity = Hash.new {|h, k| h[k] = {} }
|
55
54
|
|
56
|
-
@
|
55
|
+
@tick_count = 0
|
57
56
|
end
|
58
57
|
|
59
58
|
|
@@ -63,7 +62,7 @@ class Chione::World
|
|
63
62
|
|
64
63
|
##
|
65
64
|
# The number of times the event loop has executed.
|
66
|
-
|
65
|
+
attr_accessor :tick_count
|
67
66
|
|
68
67
|
##
|
69
68
|
# The Hash of all Entities in the World, keyed by ID
|
@@ -85,6 +84,16 @@ class Chione::World
|
|
85
84
|
# The Thread object running the World's IO reactor loop
|
86
85
|
attr_reader :main_thread
|
87
86
|
|
87
|
+
##
|
88
|
+
# The Hash of Sets of Entities which have a particular component, keyed by
|
89
|
+
# Component class.
|
90
|
+
attr_reader :entities_by_component
|
91
|
+
|
92
|
+
##
|
93
|
+
# The Hash of Hashes of Components which have been added to an Entity, keyed by
|
94
|
+
# the Entity's ID and the Component class.
|
95
|
+
attr_reader :components_by_entity
|
96
|
+
|
88
97
|
##
|
89
98
|
# The Hash of event subscription callbacks registered with the world, keyed by
|
90
99
|
# event pattern.
|
@@ -120,6 +129,15 @@ class Chione::World
|
|
120
129
|
end
|
121
130
|
|
122
131
|
|
132
|
+
### Step the world +delta_seconds+ into the future.
|
133
|
+
def tick( delta_seconds=1.0/60.0 )
|
134
|
+
self.publish( 'timing', delta_seconds, self.tick_count )
|
135
|
+
self.publish_deferred_events
|
136
|
+
|
137
|
+
self.tick_count += 1
|
138
|
+
end
|
139
|
+
|
140
|
+
|
123
141
|
### Start any Managers registered with the world.
|
124
142
|
def start_managers
|
125
143
|
self.log.info "Starting %d Managers" % [ self.managers.length ]
|
@@ -168,7 +186,7 @@ class Chione::World
|
|
168
186
|
|
169
187
|
### Returns +true+ if the World is running (i.e., if #start has been called)
|
170
188
|
def running?
|
171
|
-
return self.started? && self.
|
189
|
+
return self.started? && self.tick_count.nonzero?
|
172
190
|
end
|
173
191
|
|
174
192
|
|
@@ -238,6 +256,7 @@ class Chione::World
|
|
238
256
|
|
239
257
|
### Send any deferred events to subscribers.
|
240
258
|
def publish_deferred_events
|
259
|
+
self.log.debug "Publishing %d deferred events" % [ self.deferred_events.length ]
|
241
260
|
while event = self.deferred_events.shift
|
242
261
|
self.call_subscription_callbacks( *event )
|
243
262
|
end
|
@@ -273,6 +292,10 @@ class Chione::World
|
|
273
292
|
end
|
274
293
|
|
275
294
|
|
295
|
+
#
|
296
|
+
# :section: Entity API
|
297
|
+
#
|
298
|
+
|
276
299
|
### Return a new Chione::Entity for the receiving World, using the optional
|
277
300
|
### +archetype+ to populate it with components if it's specified.
|
278
301
|
def create_entity( archetype=nil )
|
@@ -304,7 +327,8 @@ class Chione::World
|
|
304
327
|
self.has_entity?( entity )
|
305
328
|
|
306
329
|
self.publish( 'entity/destroyed', entity )
|
307
|
-
|
330
|
+
self.entities_by_component.each_value {|set| set.delete(entity.id) }
|
331
|
+
self.components_by_entity.delete( entity.id )
|
308
332
|
@entities.delete( entity.id )
|
309
333
|
end
|
310
334
|
|
@@ -320,74 +344,171 @@ class Chione::World
|
|
320
344
|
end
|
321
345
|
|
322
346
|
|
323
|
-
|
324
|
-
|
325
|
-
|
347
|
+
#
|
348
|
+
# :section: Component API
|
349
|
+
#
|
350
|
+
|
351
|
+
### Add the specified +component+ to the specified +entity+.
|
352
|
+
def add_component_to( entity, component, **init_values )
|
353
|
+
entity = entity.id if entity.respond_to?( :id )
|
354
|
+
component = Chione::Component( component, init_values )
|
355
|
+
component.entity_id = entity
|
356
|
+
|
326
357
|
self.log.debug "Adding %p for %p" % [ component.class, entity ]
|
327
|
-
|
358
|
+
self.entities_by_component[ component.class ].add( entity )
|
359
|
+
component_hash = self.components_by_entity[ entity ]
|
360
|
+
component_hash[ component.class ] = component
|
361
|
+
|
362
|
+
self.update_entity_caches( entity, component_hash )
|
328
363
|
end
|
364
|
+
alias_method :add_component_for, :add_component_to
|
329
365
|
|
330
366
|
|
331
|
-
### Return
|
332
|
-
###
|
333
|
-
def
|
334
|
-
|
335
|
-
return self.
|
367
|
+
### Return a Hash of the Component instances associated with +entity+, keyed by
|
368
|
+
### their class.
|
369
|
+
def components_for( entity )
|
370
|
+
entity = entity.id if entity.respond_to?( :id )
|
371
|
+
return self.components_by_entity[ entity ].dup
|
336
372
|
end
|
337
373
|
|
338
374
|
|
339
|
-
### Return
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
345
|
-
|
375
|
+
### Return the Component instance of the specified +component_class+ that's
|
376
|
+
### associated with the given +entity+, if it has one.
|
377
|
+
def get_component_for( entity, component_class )
|
378
|
+
entity = entity.id if entity.respond_to?( :id )
|
379
|
+
return self.components_by_entity[ entity ][ component_class ]
|
380
|
+
end
|
381
|
+
|
382
|
+
|
383
|
+
### Remove the specified +component+ from the given +entity+. If +component+ is
|
384
|
+
### a Component subclass, any instance of it will be removed. If it's a
|
385
|
+
### Component instance, it will be removed iff it is the same instance associated
|
386
|
+
### with the given +entity+.
|
387
|
+
def remove_component_from( entity, component )
|
388
|
+
entity = entity.id if entity.respond_to?( :id )
|
389
|
+
if component.is_a?( Class )
|
390
|
+
self.entities_by_component[ component ].delete( entity )
|
391
|
+
component_hash = self.components_by_entity[ entity ]
|
392
|
+
component_hash.delete( component )
|
393
|
+
self.update_entity_caches( entity, component_hash )
|
394
|
+
else
|
395
|
+
self.remove_component_from( entity, component.class ) if
|
396
|
+
self.has_component_for?( entity, component )
|
397
|
+
end
|
398
|
+
end
|
399
|
+
alias_method :remove_component_for, :remove_component_from
|
346
400
|
|
347
|
-
with_one = initial_set.reduce( :| )
|
348
|
-
with_all = @entities_by_component.values_at( *aspect.all_of ).reduce( with_one, :& )
|
349
|
-
without_any = @entities_by_component.values_at( *aspect.none_of ).reduce( with_all, :- )
|
350
401
|
|
351
|
-
|
402
|
+
### Return +true+ if the specified +entity+ has the given +component+. If
|
403
|
+
### +component+ is a Component subclass, any instance of it will test +true+. If
|
404
|
+
### +component+ is a Component instance, it will only test +true+ if the
|
405
|
+
### +entity+ is associated with that particular instance.
|
406
|
+
def has_component_for?( entity, component )
|
407
|
+
entity = entity.id if entity.respond_to?( :id )
|
408
|
+
if component.is_a?( Class )
|
409
|
+
return self.components_by_entity[ entity ].key?( component )
|
410
|
+
else
|
411
|
+
return self.components_by_entity[ entity ][ component.class ] == component
|
412
|
+
end
|
352
413
|
end
|
353
414
|
|
354
415
|
|
416
|
+
#
|
417
|
+
# :section: System API
|
418
|
+
#
|
419
|
+
|
355
420
|
### Add an instance of the specified +system_type+ to the world and return it.
|
356
421
|
### It will replace any existing system of the same type.
|
357
422
|
def add_system( system_type, *args )
|
358
423
|
system_obj = system_type.new( self, *args )
|
359
|
-
|
424
|
+
self.systems[ system_type ] = system_obj
|
360
425
|
|
361
426
|
if self.running?
|
362
427
|
self.log.info "Starting %p added to running world." % [ system_type ]
|
363
428
|
system_obj.start
|
364
429
|
end
|
365
430
|
|
366
|
-
self.publish( 'system/added',
|
431
|
+
self.publish( 'system/added', system_obj )
|
367
432
|
return system_obj
|
368
433
|
end
|
369
434
|
|
370
435
|
|
436
|
+
### Remove the instance of the specified +system_type+ from the world and return
|
437
|
+
### it if it's been added. Returns +nil+ if no instance of the specified
|
438
|
+
### +system_type+ was added.
|
439
|
+
def remove_system( system_type )
|
440
|
+
system_obj = self.systems.delete( system_type ) or return nil
|
441
|
+
|
442
|
+
self.publish( 'system/removed', system_obj )
|
443
|
+
|
444
|
+
if self.running?
|
445
|
+
self.log.info "Stopping %p before being removed from runnning world." % [ system_type ]
|
446
|
+
system_obj.stop
|
447
|
+
end
|
448
|
+
|
449
|
+
return system_obj
|
450
|
+
end
|
451
|
+
|
452
|
+
|
453
|
+
### Return an Array of all entities that match the specified +aspect+.
|
454
|
+
def entities_with( aspect )
|
455
|
+
return aspect.matching_entities( self.entities_by_component )
|
456
|
+
end
|
457
|
+
|
458
|
+
|
459
|
+
#
|
460
|
+
# :section: Manager API
|
461
|
+
#
|
462
|
+
|
371
463
|
### Add an instance of the specified +manager_type+ to the world and return it.
|
372
464
|
### It will replace any existing manager of the same type.
|
373
465
|
def add_manager( manager_type, *args )
|
374
466
|
manager_obj = manager_type.new( self, *args )
|
375
|
-
|
467
|
+
self.managers[ manager_type ] = manager_obj
|
376
468
|
|
377
469
|
if self.running?
|
378
470
|
self.log.info "Starting %p added to running world." % [ manager_type ]
|
379
471
|
manager_obj.start
|
380
472
|
end
|
381
473
|
|
382
|
-
self.publish( 'manager/added',
|
474
|
+
self.publish( 'manager/added', manager_obj )
|
475
|
+
return manager_obj
|
476
|
+
end
|
477
|
+
|
478
|
+
|
479
|
+
### Remove the instance of the specified +manager_type+ from the world and
|
480
|
+
### return it if it's been added. Returns +nil+ if no instance of the specified
|
481
|
+
### +manager_type+ was added.
|
482
|
+
def remove_manager( manager_type )
|
483
|
+
manager_obj = self.managers.delete( manager_type ) or return nil
|
484
|
+
self.publish( 'manager/removed', manager_obj )
|
485
|
+
|
486
|
+
if self.running?
|
487
|
+
self.log.info "Stopping %p removed from running world." % [ manager_type ]
|
488
|
+
manager_obj.stop
|
489
|
+
end
|
490
|
+
|
383
491
|
return manager_obj
|
384
492
|
end
|
385
493
|
|
386
494
|
|
495
|
+
# :section:
|
496
|
+
|
497
|
+
|
387
498
|
#########
|
388
499
|
protected
|
389
500
|
#########
|
390
501
|
|
502
|
+
### Update any entity caches in the system when an +entity+ has its +components+ hash changed.
|
503
|
+
def update_entity_caches( entity, components )
|
504
|
+
entity = entity.id if entity.respond_to?( :id )
|
505
|
+
self.log.debug " updating entity cache for %p" % [ entity ]
|
506
|
+
self.systems.each_value do |sys|
|
507
|
+
sys.entity_components_updated( entity, components )
|
508
|
+
end
|
509
|
+
end
|
510
|
+
|
511
|
+
|
391
512
|
### The loop the main thread executes after the world is started. The default
|
392
513
|
### implementation just broadcasts the +timing+ event, so you will likely want to
|
393
514
|
### override this if the main thread should do something else.
|
@@ -396,22 +517,18 @@ class Chione::World
|
|
396
517
|
last_timing_event = Time.now
|
397
518
|
interval = self.class.timing_event_interval
|
398
519
|
self.defer_events = false
|
399
|
-
|
520
|
+
self.tick_count = 0
|
400
521
|
|
401
522
|
loop do
|
402
523
|
previous_time, last_timing_event = last_timing_event, Time.now
|
403
|
-
|
404
|
-
self.publish( 'timing', last_timing_event - previous_time, @timing_event_count )
|
405
|
-
self.publish_deferred_events
|
406
|
-
|
407
|
-
@timing_event_count += 1
|
524
|
+
self.tick( last_timing_event - previous_time )
|
408
525
|
remaining_time = interval - (Time.now - last_timing_event)
|
409
526
|
|
410
527
|
if remaining_time > 0
|
411
528
|
sleep( remaining_time )
|
412
529
|
else
|
413
530
|
self.log.warn "Timing loop %d exceeded `timing_event_interval` (by %0.6fs)" %
|
414
|
-
[
|
531
|
+
[ self.tick_count, remaining_time.abs ]
|
415
532
|
end
|
416
533
|
end
|
417
534
|
|