game_ecs 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +13 -0
- data/.rspec +3 -0
- data/.travis.yml +5 -0
- data/Gemfile +6 -0
- data/LICENSE.txt +21 -0
- data/README.md +254 -0
- data/Rakefile +6 -0
- data/contrib/sample_systems_components.rb +56 -0
- data/contrib/simple_system.rb +53 -0
- data/examples/coin.wav +0 -0
- data/examples/coin_getter_game.rb +215 -0
- data/game_ecs.gemspec +28 -0
- data/lib/game_ecs.rb +5 -0
- data/lib/game_ecs/entity_store.rb +469 -0
- data/lib/game_ecs/version.rb +3 -0
- metadata +115 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: ececc23f2d9b4a5f22dd86c23315acc3aec9e1d7
|
4
|
+
data.tar.gz: d1c43248bcf8feed51969521b8b1b0e08917eb86
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 182736984659f36cb814ff787560e0e18f210f7399f8cce7f29a981191ed926fac45487d2caba78f923b9e2c6ec7e54ce16f1e85a3a90987e5587fcb69faa959
|
7
|
+
data.tar.gz: f42a0e425250cc83998c11027b7b6cdc3d28987ac51dc460fe1b7acdded220e4570708a959b744d4877938ebd839cd75c2bc5b182abd27eb62fe28c58452620e
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2018 Shawn Anderson
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,254 @@
|
|
1
|
+
# GameEcs
|
2
|
+
|
3
|
+
An easy to use Entity Component System library for use in game development. Learn more about ECS here:
|
4
|
+
* [Evolve Your Heirachy](http://cowboyprogramming.com/2007/01/05/evolve-your-heirachy/)
|
5
|
+
* [Wikipedia](https://en.wikipedia.org/wiki/Entity%E2%80%93component%E2%80%93system)
|
6
|
+
|
7
|
+
Getting Started
|
8
|
+
|
9
|
+
* [Installation](#installation)
|
10
|
+
* [Usage](#usage)
|
11
|
+
* [Components](#components)
|
12
|
+
* [Creating Entities](#creating-entities)
|
13
|
+
* [Add/Remove Components](#adding-and-removing-components)
|
14
|
+
* [Finding Entities](#finding-entities)
|
15
|
+
* [Updating Components](#updating-components)
|
16
|
+
* [Advanced Querying](#advanced-querying)
|
17
|
+
* [Big Picture](#big-picture)
|
18
|
+
* [Notes](#notes)
|
19
|
+
|
20
|
+
### Adding and Removing Components
|
21
|
+
|
22
|
+
## Installation
|
23
|
+
|
24
|
+
Add this line to your application's Gemfile:
|
25
|
+
|
26
|
+
```ruby
|
27
|
+
gem 'game_ecs'
|
28
|
+
```
|
29
|
+
|
30
|
+
And then execute:
|
31
|
+
|
32
|
+
$ bundle
|
33
|
+
|
34
|
+
Or install it yourself as:
|
35
|
+
|
36
|
+
$ gem install game_ecs
|
37
|
+
|
38
|
+
## Usage
|
39
|
+
|
40
|
+
### Components
|
41
|
+
|
42
|
+
Components in GameEcs are ordinary Ruby classes. In most cases, they should be struct-like classes with `attr_accessor` properties only. Adding default values in the constructor is as advanced as these objects should get.
|
43
|
+
|
44
|
+
```ruby
|
45
|
+
# Example components
|
46
|
+
class Position
|
47
|
+
attr_accessor :x, :y
|
48
|
+
def initialize(x:0,y:0)
|
49
|
+
@x = x
|
50
|
+
@y = y
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
class Tag
|
55
|
+
attr_accessor :name
|
56
|
+
def initialize(name:)
|
57
|
+
@name = name
|
58
|
+
end
|
59
|
+
end
|
60
|
+
```
|
61
|
+
|
62
|
+
### Creating Entities
|
63
|
+
|
64
|
+
An Entity is simply a collection of Components joined together by an id. To create one, simply call the `add_entity` method with a list of the Components you want the Entity to initially have:
|
65
|
+
|
66
|
+
```ruby
|
67
|
+
# Create and save your store in your higher level Game / State class
|
68
|
+
store = GameEcs::EntityStore.new
|
69
|
+
|
70
|
+
# Creating an entity returns its id.
|
71
|
+
# In most cases, you will not need to keep this id around.
|
72
|
+
ent_id = store.add_entity(Position.new(x:1,y:3), Tag.new(name:"Player"))
|
73
|
+
```
|
74
|
+
|
75
|
+
I recommend creating an `EntityFactory` or `Prefab` class to store factory methods that know how to build each kind of entity:
|
76
|
+
|
77
|
+
```ruby
|
78
|
+
class Prefab
|
79
|
+
def self.player_at(store:, x:,y:)
|
80
|
+
store.add_entity(Position.new(x:x,y:y), Tag.new(name:"Player"))
|
81
|
+
end
|
82
|
+
|
83
|
+
def self.tank(*args)
|
84
|
+
# ...
|
85
|
+
end
|
86
|
+
|
87
|
+
# etc
|
88
|
+
end
|
89
|
+
```
|
90
|
+
|
91
|
+
### Adding and Removing Components
|
92
|
+
|
93
|
+
The great thing about ECS is the ability to add/remove components at runtime. Here's how to do it:
|
94
|
+
|
95
|
+
```ruby
|
96
|
+
# add a Color component
|
97
|
+
store.add_component(id: ent_id, component: Color.new(red: 255, green: 255, blue: 0))
|
98
|
+
|
99
|
+
# we remove by class
|
100
|
+
store.remove_component(id: ent_id, klass: Color)
|
101
|
+
|
102
|
+
# remove an entire entity
|
103
|
+
store.remove_entity(id: ent_id)
|
104
|
+
|
105
|
+
# remove many entities
|
106
|
+
store.remove_entities(ids: list_of_ids)
|
107
|
+
```
|
108
|
+
|
109
|
+
### Finding Entities
|
110
|
+
|
111
|
+
There are two main ways of finding the entities you want. You can ask for them directly by id or you can search for them by Components.
|
112
|
+
|
113
|
+
#### By Id
|
114
|
+
Finding by id is nice if you are looking for a single entity. You merely specify the id and the components you want available for modification. If the id does not exist or the entity does not have one of the specified components, `nil` is returned.
|
115
|
+
|
116
|
+
```ruby
|
117
|
+
ent = store.find_by_id(ent_id, Position, Color)
|
118
|
+
id = ent.id
|
119
|
+
pos, color = ent.components
|
120
|
+
```
|
121
|
+
|
122
|
+
#### Querying by Components
|
123
|
+
|
124
|
+
GameEcs has a `Query` class that can be used for more advanced queries, but the most common case is that you want all enitities that have all the components you're interested in. `musts` is short had for building these types of queries:
|
125
|
+
|
126
|
+
```ruby
|
127
|
+
ents_that_need_move = store.musts(Position, Velocity)
|
128
|
+
ents_that_need_move.each do |ent|
|
129
|
+
pos,vel = ent.components
|
130
|
+
# modify pos based on vel
|
131
|
+
end
|
132
|
+
```
|
133
|
+
|
134
|
+
This pattern of find the ents and loop over them is so common there is a helper that does just that called `each_entity`:
|
135
|
+
|
136
|
+
```ruby
|
137
|
+
store.each_entity(Position, Velocity) do |ent|
|
138
|
+
pos,vel = ent.components
|
139
|
+
# modify pos based on vel
|
140
|
+
end
|
141
|
+
```
|
142
|
+
|
143
|
+
### Updating Components
|
144
|
+
|
145
|
+
Once you've got hold of an "entity" from the store. You can access the components you queried for via the `components` method on the entity. Once you have it, you can directly modify its values.
|
146
|
+
|
147
|
+
```ruby
|
148
|
+
store.each_entity(Position, Velocity) do |ent|
|
149
|
+
pos,vel = ent.components
|
150
|
+
pos.x += vel.x * time_scale
|
151
|
+
pos.y += vel.y * time_scale
|
152
|
+
end
|
153
|
+
```
|
154
|
+
|
155
|
+
### Advanced Querying
|
156
|
+
|
157
|
+
`each_entity` and `musts` are really shorthand for creating `GameEcs::Query` objects and passing it to the `query` method. Let's look at the longhand version; the following two lines are synonymous:
|
158
|
+
|
159
|
+
```ruby
|
160
|
+
ents = store.query(Query.must(Position).must(Color))
|
161
|
+
ents = store.musts(Position, Color)
|
162
|
+
```
|
163
|
+
|
164
|
+
By using the `Query` directly, we can add in `maybe` cases. A Maybe will still match if the entity does not have the desired component, but will return nil for that component.
|
165
|
+
|
166
|
+
|
167
|
+
```ruby
|
168
|
+
store.query(Query.must(Position).maybe(Color)).each do |ent|
|
169
|
+
# color may be nil
|
170
|
+
pos,color = ent.components
|
171
|
+
end
|
172
|
+
```
|
173
|
+
|
174
|
+
#### Experimental!
|
175
|
+
We can also query based on components' values using `with`:
|
176
|
+
|
177
|
+
```ruby
|
178
|
+
# Only entities with a Position component with x val == 12 will be returned
|
179
|
+
store.query(Query.must(Position).with(x: 12).must(Color)).each do |ent|
|
180
|
+
pos,color = ent.components
|
181
|
+
end
|
182
|
+
```
|
183
|
+
|
184
|
+
We can also use lambdas to determine if a value matches:
|
185
|
+
```ruby
|
186
|
+
# Only entities with a Position less than 12 will be returned
|
187
|
+
store.query(Query.must(Position).with(x: ->(x){ x < 12 }).must(Color)).each do |ent|
|
188
|
+
pos,color = ent.components
|
189
|
+
end
|
190
|
+
```
|
191
|
+
|
192
|
+
*_!! DANGER !!_*
|
193
|
+
|
194
|
+
Currently the caching mechanism in GameEcs does not know if the value of a component has changed since it was cached. Only use this for component values that do not change often, or clear your cache to get the results to update. The rough plan here is to eventually change components to be more of a DSL and have them notify the store on value changes of interest (If any queries care about the change)
|
195
|
+
|
196
|
+
### Big Picture
|
197
|
+
|
198
|
+
`EntityStore` is meant to be constructed and passed into a list of processing systems. This gem is entirely agnostic to how you implement your Game and Systems. A quick example _could_ look like:
|
199
|
+
|
200
|
+
```ruby
|
201
|
+
class Game
|
202
|
+
def intialize
|
203
|
+
@store = GameEcs::EntityStore.new
|
204
|
+
@render_system = RenderSystem.new(@store)
|
205
|
+
@systems = [
|
206
|
+
MovementSystem.new(@store),
|
207
|
+
# .. other systems
|
208
|
+
@render_system
|
209
|
+
]
|
210
|
+
end
|
211
|
+
|
212
|
+
def update(time_delta, inputs)
|
213
|
+
@systems.each{|sys| sys.update(time_delta, inputs) }
|
214
|
+
end
|
215
|
+
|
216
|
+
def draw
|
217
|
+
@render_system.render
|
218
|
+
end
|
219
|
+
end
|
220
|
+
|
221
|
+
class MovementSystem
|
222
|
+
def initialize(store)
|
223
|
+
@store = store
|
224
|
+
end
|
225
|
+
|
226
|
+
def update(dt, inputs)
|
227
|
+
@store.each_entity(Position, Velocity) do |ent|
|
228
|
+
pos,vel = ent.components
|
229
|
+
pos.x += vel.x * dt
|
230
|
+
pos.y += vel.y * dt
|
231
|
+
end
|
232
|
+
end
|
233
|
+
end
|
234
|
+
|
235
|
+
|
236
|
+
```
|
237
|
+
For a more fully fleshed out game using ECS in this way, checkout [Pixel Monster](https://github.com/shawn42/pixel_monster)
|
238
|
+
|
239
|
+
|
240
|
+
### Notes
|
241
|
+
|
242
|
+
* entities can only have one instance of each component type
|
243
|
+
* adding/removing entities and components is delayed until all iterating code has finished (calls to `each_entity`).
|
244
|
+
* all queries are cached by default calling `clear_cache!` will reset the cache
|
245
|
+
* to dump all entities and components from the store, use `clear!`
|
246
|
+
|
247
|
+
|
248
|
+
## Contributing
|
249
|
+
|
250
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/shawn42/game_ecs.
|
251
|
+
|
252
|
+
## License
|
253
|
+
|
254
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
@@ -0,0 +1,56 @@
|
|
1
|
+
# TODO docs here
|
2
|
+
class TimerSystem
|
3
|
+
def update(store, inputs)
|
4
|
+
current_time_ms = inputs[:total_time]
|
5
|
+
store.each_entity Timer do |rec|
|
6
|
+
timer = rec.get(Timer)
|
7
|
+
ent_id = rec.id
|
8
|
+
|
9
|
+
if timer.expires_at
|
10
|
+
if timer.expires_at < current_time_ms
|
11
|
+
if timer.event
|
12
|
+
event_comp = timer.event.is_a?(Class) ? timer.event.new : timer.event
|
13
|
+
store.add_component component: event_comp, id: ent_id
|
14
|
+
end
|
15
|
+
if timer.repeat
|
16
|
+
timer.expires_at = current_time_ms + timer.total
|
17
|
+
else
|
18
|
+
store.remove_component(klass: timer.class, id: ent_id)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
else
|
22
|
+
timer.expires_at = current_time_ms + timer.total
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
class Timer
|
28
|
+
attr_accessor :ttl, :repeat, :total, :event, :name, :expires_at
|
29
|
+
def initialize(name, ttl, repeat, event = nil)
|
30
|
+
@name = name
|
31
|
+
@total = ttl
|
32
|
+
@ttl = ttl
|
33
|
+
@repeat = repeat
|
34
|
+
@event = event
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
class SoundSystem
|
39
|
+
def preload(asset)
|
40
|
+
@assets ||= {}
|
41
|
+
@assets[asset] = Gosu::Sample.new(asset)
|
42
|
+
end
|
43
|
+
def update(store, inputs)
|
44
|
+
store.each_entity SoundEffectEvent do |rec|
|
45
|
+
ent_id = rec.id
|
46
|
+
effect = rec.get(SoundEffectEvent)
|
47
|
+
store.remove_component klass: effect.class, id: ent_id
|
48
|
+
if @assets[effect.sound_to_play]
|
49
|
+
@assets[effect.sound_to_play].play
|
50
|
+
else
|
51
|
+
puts "Warning: #{effect.sound_to_play} is missing!"
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
SoundEffectEvent = Struct.new :sound_to_play
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module GameEcs
|
2
|
+
class SimpleSystem
|
3
|
+
def self.from_query(query, &block)
|
4
|
+
new(query, &block)
|
5
|
+
end
|
6
|
+
|
7
|
+
def initialize(query=Query.none, &block)
|
8
|
+
@query = query
|
9
|
+
@system_block = block
|
10
|
+
end
|
11
|
+
|
12
|
+
def update(entity_store, *args)
|
13
|
+
before_update(entity_store, *args)
|
14
|
+
entity_store.query.each do |ent|
|
15
|
+
each(ent, *args)
|
16
|
+
end
|
17
|
+
after_update(entity_store, *args)
|
18
|
+
end
|
19
|
+
|
20
|
+
def before_update(entity_store, *args)
|
21
|
+
end
|
22
|
+
def after_update(entity_store, *args)
|
23
|
+
end
|
24
|
+
def update_each(entity_store, ent, *args)
|
25
|
+
# handle a single update of an ent
|
26
|
+
@system_block.call(entity_store, ent, *args)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
31
|
+
|
32
|
+
if $0 == __FILE__
|
33
|
+
include GameEcs
|
34
|
+
MovementSystem = SimpleSystem.from_query( Q.musts(Position, Velocity) ) do |store, ent, dt, input|
|
35
|
+
pos,vel = ent.components
|
36
|
+
pos.x += vel.x * dt
|
37
|
+
pos.y += vel.y * dt
|
38
|
+
end
|
39
|
+
|
40
|
+
# long hand via subclassing, allows for before/after overrides
|
41
|
+
class MovementSystem < SimpleSystem
|
42
|
+
def after_update(store, *args)
|
43
|
+
log "MovementSystem finished: #{Time.now}"
|
44
|
+
end
|
45
|
+
def update_each(store, ent, dt, input)
|
46
|
+
Q.each_entity(Position, Velocity) do
|
47
|
+
pos,vel = ent.components
|
48
|
+
pos.x += vel.x * dt
|
49
|
+
pos.y += vel.y * dt
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
data/examples/coin.wav
ADDED
Binary file
|
@@ -0,0 +1,215 @@
|
|
1
|
+
$: << '../lib'
|
2
|
+
$: << '../contrib'
|
3
|
+
require_relative '../lib/game_ecs'
|
4
|
+
require_relative '../contrib/sample_systems_components'
|
5
|
+
|
6
|
+
require 'gosu'
|
7
|
+
include Gosu
|
8
|
+
Q = GameEcs::Query
|
9
|
+
|
10
|
+
class CoinGetterGame < Window
|
11
|
+
def initialize
|
12
|
+
super(800,600)
|
13
|
+
@entity_store = GameEcs::EntityStore.new
|
14
|
+
@render_system = RenderSystem.new
|
15
|
+
@sound_system = SoundSystem.new
|
16
|
+
@sound_system.preload('coin.wav')
|
17
|
+
@downs = []
|
18
|
+
|
19
|
+
@systems = [
|
20
|
+
ControlSystem.new,
|
21
|
+
MotionSystem.new,
|
22
|
+
TimerSystem.new,
|
23
|
+
CoinSystem.new,
|
24
|
+
@sound_system,
|
25
|
+
@render_system
|
26
|
+
]
|
27
|
+
|
28
|
+
Prefab.player(@entity_store, 400, 300)
|
29
|
+
10.times do
|
30
|
+
Prefab.coin(@entity_store, rand(50..750), rand(50..550))
|
31
|
+
end
|
32
|
+
Prefab.coin_gen_timer(@entity_store)
|
33
|
+
end
|
34
|
+
|
35
|
+
def button_down(id)
|
36
|
+
close if id == KbEscape
|
37
|
+
@downs << id
|
38
|
+
end
|
39
|
+
def button_up(id)
|
40
|
+
@downs.delete id
|
41
|
+
end
|
42
|
+
|
43
|
+
def update
|
44
|
+
@systems.each do |sys|
|
45
|
+
sys.update(@entity_store, {dt: relative_delta, down_ids: @downs, total_time: Gosu::milliseconds})
|
46
|
+
end
|
47
|
+
self.caption = "FPS: #{Gosu.fps}"
|
48
|
+
end
|
49
|
+
|
50
|
+
def draw
|
51
|
+
@render_system.draw(@entity_store, self)
|
52
|
+
end
|
53
|
+
|
54
|
+
|
55
|
+
private
|
56
|
+
|
57
|
+
MAX_UPDATE_SIZE_IN_MILLIS = 500
|
58
|
+
def relative_delta
|
59
|
+
total_millis = Gosu::milliseconds.to_f
|
60
|
+
@last_millis ||= total_millis
|
61
|
+
delta = total_millis
|
62
|
+
delta -= @last_millis if total_millis > @last_millis
|
63
|
+
@last_millis = total_millis
|
64
|
+
delta = MAX_UPDATE_SIZE_IN_MILLIS if delta > MAX_UPDATE_SIZE_IN_MILLIS
|
65
|
+
delta
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
class Prefab
|
70
|
+
def self.coin(store, x, y)
|
71
|
+
store.add_entity(Position.new(x,y,1), Velocity.new(0.5-rand,0.5-rand), Color.new(:green), Size.new(10), Tag.new("coin") )
|
72
|
+
end
|
73
|
+
|
74
|
+
def self.player(store, x, y)
|
75
|
+
store.add_entity(Position.new(400,40,3), Tag.new("p1"), Score.new(0))
|
76
|
+
store.add_entity(Position.new(x,y,2), Velocity.new(0,0), Color.new(:red), Tag.new("p1"), Input.new, Size.new(20))
|
77
|
+
end
|
78
|
+
|
79
|
+
def self.coin_gen_timer(store)
|
80
|
+
store.add_entity(Timer.new(:coin_gen, 2000, true, GenerateNewCoinEvent))
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
|
85
|
+
# Systems
|
86
|
+
class ControlSystem
|
87
|
+
def update(store, inputs)
|
88
|
+
downs = inputs[:down_ids]
|
89
|
+
player_one = store.query(Q.must(Input).must(Tag).with(name: "p1")).first.get(Input)
|
90
|
+
player_one.left = downs.include?(KbLeft) || downs.include?(GpLeft)
|
91
|
+
player_one.right = downs.include?(KbRight) || downs.include?(GpRight)
|
92
|
+
player_one.up = downs.include?(KbUp) || downs.include?(GpUp)
|
93
|
+
player_one.down = downs.include?(KbDown) || downs.include?(GpDown)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
class MotionSystem
|
97
|
+
MAX_VELOCITY = 2
|
98
|
+
ACCEL = 0.03
|
99
|
+
FRICTION = 0.96
|
100
|
+
def update(store, inputs)
|
101
|
+
time_scale = inputs[:dt] * 0.01
|
102
|
+
store.each_entity(Input, Velocity) do |ent|
|
103
|
+
input, vel = ent.components
|
104
|
+
vel.dx += ACCEL*time_scale if input.right
|
105
|
+
vel.dx -= ACCEL*time_scale if input.left
|
106
|
+
vel.dy += ACCEL*time_scale if input.down
|
107
|
+
vel.dy -= ACCEL*time_scale if input.up
|
108
|
+
|
109
|
+
vel.dx *= FRICTION
|
110
|
+
vel.dy *= FRICTION
|
111
|
+
|
112
|
+
mag = vec_mag(vel.dx, vel.dy)
|
113
|
+
if mag > MAX_VELOCITY
|
114
|
+
vel.dx, vel.dy = vec_clip_to_mag(vel.dx, vel.dy, MAX_VELOCITY)
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
store.each_entity(Position, Velocity) do |ent|
|
119
|
+
pos,vel = ent.components
|
120
|
+
pos.x += vel.dx*time_scale
|
121
|
+
pos.y += vel.dy*time_scale
|
122
|
+
pos.x %= 800
|
123
|
+
pos.y %= 600
|
124
|
+
end
|
125
|
+
end
|
126
|
+
def vec_mag(x,y)
|
127
|
+
Math.sqrt(x*x + y*y)
|
128
|
+
end
|
129
|
+
def vec_clip_to_mag(x,y,max_mag)
|
130
|
+
mag = vec_mag(x,y)
|
131
|
+
[x.to_f/mag*max_mag, y.to_f/mag*max_mag]
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
class RenderSystem
|
136
|
+
def initialize
|
137
|
+
@colors = {
|
138
|
+
red: Gosu::Color::RED,
|
139
|
+
green: Gosu::Color::GREEN
|
140
|
+
}
|
141
|
+
@font = Gosu::Font.new(40)
|
142
|
+
end
|
143
|
+
def update(store,inputs); end
|
144
|
+
def draw(store, target)
|
145
|
+
store.each_entity(Position, Score) do |ent|
|
146
|
+
pos, score = ent.components
|
147
|
+
@font.draw("#{score.points}", pos.x, pos.y, pos.z, 1.0, 1.0, Gosu::Color::WHITE)
|
148
|
+
end
|
149
|
+
store.each_entity(Position, Color, Size) do |ent|
|
150
|
+
pos, col, size = ent.components
|
151
|
+
w = size.width
|
152
|
+
c1 = c2 = c3 = c4 = @colors[col.name]
|
153
|
+
x1 = pos.x
|
154
|
+
x2 = pos.x + w
|
155
|
+
x3 = x2
|
156
|
+
x4 = x1
|
157
|
+
|
158
|
+
y1 = pos.y
|
159
|
+
y2 = y1
|
160
|
+
y3 = pos.y + w
|
161
|
+
y4 = y3
|
162
|
+
|
163
|
+
target.draw_quad(x1, y1, c1, x2, y2, c2, x3, y3, c3, x4, y4, c4, pos.z)
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
class CoinSystem
|
169
|
+
def update(store, inputs)
|
170
|
+
p1_score = store.query(Q.must(Score).must(Tag).with(name: "p1")).first
|
171
|
+
p1_score, _ = p1_score.components
|
172
|
+
p1 = store.query(Q.must(Position).must(Size).must(Tag).with(name: "p1")).first
|
173
|
+
p1_pos, p1_size, _ = p1.components
|
174
|
+
|
175
|
+
store.query(Q.must(Position).must(Size).must(Tag).with(name:"coin")).each do |coin|
|
176
|
+
coin_pos, coin_size, _ = coin.components
|
177
|
+
if (coin_pos.x >= p1_pos.x &&
|
178
|
+
coin_pos.x <= p1_pos.x + p1_size.width) ||
|
179
|
+
(coin_pos.x + coin_size.width >= p1_pos.x &&
|
180
|
+
coin_pos.x + coin_size.width <= p1_pos.x + p1_size.width)
|
181
|
+
|
182
|
+
if (coin_pos.y >= p1_pos.y &&
|
183
|
+
coin_pos.y <= p1_pos.y + p1_size.width) ||
|
184
|
+
(coin_pos.y + coin_size.width >= p1_pos.y &&
|
185
|
+
coin_pos.y + coin_size.width <= p1_pos.y + p1_size.width)
|
186
|
+
|
187
|
+
store.remove_entity(id: coin.id)
|
188
|
+
p1_score.points += 1
|
189
|
+
store.add_entity SoundEffectEvent.new('coin.wav')
|
190
|
+
end
|
191
|
+
end
|
192
|
+
end
|
193
|
+
store.each_entity(GenerateNewCoinEvent) do |ent|
|
194
|
+
Prefab.coin(store, rand(50..750), rand(50..550))
|
195
|
+
store.remove_component(klass: GenerateNewCoinEvent, id: ent.id)
|
196
|
+
end
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
# Components
|
201
|
+
class Input
|
202
|
+
attr_accessor :left, :right, :up, :down
|
203
|
+
end
|
204
|
+
GenerateNewCoinEvent = Class.new
|
205
|
+
Tag = Struct.new :name
|
206
|
+
Position = Struct.new :x, :y, :z
|
207
|
+
Size = Struct.new :width
|
208
|
+
Velocity = Struct.new :dx, :dy
|
209
|
+
Color = Struct.new :name
|
210
|
+
Score = Struct.new :points
|
211
|
+
|
212
|
+
if $0 == __FILE__
|
213
|
+
game = CoinGetterGame.new
|
214
|
+
game.show
|
215
|
+
end
|
data/game_ecs.gemspec
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
|
2
|
+
lib = File.expand_path("../lib", __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require "game_ecs/version"
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "game_ecs"
|
8
|
+
spec.version = GameEcs::VERSION
|
9
|
+
spec.authors = ["Shawn Anderson"]
|
10
|
+
spec.email = ["shawn42@gmail.com"]
|
11
|
+
|
12
|
+
spec.summary = %q{Entity Component System architecture in Ruby}
|
13
|
+
spec.description = %q{Entity Component System architecture in Ruby}
|
14
|
+
spec.homepage = "https://github.com/shawn42/game_ecs"
|
15
|
+
spec.license = "MIT"
|
16
|
+
|
17
|
+
spec.files = `git ls-files -z`.split("\x0").reject do |f|
|
18
|
+
f.match(%r{^(test|spec|features)/})
|
19
|
+
end
|
20
|
+
spec.bindir = "exe"
|
21
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
22
|
+
spec.require_paths = ["lib"]
|
23
|
+
|
24
|
+
spec.add_development_dependency "bundler", "~> 1.16"
|
25
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
26
|
+
spec.add_development_dependency "rspec", "~> 3.0"
|
27
|
+
spec.add_development_dependency "pry", "~> 0.12.0"
|
28
|
+
end
|
data/lib/game_ecs.rb
ADDED
@@ -0,0 +1,469 @@
|
|
1
|
+
require 'forwardable'
|
2
|
+
require 'set'
|
3
|
+
|
4
|
+
module GameEcs
|
5
|
+
class EntityStore
|
6
|
+
attr_reader :entity_count, :id_to_comp
|
7
|
+
def initialize
|
8
|
+
clear!
|
9
|
+
end
|
10
|
+
|
11
|
+
def deep_clone
|
12
|
+
# NOTE! does not work for Hashes with default procs
|
13
|
+
if _iterating?
|
14
|
+
raise "AHH! EM is still iterating!!"
|
15
|
+
else
|
16
|
+
_apply_updates
|
17
|
+
clear_cache!
|
18
|
+
em = Marshal.load( Marshal.dump(self) )
|
19
|
+
em
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def num_entities
|
24
|
+
@id_to_comp.keys.size
|
25
|
+
end
|
26
|
+
|
27
|
+
def clear!
|
28
|
+
@comp_to_id = {}
|
29
|
+
@id_to_comp = {}
|
30
|
+
@cache = {}
|
31
|
+
@entity_count = 0
|
32
|
+
|
33
|
+
@iterator_count = 0
|
34
|
+
@ents_to_add_later = []
|
35
|
+
@comps_to_add_later = []
|
36
|
+
@comps_to_remove_later = []
|
37
|
+
@ents_to_remove_later = []
|
38
|
+
clear_cache!
|
39
|
+
end
|
40
|
+
|
41
|
+
def clear_cache!
|
42
|
+
@cache = {}
|
43
|
+
end
|
44
|
+
|
45
|
+
def find_by_id(id, *klasses)
|
46
|
+
return nil unless @id_to_comp.key? id
|
47
|
+
ent_record = @id_to_comp[id]
|
48
|
+
components = ent_record.values_at(*klasses)
|
49
|
+
rec = build_record(id, @id_to_comp[id], klasses) unless components.any?(&:nil?)
|
50
|
+
if block_given?
|
51
|
+
yield rec
|
52
|
+
else
|
53
|
+
rec
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def musts(*klasses)
|
58
|
+
raise "specify at least one component" if klasses.empty?
|
59
|
+
q = Q
|
60
|
+
klasses.each{|k| q = q.must(k)}
|
61
|
+
query(q)
|
62
|
+
end
|
63
|
+
alias find musts
|
64
|
+
|
65
|
+
def query(q)
|
66
|
+
# TODO cache results as q with content based cache
|
67
|
+
# invalidate cache based on queried_comps
|
68
|
+
cache_hit = @cache[q]
|
69
|
+
return cache_hit if cache_hit
|
70
|
+
|
71
|
+
queried_comps = q.components
|
72
|
+
required_comps = q.required_components
|
73
|
+
|
74
|
+
required_comps.each do |k|
|
75
|
+
@comp_to_id[k] ||= Set.new
|
76
|
+
end
|
77
|
+
|
78
|
+
intersecting_ids = []
|
79
|
+
unless required_comps.empty?
|
80
|
+
id_collection = @comp_to_id.values_at(*required_comps)
|
81
|
+
intersecting_ids = id_collection.sort_by(&:size).inject &:&
|
82
|
+
end
|
83
|
+
|
84
|
+
recs = intersecting_ids.
|
85
|
+
select{|eid| q.matches?(eid, @id_to_comp[eid]) }.
|
86
|
+
map do |eid|
|
87
|
+
build_record eid, @id_to_comp[eid], queried_comps
|
88
|
+
end
|
89
|
+
result = QueryResultSet.new(records: recs, ids: recs.map(&:id))
|
90
|
+
|
91
|
+
@cache[q] = result if q.cacheable?
|
92
|
+
result
|
93
|
+
end
|
94
|
+
|
95
|
+
def first(*klasses)
|
96
|
+
find(*klasses).first
|
97
|
+
end
|
98
|
+
|
99
|
+
def each_entity(*klasses, &blk)
|
100
|
+
ents = find(*klasses)
|
101
|
+
if block_given?
|
102
|
+
_iterating do
|
103
|
+
ents.each &blk
|
104
|
+
end
|
105
|
+
end
|
106
|
+
ents
|
107
|
+
end
|
108
|
+
|
109
|
+
def remove_component(klass:, id:)
|
110
|
+
if _iterating?
|
111
|
+
_remove_component_later klass: klass, id: id
|
112
|
+
else
|
113
|
+
_remove_component klass: klass, id: id
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
def add_component(component:,id:)
|
118
|
+
if _iterating?
|
119
|
+
_add_component_later component: component, id: id
|
120
|
+
else
|
121
|
+
_add_component component: component, id: id
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
def remove_entites(ids:)
|
126
|
+
if _iterating?
|
127
|
+
_remove_entities_later(ids: ids)
|
128
|
+
else
|
129
|
+
_remove_entites(ids: ids)
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
def remove_entity(id:)
|
134
|
+
if _iterating?
|
135
|
+
_remove_entity_later(id: id)
|
136
|
+
else
|
137
|
+
_remove_entity(id: id)
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
def add_entity(*components)
|
142
|
+
id = generate_id
|
143
|
+
if _iterating?
|
144
|
+
_add_entity_later(id:id, components: components)
|
145
|
+
else
|
146
|
+
_add_entity(id: id, components: components)
|
147
|
+
end
|
148
|
+
id
|
149
|
+
end
|
150
|
+
|
151
|
+
private
|
152
|
+
def _add_entity_later(id:,components:)
|
153
|
+
@ents_to_add_later << {components: components, id: id}
|
154
|
+
end
|
155
|
+
def _remove_entities_later(ids:)
|
156
|
+
ids.each do |id|
|
157
|
+
@ents_to_remove_later << id
|
158
|
+
end
|
159
|
+
end
|
160
|
+
def _remove_entity_later(id:)
|
161
|
+
@ents_to_remove_later << id
|
162
|
+
end
|
163
|
+
|
164
|
+
def _remove_component_later(klass:,id:)
|
165
|
+
@comps_to_remove_later << {klass: klass, id: id}
|
166
|
+
end
|
167
|
+
def _add_component_later(component:,id:)
|
168
|
+
@comps_to_add_later << {component: component, id: id}
|
169
|
+
end
|
170
|
+
|
171
|
+
def _apply_updates
|
172
|
+
_remove_entites ids: @ents_to_remove_later
|
173
|
+
@ents_to_remove_later.clear
|
174
|
+
|
175
|
+
@comps_to_remove_later.each do |opts|
|
176
|
+
_remove_component klass: opts[:klass], id: opts[:id]
|
177
|
+
end
|
178
|
+
@comps_to_remove_later.clear
|
179
|
+
|
180
|
+
@comps_to_add_later.each do |opts|
|
181
|
+
_add_component component: opts[:component], id: opts[:id]
|
182
|
+
end
|
183
|
+
@comps_to_add_later.clear
|
184
|
+
|
185
|
+
@ents_to_add_later.each do |opts|
|
186
|
+
_add_entity id: opts[:id], components: opts[:components]
|
187
|
+
end
|
188
|
+
@ents_to_add_later.clear
|
189
|
+
end
|
190
|
+
|
191
|
+
def _iterating
|
192
|
+
@iterator_count += 1
|
193
|
+
yield
|
194
|
+
@iterator_count -= 1
|
195
|
+
_apply_updates unless _iterating?
|
196
|
+
end
|
197
|
+
|
198
|
+
def _iterating?
|
199
|
+
@iterator_count > 0
|
200
|
+
end
|
201
|
+
|
202
|
+
def _add_component(component:,id:)
|
203
|
+
raise "Cannot add nil component" if component.nil?
|
204
|
+
|
205
|
+
@comp_to_id[component.class] ||= Set.new
|
206
|
+
@comp_to_id[component.class] << id
|
207
|
+
@id_to_comp[id] ||= {}
|
208
|
+
ent_record = @id_to_comp[id]
|
209
|
+
klass = component.class
|
210
|
+
|
211
|
+
raise "Cannot add component twice! #{component} -> #{id}" if ent_record.has_key? klass
|
212
|
+
ent_record[klass] = component
|
213
|
+
|
214
|
+
@cache.each do |q, results|
|
215
|
+
# TODO make results a smart result set that knows about ids to avoid the linear scan
|
216
|
+
# will musts vs maybes help here?
|
217
|
+
comp_klasses = q.components
|
218
|
+
if comp_klasses.include?(klass)
|
219
|
+
if results.has_id?(id)
|
220
|
+
results.add_component(id: id, component: component)
|
221
|
+
else
|
222
|
+
results << build_record(id, ent_record, comp_klasses) if q.matches?(id, ent_record)
|
223
|
+
end
|
224
|
+
end
|
225
|
+
end
|
226
|
+
nil
|
227
|
+
end
|
228
|
+
|
229
|
+
def _remove_component(klass:, id:)
|
230
|
+
@comp_to_id[klass] ||= Set.new
|
231
|
+
@comp_to_id[klass].delete id
|
232
|
+
@id_to_comp[id] ||= {}
|
233
|
+
@id_to_comp[id].delete klass
|
234
|
+
|
235
|
+
@cache.each do |q, results|
|
236
|
+
comp_klasses = q.components
|
237
|
+
if comp_klasses.include?(klass)
|
238
|
+
results.delete(id: id) unless q.matches?(id, @id_to_comp[id])
|
239
|
+
end
|
240
|
+
end
|
241
|
+
nil
|
242
|
+
end
|
243
|
+
|
244
|
+
def _remove_entites(ids:)
|
245
|
+
return if ids.empty?
|
246
|
+
|
247
|
+
ids.each do |id|
|
248
|
+
@id_to_comp.delete(id)
|
249
|
+
end
|
250
|
+
|
251
|
+
@comp_to_id.each do |_klass, ents|
|
252
|
+
ents.delete_if{|ent_id| ids.include? ent_id}
|
253
|
+
end
|
254
|
+
|
255
|
+
@cache.each do |comp_klasses, results|
|
256
|
+
results.delete ids: ids
|
257
|
+
end
|
258
|
+
end
|
259
|
+
|
260
|
+
def _remove_entity(id:)
|
261
|
+
comp_map = @id_to_comp[id]
|
262
|
+
if @id_to_comp.delete(id)
|
263
|
+
ent_comps = comp_map.keys
|
264
|
+
ent_comps.each do |klass|
|
265
|
+
@comp_to_id[klass].delete id
|
266
|
+
end
|
267
|
+
@cache.each do |_query, results|
|
268
|
+
results.delete id: id
|
269
|
+
end
|
270
|
+
end
|
271
|
+
end
|
272
|
+
|
273
|
+
def _add_entity(id:, components:)
|
274
|
+
components.each do |comp|
|
275
|
+
_add_component component: comp, id: id
|
276
|
+
end
|
277
|
+
id
|
278
|
+
end
|
279
|
+
|
280
|
+
def generate_id
|
281
|
+
@entity_count += 1
|
282
|
+
@ent_counter ||= 0
|
283
|
+
@ent_counter += 1
|
284
|
+
end
|
285
|
+
|
286
|
+
def build_record(*args)
|
287
|
+
EntityQueryResult.new(*args)
|
288
|
+
end
|
289
|
+
|
290
|
+
class QueryResultSet
|
291
|
+
def initialize(records:, ids:)
|
292
|
+
@records = records
|
293
|
+
@ids = Set.new(ids)
|
294
|
+
end
|
295
|
+
def <<(rec)
|
296
|
+
@ids << rec.id
|
297
|
+
@records << rec
|
298
|
+
end
|
299
|
+
def has_id?(id)
|
300
|
+
@ids.include? id
|
301
|
+
end
|
302
|
+
def add_component(id:, component:)
|
303
|
+
index = @records.index{ |rec| id == rec&.id }
|
304
|
+
@records[index].update_component(component) if index >= 0
|
305
|
+
end
|
306
|
+
def delete(id:nil, ids:nil)
|
307
|
+
if id
|
308
|
+
@records.delete_at(@records.index{ |rec| id == rec&.id } || @records.size) if @ids.include? id
|
309
|
+
@ids.delete id
|
310
|
+
else
|
311
|
+
unless (@ids & ids).empty?
|
312
|
+
# ids.each do |id|
|
313
|
+
# @ids.delete id
|
314
|
+
# end
|
315
|
+
@ids = @ids - ids
|
316
|
+
@records.delete_if{|res| ids.include? res.id}
|
317
|
+
end
|
318
|
+
end
|
319
|
+
end
|
320
|
+
def each
|
321
|
+
@records.each do |rec|
|
322
|
+
yield rec
|
323
|
+
end
|
324
|
+
end
|
325
|
+
extend ::Forwardable
|
326
|
+
def_delegators :@records, :first, :any?, :size, :select, :find, :empty?, :first, :map, :[]
|
327
|
+
|
328
|
+
end
|
329
|
+
|
330
|
+
class EntityQueryResult
|
331
|
+
attr_reader :id
|
332
|
+
def initialize(id, components, queried_components)
|
333
|
+
@id = id
|
334
|
+
@components = components
|
335
|
+
@queried_components = queried_components
|
336
|
+
end
|
337
|
+
|
338
|
+
def get(klass)
|
339
|
+
@components[klass]
|
340
|
+
end
|
341
|
+
|
342
|
+
def update_component(component)
|
343
|
+
@components[component.class] = component
|
344
|
+
@comp_cache = comp_cache
|
345
|
+
end
|
346
|
+
|
347
|
+
def components
|
348
|
+
@comp_cache ||= comp_cache
|
349
|
+
end
|
350
|
+
|
351
|
+
private
|
352
|
+
def comp_cache
|
353
|
+
@queried_components.map{|qc| @components[qc]}
|
354
|
+
end
|
355
|
+
end
|
356
|
+
end
|
357
|
+
|
358
|
+
class Condition
|
359
|
+
attr_reader :k, :attr_conditions
|
360
|
+
def initialize(k)
|
361
|
+
@attr_conditions = {}
|
362
|
+
@k = k
|
363
|
+
end
|
364
|
+
|
365
|
+
def ==(other)
|
366
|
+
@k == other.k &&
|
367
|
+
@attr_conditions.size == other.attr_conditions.size &&
|
368
|
+
@attr_conditions.all?{|ac,v| other.attr_conditions[ac] == v}
|
369
|
+
end
|
370
|
+
alias eql? ==
|
371
|
+
def hash
|
372
|
+
@_hash ||= @k.hash ^ @attr_conditions.hash
|
373
|
+
end
|
374
|
+
|
375
|
+
def components
|
376
|
+
@k
|
377
|
+
end
|
378
|
+
|
379
|
+
def attrs_match?(id, comps)
|
380
|
+
comp = comps[@k]
|
381
|
+
@attr_conditions.all? do |name, cond|
|
382
|
+
val = comp.send(name)
|
383
|
+
if cond.respond_to? :call
|
384
|
+
cond.call val
|
385
|
+
else
|
386
|
+
val == cond
|
387
|
+
end
|
388
|
+
end
|
389
|
+
end
|
390
|
+
|
391
|
+
def merge_conditions(attrs)
|
392
|
+
@attr_conditions ||= {}
|
393
|
+
@attr_conditions.merge! attrs
|
394
|
+
end
|
395
|
+
end
|
396
|
+
|
397
|
+
class Must < Condition
|
398
|
+
def matches?(id, comps)
|
399
|
+
comps.keys.include?(@k) && attrs_match?(id, comps)
|
400
|
+
end
|
401
|
+
end
|
402
|
+
|
403
|
+
class Maybe < Condition
|
404
|
+
def matches?(id, comps)
|
405
|
+
attrs_match?(id, comps)
|
406
|
+
end
|
407
|
+
|
408
|
+
end
|
409
|
+
|
410
|
+
class Query
|
411
|
+
attr_reader :components, :musts, :maybes
|
412
|
+
def self.none
|
413
|
+
Query.new
|
414
|
+
end
|
415
|
+
def self.must(*args)
|
416
|
+
Query.new.must(*args)
|
417
|
+
end
|
418
|
+
def self.maybe(*args)
|
419
|
+
Query.new.maybe(*args)
|
420
|
+
end
|
421
|
+
|
422
|
+
def initialize
|
423
|
+
@components = []
|
424
|
+
@musts = []
|
425
|
+
end
|
426
|
+
|
427
|
+
def must(k)
|
428
|
+
@last_condition = Must.new(k)
|
429
|
+
@musts << @last_condition
|
430
|
+
@components << k
|
431
|
+
self
|
432
|
+
end
|
433
|
+
|
434
|
+
def required_components
|
435
|
+
@musts.flat_map(&:components).uniq
|
436
|
+
end
|
437
|
+
|
438
|
+
def with(attr_map)
|
439
|
+
@last_condition.merge_conditions(attr_map)
|
440
|
+
self
|
441
|
+
end
|
442
|
+
|
443
|
+
def maybe(k)
|
444
|
+
@maybes ||= []
|
445
|
+
@last_condition = Maybe.new(k)
|
446
|
+
@maybes << @last_condition
|
447
|
+
@components << k
|
448
|
+
self
|
449
|
+
end
|
450
|
+
|
451
|
+
def matches?(eid, comps)
|
452
|
+
@musts.all?{|m| m.matches?(eid, comps)} # ignore maybes ;)
|
453
|
+
end
|
454
|
+
|
455
|
+
def ==(other)
|
456
|
+
self.musts == other.musts && self.maybes == other.maybes
|
457
|
+
end
|
458
|
+
|
459
|
+
def cacheable?
|
460
|
+
@cacheable ||= @musts.all?{|m| m.attr_conditions.values.all?{|ac| !ac.respond_to?(:call) } }
|
461
|
+
end
|
462
|
+
|
463
|
+
alias eql? ==
|
464
|
+
def hash
|
465
|
+
@_hash ||= self.musts.hash ^ self.maybes.hash
|
466
|
+
end
|
467
|
+
end
|
468
|
+
Q = Query
|
469
|
+
end
|
metadata
ADDED
@@ -0,0 +1,115 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: game_ecs
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Shawn Anderson
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2019-02-03 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.16'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.16'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '10.0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '10.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rspec
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '3.0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '3.0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: pry
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 0.12.0
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: 0.12.0
|
69
|
+
description: Entity Component System architecture in Ruby
|
70
|
+
email:
|
71
|
+
- shawn42@gmail.com
|
72
|
+
executables: []
|
73
|
+
extensions: []
|
74
|
+
extra_rdoc_files: []
|
75
|
+
files:
|
76
|
+
- ".gitignore"
|
77
|
+
- ".rspec"
|
78
|
+
- ".travis.yml"
|
79
|
+
- Gemfile
|
80
|
+
- LICENSE.txt
|
81
|
+
- README.md
|
82
|
+
- Rakefile
|
83
|
+
- contrib/sample_systems_components.rb
|
84
|
+
- contrib/simple_system.rb
|
85
|
+
- examples/coin.wav
|
86
|
+
- examples/coin_getter_game.rb
|
87
|
+
- game_ecs.gemspec
|
88
|
+
- lib/game_ecs.rb
|
89
|
+
- lib/game_ecs/entity_store.rb
|
90
|
+
- lib/game_ecs/version.rb
|
91
|
+
homepage: https://github.com/shawn42/game_ecs
|
92
|
+
licenses:
|
93
|
+
- MIT
|
94
|
+
metadata: {}
|
95
|
+
post_install_message:
|
96
|
+
rdoc_options: []
|
97
|
+
require_paths:
|
98
|
+
- lib
|
99
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - ">="
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0'
|
104
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
105
|
+
requirements:
|
106
|
+
- - ">="
|
107
|
+
- !ruby/object:Gem::Version
|
108
|
+
version: '0'
|
109
|
+
requirements: []
|
110
|
+
rubyforge_project:
|
111
|
+
rubygems_version: 2.6.11
|
112
|
+
signing_key:
|
113
|
+
specification_version: 4
|
114
|
+
summary: Entity Component System architecture in Ruby
|
115
|
+
test_files: []
|