draco 0.1.0 → 0.2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 69a39f9f030f1724d09938774a480edfbbe5d342dd12e3e7cc4f652ec62ce88a
4
- data.tar.gz: 8c904f8141775453f61519ecdb9b8d9fc4ceeefdf066e38243a0da34b36efa48
3
+ metadata.gz: fabf6d184411f5d8dc5782499dfd2e7d07174333f6927566fb3fe51d59ae0df3
4
+ data.tar.gz: 5a647308729162793c8d4841b86f954298640d319dbfa836f8cb072f491c04cb
5
5
  SHA512:
6
- metadata.gz: 64bbfeac768aacc1edcd6dc9c7691deb7c5ce52b29299ee8f73693920ae7a470561667c113caa7a9410779f961de075ddc6561a639af343f7fad26e8e5d1bc38
7
- data.tar.gz: cf4631a868baea5f76f65e4f20be596825f3587f373b5d1bb9119a320d02ccf4cbfd65092e0b53be8ed7e10011c21f27a8f47b34b71051719af58bbc9bee542f
6
+ metadata.gz: 6e754598051acec8e35c62d40d52c255bf2187fa68b6e2e18112de90661af7faa53b9bb7fc402b8425df99bac3fb6d9d6a3cf15dabb0f956004dd43a63d04ae7
7
+ data.tar.gz: a558139ee8f227e9307c406453b18a5280ceac36ee0386fbd3998eebfe975cb0fc6e21effff52292aab4b594fbef56498a69623aa75586d6229a0b34740613a2
@@ -9,31 +9,32 @@ name: Ruby
9
9
 
10
10
  on:
11
11
  push:
12
- branches: [ main ]
12
+ branches: [main]
13
13
  pull_request:
14
- branches: [ main ]
14
+ branches: [main]
15
15
 
16
16
  jobs:
17
17
  test:
18
-
19
18
  runs-on: ubuntu-latest
20
19
 
21
20
  steps:
22
- - uses: actions/checkout@v2
23
- - name: Set up Ruby
24
- # To automatically get bug fixes and new Ruby versions for ruby/setup-ruby,
25
- # change this to (see https://github.com/ruby/setup-ruby#versioning):
26
- # uses: ruby/setup-ruby@v1
27
- uses: ruby/setup-ruby@ec106b438a1ff6ff109590de34ddc62c540232e0
28
- with:
29
- ruby-version: 2.6
30
- - name: Install dependencies
31
- run: bundle install
32
- - name: Run tests
33
- run: rake spec
34
- - name: Upload coverage results
35
- uses: actions/upload-artifact@master
36
- if: always()
37
- with:
38
- name: coverage-report
39
- path: coverage
21
+ - uses: actions/checkout@v2
22
+ - name: Set up Ruby
23
+ # To automatically get bug fixes and new Ruby versions for ruby/setup-ruby,
24
+ # change this to (see https://github.com/ruby/setup-ruby#versioning):
25
+ # uses: ruby/setup-ruby@v1
26
+ uses: ruby/setup-ruby@ec106b438a1ff6ff109590de34ddc62c540232e0
27
+ with:
28
+ ruby-version: 2.6
29
+ - name: Install dependencies
30
+ run: bundle install
31
+ - name: Run tests
32
+ run: bundle exec rake spec
33
+ - name: Upload coverage results
34
+ uses: actions/upload-artifact@master
35
+ if: always()
36
+ with:
37
+ name: coverage-report
38
+ path: coverage
39
+ - name: Rubocop
40
+ run: bundle exec rubocop
@@ -0,0 +1,12 @@
1
+ Metrics/BlockLength:
2
+ Exclude:
3
+ - "Rakefile"
4
+ - "spec/**/*.rb"
5
+
6
+ AllCops:
7
+ NewCops: enable
8
+ Exclude:
9
+ - "benchmark.rb"
10
+
11
+ Style/StringLiterals:
12
+ EnforcedStyle: double_quotes
data/Gemfile CHANGED
@@ -1,8 +1,12 @@
1
+ # frozen_string_literal: true
2
+
1
3
  source "https://rubygems.org"
2
4
 
5
+ # Commented out because we have no dependencies and it interferes with simplecov
3
6
  # Specify your gem's dependencies in draco.gemspec
4
- gemspec
7
+ # gemspec
5
8
 
6
9
  gem "rake", "~> 12.0"
7
10
  gem "rspec", "~> 3.0"
8
- gem 'simplecov', require: false
11
+ gem "rubocop", require: false
12
+ gem "simplecov", require: false
@@ -1,14 +1,16 @@
1
- PATH
2
- remote: .
3
- specs:
4
- draco (0.1.0)
5
-
6
1
  GEM
7
2
  remote: https://rubygems.org/
8
3
  specs:
4
+ ast (2.4.1)
9
5
  diff-lcs (1.4.4)
10
6
  docile (1.3.2)
7
+ parallel (1.19.2)
8
+ parser (2.7.2.0)
9
+ ast (~> 2.4.1)
10
+ rainbow (3.0.0)
11
11
  rake (12.3.3)
12
+ regexp_parser (1.8.2)
13
+ rexml (3.2.4)
12
14
  rspec (3.10.0)
13
15
  rspec-core (~> 3.10.0)
14
16
  rspec-expectations (~> 3.10.0)
@@ -22,18 +24,31 @@ GEM
22
24
  diff-lcs (>= 1.2.0, < 2.0)
23
25
  rspec-support (~> 3.10.0)
24
26
  rspec-support (3.10.0)
27
+ rubocop (1.2.0)
28
+ parallel (~> 1.10)
29
+ parser (>= 2.7.1.5)
30
+ rainbow (>= 2.2.2, < 4.0)
31
+ regexp_parser (>= 1.8)
32
+ rexml
33
+ rubocop-ast (>= 1.0.1)
34
+ ruby-progressbar (~> 1.7)
35
+ unicode-display_width (>= 1.4.0, < 2.0)
36
+ rubocop-ast (1.1.1)
37
+ parser (>= 2.7.1.5)
38
+ ruby-progressbar (1.10.1)
25
39
  simplecov (0.19.0)
26
40
  docile (~> 1.1)
27
41
  simplecov-html (~> 0.11)
28
42
  simplecov-html (0.12.2)
43
+ unicode-display_width (1.7.0)
29
44
 
30
45
  PLATFORMS
31
46
  ruby
32
47
 
33
48
  DEPENDENCIES
34
- draco!
35
49
  rake (~> 12.0)
36
50
  rspec (~> 3.0)
51
+ rubocop
37
52
  simplecov
38
53
 
39
54
  BUNDLED WITH
data/README.md CHANGED
@@ -23,7 +23,7 @@ These can be shared across many different types of game objects.
23
23
  class Visible < Draco::Component; end
24
24
  ```
25
25
 
26
- `Visible` is an example of a flag component. An entity either has it, or it doesn't. We can also associate data with our
26
+ `Visible` is an example of a label component. An entity either has it, or it doesn't. We can also associate data with our
27
27
  components.
28
28
 
29
29
  ```ruby
@@ -95,17 +95,17 @@ class RenderSpriteSystem < Draco::System
95
95
  # You can also access the world that called the system.
96
96
  camera = world.filter([Camera]).first
97
97
 
98
- entities.each do |entity|
99
- next unless
100
-
101
- args.outputs.sprites << {
102
- x: entity.position.x,
103
- y: entity.position.y,
98
+ sprites = entities.select { |e| entity_in_camera?(e, camera) }.map do |entity|
99
+ {
100
+ x: entity.position.x - camera.position.x,
101
+ y: entity.position.y - camera.position.y,
104
102
  w: entity.sprite.w,
105
103
  h: entity.sprite.h,
106
104
  path: entity.sprite.path
107
105
  }
108
106
  end
107
+
108
+ args.outputs.sprites << sprites
109
109
  end
110
110
 
111
111
  def entity_in_camera?(entity, camera)
@@ -129,10 +129,19 @@ world.systems << RenderSpriteSystem
129
129
  world.tick(args)
130
130
  ```
131
131
 
132
+ ## Learn More
133
+
134
+ Here are some good resources to learn about Entity Component Systems
135
+
136
+ - [Evolve Your Heirarchy](https://cowboyprogramming.com/2007/01/05/evolve-your-heirachy/)
137
+ - [Overwatch Gameplay Architecture and Netcode](https://www.youtube.com/watch?v=W3aieHjyNvw)
138
+
132
139
  ## Commercial License
133
140
 
134
141
  Draco is licensed under AGPL. You can purchase the right to use Draco under the [commercial license](https://github.com/guitsaru/draco/blob/master/COMM-LICENSE).
135
142
 
143
+ <a class="gumroad-button" href="https://guitsaru.itch.io/draco" target="_blank">Purchase Commercial License</a>
144
+
136
145
  ## Development
137
146
 
138
147
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
data/Rakefile CHANGED
@@ -1,6 +1,8 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "bundler/gem_tasks"
2
4
  require "rspec/core/rake_task"
3
5
 
4
6
  RSpec::Core::RakeTask.new(:spec)
5
7
 
6
- task :default => :spec
8
+ task default: :spec
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/draco"
4
+ require "benchmark"
5
+
6
+ puts "Defining Component"
7
+ class SampleComponent < Draco::Component
8
+ attribute :a, default: 0
9
+ attribute :b, default: 0
10
+ attribute :c, default: 0
11
+ attribute :d, default: 0
12
+ attribute :e, default: 0
13
+ attribute :f, default: 0
14
+ attribute :g, default: 0
15
+ end
16
+
17
+ puts "Defining System"
18
+ class SampleSystem < Draco::System
19
+ filter SampleComponent
20
+
21
+ def tick(_)
22
+ entities.each do |entity|
23
+ entity.sample_component.a
24
+ end
25
+ end
26
+ end
27
+
28
+ puts "Dynamic Components"
29
+ dynamic_components = (1..1000).map do |i|
30
+ name = "DynamicComponent#{i}"
31
+ klass = Class.new(SampleComponent)
32
+
33
+ Object.const_set(name, klass)
34
+ klass
35
+ end
36
+
37
+ puts "Defining Entity"
38
+ class SampleEntity < Draco::Entity
39
+ component SampleComponent
40
+ end
41
+
42
+ puts "Adding dynamic components to Entity"
43
+ dynamic_components.each do |dynamic_component|
44
+ SampleEntity.component(dynamic_component) if [true, false, false, false, false].sample
45
+ end
46
+
47
+ puts "Defining World"
48
+ world = Draco::World.new
49
+ world.systems << SampleSystem
50
+
51
+ Benchmark.bm do |bm|
52
+ bm.report("initialize entity") do
53
+ SampleEntity.new
54
+ end
55
+ end
56
+
57
+ puts "Generating 10_000 entities"
58
+ (1..10_000).each do |i|
59
+ print "#{i} " if (i % 100).zero?
60
+
61
+ entity = SampleEntity.new
62
+ world.entities << entity
63
+ end
64
+
65
+ puts
66
+ puts "Goal: #{1.0 / 60.0}"
67
+
68
+ Benchmark.bm do |bm|
69
+ bm.report("tick") do
70
+ world.tick(nil)
71
+ end
72
+
73
+ bm.report("filter") { world.filter(SampleComponent) }
74
+ end
75
+
76
+ # Initial Implementation
77
+ # user system total real
78
+ # tick 0.345672 0.006669 0.352341 ( 0.354792)
79
+ # filter 0.288709 0.000000 0.288709 ( 0.290659)
80
+
81
+ # After optimization
82
+ # user system total real
83
+ # tick 0.007575 0.000005 0.007580 ( 0.007589)
84
+ # filter 0.000013 0.000000 0.000013 ( 0.000012)
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
2
3
 
3
4
  require "bundler/setup"
4
5
  require "draco"
@@ -1,4 +1,6 @@
1
- require_relative 'lib/draco'
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/draco"
2
4
 
3
5
  Gem::Specification.new do |spec|
4
6
  spec.name = "draco"
@@ -6,17 +8,17 @@ Gem::Specification.new do |spec|
6
8
  spec.authors = ["Matt Pruitt"]
7
9
  spec.email = ["matt@guitsaru.com"]
8
10
 
9
- spec.summary = %q{An ECS library.}
10
- spec.description = %q{A library for Entities, Components, and Systems in games.}
11
- spec.homepage = "https://mattpruitt.com"
12
- spec.required_ruby_version = Gem::Requirement.new(">= 2.3.0")
11
+ spec.summary = "An ECS library."
12
+ spec.description = "A library for Entities, Components, and Systems in games."
13
+ spec.homepage = "https://github.com/guitsaru/draco"
14
+ spec.required_ruby_version = Gem::Requirement.new(">= 2.4.0")
13
15
 
14
16
  spec.metadata["homepage_uri"] = spec.homepage
15
17
  spec.metadata["source_code_uri"] = "https://github.com/guitsaru/draco"
16
18
 
17
19
  # Specify which files should be added to the gem when it is released.
18
20
  # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
19
- spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
21
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
20
22
  `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
21
23
  end
22
24
  spec.bindir = "exe"
@@ -1,27 +1,25 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # Public: Draco is an Entity Component System for use in game engines like DragonRuby.
2
4
  #
3
- # An Entity Component System is an architectural pattern used in game development to decouple behaviour from game objects.
5
+ # An Entity Component System is an architectural pattern used in game development to decouple behavior from objects.
4
6
  module Draco
5
7
  # Public: The version of the library. Draco uses semver to version releases.
6
- VERSION = "0.1.0"
8
+ VERSION = "0.2.0"
7
9
 
8
10
  # Public: A general purpose game object that consists of a unique id and a collection of Components.
9
11
  class Entity
12
+ # rubocop:disable Style/ClassVars
10
13
  @default_components = {}
11
14
  @@next_id = 1
12
15
 
13
- # Public: Returns the Integer id of the Entity.
14
- attr_reader :id
15
-
16
- # Public: Returns the Array of the Entity's components
17
- attr_reader :components
18
-
19
16
  # Internal: Resets the default components for each class that inherites Entity.
20
17
  #
21
18
  # sub - The class that is inheriting Entity.
22
19
  #
23
20
  # Returns nothing.
24
21
  def self.inherited(sub)
22
+ super
25
23
  sub.instance_variable_set(:@default_components, {})
26
24
  end
27
25
 
@@ -42,10 +40,16 @@ module Draco
42
40
  end
43
41
 
44
42
  # Internal: Returns the default components for the class.
45
- def self.default_components
46
- @default_components
43
+ class << self
44
+ attr_reader :default_components
47
45
  end
48
46
 
47
+ # Public: Returns the Integer id of the Entity.
48
+ attr_reader :id
49
+
50
+ # Public: Returns the Array of the Entity's components
51
+ attr_reader :components
52
+
49
53
  # Public: Initialize a new Entity.
50
54
  #
51
55
  # args - A Hash of arguments to pass into the components.
@@ -60,22 +64,39 @@ module Draco
60
64
  def initialize(args = {})
61
65
  @id = args.fetch(:id, @@next_id)
62
66
  @@next_id = [@id + 1, @@next_id].max
63
- @components = []
67
+ @components = ComponentStore.new(self)
68
+ @subscriptions = []
64
69
 
65
70
  self.class.default_components.each do |component, default_args|
66
- arguments = default_args.merge(args[underscore(component.name.to_s).to_sym] || {})
71
+ arguments = default_args.merge(args[Draco.underscore(component.name.to_s).to_sym] || {})
67
72
  @components << component.new(arguments)
68
73
  end
69
74
  end
70
75
 
76
+ # Public: Subscribe to an Entity's Component updates.
77
+ #
78
+ # subscriber - The object to notify when Components change.
79
+ #
80
+ # Returns nothing.
81
+ def subscribe(subscriber)
82
+ @subscriptions << subscriber
83
+ end
84
+
85
+ # Internal: Notifies subscribers that components have been updated.
86
+ #
87
+ # Returns nothing.
88
+ def components_updated
89
+ @subscriptions.each { |sub| sub.entity_updated(self) }
90
+ end
91
+
71
92
  # Public: Serializes the Entity to save the current state.
72
93
  #
73
94
  # Returns a Hash representing the Entity.
74
95
  def serialize
75
- serialized = {id: id}
96
+ serialized = { id: id }
76
97
 
77
98
  components.each do |component|
78
- serialized[underscore(component.class.name.to_s).to_sym] = component.serialize
99
+ serialized[Draco.underscore(component.class.name.to_s).to_sym] = component.serialize
79
100
  end
80
101
 
81
102
  serialized
@@ -111,31 +132,95 @@ module Draco
111
132
  #
112
133
  # Returns the Component instance.
113
134
 
114
- def method_missing(m, *args, &block)
115
- component = components.find { |c| underscore(c.class.name.to_s) == m.to_s }
135
+ def method_missing(method, *args, &block)
136
+ component = components[method.to_sym]
116
137
  return component if component
117
138
 
118
139
  super
119
140
  end
120
141
 
121
- # Internal: Converts a camel cased string to an underscored string.
122
- #
123
- # Examples
124
- #
125
- # underscore("CamelCase")
126
- # # => "camel_case"
127
- #
128
- # Returns a String.
129
- def underscore(string)
130
- string.split("::").last.bytes.map.with_index do |byte, i|
131
- if byte > 64 && byte < 97
132
- downcased = byte + 32
133
- i == 0 ? downcased.chr : "_#{downcased.chr}"
134
- else
135
- byte.chr
136
- end
137
- end.join
142
+ def respond_to_missing?(method, _include_private = false)
143
+ !!components[method.to_sym] or super
138
144
  end
145
+
146
+ # Internal: An Array that notifies it's parent of updates.
147
+ class ComponentStore
148
+ include Enumerable
149
+
150
+ # Internal: Initializes a new ComponentStore
151
+ #
152
+ # parent - The object to notify about updates.
153
+ def initialize(parent)
154
+ @components = {}
155
+ @parent = parent
156
+ end
157
+
158
+ # Internal: Adds Components to the ComponentStore.
159
+ #
160
+ # Side Effects: Notifies the parent that the components were updated.
161
+ #
162
+ # components - The Component or Array list of Components to add to the ComponentStore.
163
+ #
164
+ # Returns the ComponentStore.
165
+ def <<(*components)
166
+ components.flatten.each { |component| add(component) }
167
+
168
+ @parent.components_updated
169
+ self
170
+ end
171
+
172
+ # Internal: Returns the Component with the underscored Component name.
173
+ #
174
+ # underscored_component - The String underscored version of the Component's class name.
175
+ #
176
+ # Returns the Component instance or nil.
177
+ def [](underscored_component)
178
+ @components[underscored_component]
179
+ end
180
+
181
+ # Internal: Adds a Component to the ComponentStore.
182
+ #
183
+ # Side Effects: Notifies the parent that the components were updated.
184
+ #
185
+ # components - The Component to add to the ComponentStore.
186
+ #
187
+ # Returns the ComponentStore.
188
+ def add(component)
189
+ name = Draco.underscore(component.class.name.to_s).to_sym
190
+ @components[name] = component
191
+
192
+ @parent.components_updated
193
+ self
194
+ end
195
+
196
+ # Internal: Removes a Component from the ComponentStore.
197
+ #
198
+ # Side Effects: Notifies the parent that the components were updated.
199
+ #
200
+ # components - The Component to remove from the ComponentStore.
201
+ #
202
+ # Returns the ComponentStore.
203
+ def delete(component)
204
+ name = Draco.underscore(component.class.name.to_s).to_sym
205
+ @components.delete(name)
206
+
207
+ @parent.components_updated
208
+ self
209
+ end
210
+
211
+ # Internal: Returns true if there are no entries in the Set.
212
+ #
213
+ # Returns a boolean.
214
+ def empty?
215
+ @components.empty?
216
+ end
217
+
218
+ # Internal: Returns an Enumerator for all of the Entities.
219
+ def each(&block)
220
+ @components.values.each(&block)
221
+ end
222
+ end
223
+ # rubocop:enable Style/ClassVars
139
224
  end
140
225
 
141
226
  # Public: The data to associate with an Entity.
@@ -148,6 +233,7 @@ module Draco
148
233
  #
149
234
  # Returns nothing.
150
235
  def self.inherited(sub)
236
+ super
151
237
  sub.instance_variable_set(:@attribute_options, {})
152
238
  end
153
239
 
@@ -160,12 +246,13 @@ module Draco
160
246
  # Returns nothing.
161
247
  def self.attribute(name, options = {})
162
248
  attr_accessor name
249
+
163
250
  @attribute_options[name] = options
164
251
  end
165
252
 
166
253
  # Internal: Returns the Hash attribute options for the current Class.
167
- def self.attribute_options
168
- @attribute_options
254
+ class << self
255
+ attr_reader :attribute_options
169
256
  end
170
257
 
171
258
  # Public: Initializes a new Component.
@@ -213,7 +300,8 @@ module Draco
213
300
  end
214
301
  end
215
302
 
216
- # Public: Systems contain the logic of the game. The System runs on each tick and manipulates the Entities in the World.
303
+ # Public: Systems contain the logic of the game.
304
+ # The System runs on each tick and manipulates the Entities in the World.
217
305
  class System
218
306
  @filter = []
219
307
 
@@ -240,6 +328,7 @@ module Draco
240
328
  #
241
329
  # Returns nothing.
242
330
  def self.inherited(sub)
331
+ super
243
332
  sub.instance_variable_set(:@filter, [])
244
333
  end
245
334
 
@@ -295,7 +384,7 @@ module Draco
295
384
  # entities - The Array of Entities for the World (default: []).
296
385
  # systems - The Array of System Classes for the World (default: []).
297
386
  def initialize(entities: [], systems: [])
298
- @entities = entities
387
+ @entities = EntityStore.new(entities)
299
388
  @systems = systems
300
389
  end
301
390
 
@@ -317,8 +406,8 @@ module Draco
317
406
  # components - An Array of Component classes to match.
318
407
  #
319
408
  # Returns an Array of matching Entities.
320
- def filter(components)
321
- entities.select { |e| (components - e.components.map(&:class)).empty? }
409
+ def filter(*components)
410
+ entities[components.flatten]
322
411
  end
323
412
 
324
413
  # Public: Serializes the World to save the current state.
@@ -340,5 +429,226 @@ module Draco
340
429
  def to_s
341
430
  serialize.to_s
342
431
  end
432
+
433
+ # Internal: Stores Entities with better performance than Array.
434
+ class EntityStore
435
+ include Enumerable
436
+
437
+ # Internal: Initializes a new EntityStore
438
+ #
439
+ # entities - The Entities to add to the EntityStore
440
+ def initialize(*entities)
441
+ @entity_to_components = Hash.new { |hash, key| hash[key] = Set.new }
442
+ @component_to_entities = Hash.new { |hash, key| hash[key] = Set.new }
443
+
444
+ self << entities
445
+ end
446
+
447
+ # Internal: Gets all Entities that implement all of the given Components
448
+ #
449
+ # components - The Component Classes to filter by
450
+ #
451
+ # Returns a Set list of Entities
452
+ def [](*components)
453
+ components
454
+ .flatten
455
+ .map { |component| @component_to_entities[component] }
456
+ .reduce { |acc, i| i & acc }
457
+ end
458
+
459
+ # Internal: Adds Entities to the EntityStore
460
+ #
461
+ # entities - The Entity or Array list of Entities to add to the EntityStore.
462
+ #
463
+ # Returns the EntityStore
464
+ def <<(entities)
465
+ Array(entities).flatten.each { |e| add(e) }
466
+ self
467
+ end
468
+
469
+ # Internal: Adds an Entity to the EntityStore.
470
+ #
471
+ # entity - The Entity to add to the EntityStore.
472
+ #
473
+ # Returns the EntityStore
474
+ def add(entity)
475
+ entity.subscribe(self)
476
+
477
+ components = entity.components.map(&:class)
478
+ @entity_to_components[entity].merge(components)
479
+
480
+ components.each do |component|
481
+ @component_to_entities[component].add(entity)
482
+ end
483
+
484
+ self
485
+ end
486
+
487
+ # Internal: Removes an Entity from the EntityStore.
488
+ #
489
+ # entity - The Entity to remove from the EntityStore.
490
+ #
491
+ # Returns the EntityStore
492
+ def delete(entity)
493
+ components = @entity_to_components.delete(entity)
494
+
495
+ components.map(&:class).each do |component|
496
+ @component_to_entities[component].delete(entity)
497
+ end
498
+ end
499
+
500
+ # Internal: Returns true if the EntityStore has no Entities.
501
+ def empty?
502
+ @entity_to_components.empty?
503
+ end
504
+
505
+ # Internal: Returns an Enumerator for all of the Entities.
506
+ def each(&block)
507
+ @entity_to_components.keys.each(&block)
508
+ end
509
+
510
+ # Internal: Updates the EntityStore when an Entity's Components are modified.
511
+ #
512
+ # entity - The Entity whose Components were updated.
513
+ #
514
+ # Returns nothing.
515
+ def entity_updated(entity)
516
+ old = @entity_to_components[entity].to_a
517
+ components = entity.components.map(&:class)
518
+ @entity_to_components[entity] = components
519
+
520
+ added = components - old
521
+ deleted = old - components
522
+
523
+ added.each { |component| @component_to_entities[component].add(entity) }
524
+ deleted.each { |component| @component_to_entities[component].delete(entity) }
525
+ end
526
+ end
527
+ end
528
+
529
+ # Internal: An implementation of Set.
530
+ class Set
531
+ include Enumerable
532
+
533
+ # Internal: Initializes a new Set.
534
+ #
535
+ # entries - The initial Array list of entries for the Set
536
+ def initialize(entries = [])
537
+ @hash = {}
538
+ merge(entries)
539
+ end
540
+
541
+ # Internal: Adds a new entry to the Set.
542
+ #
543
+ # entry - The object to add to the Set.
544
+ #
545
+ # Returns the Set.
546
+ def add(entry)
547
+ @hash[entry] = true
548
+ self
549
+ end
550
+
551
+ # Internal: Adds a new entry to the Set.
552
+ #
553
+ # entry - The object to add to the Set.
554
+ #
555
+ # Returns the Set.
556
+ def delete(entry)
557
+ @hash.delete(entry)
558
+ self
559
+ end
560
+
561
+ # Internal: Adds multiple objects to the Set.
562
+ #
563
+ # entry - The Array list of objects to add to the Set.
564
+ #
565
+ # Returns the Set.
566
+ def merge(entries)
567
+ Array(entries).each { |entry| add(entry) }
568
+ self
569
+ end
570
+
571
+ # Internal: Returns an Enumerator for all of the entries in the Set.
572
+ def each(&block)
573
+ @hash.keys.each(&block)
574
+ end
575
+
576
+ # Internal: Returns true if the object is in the Set.
577
+ #
578
+ # member - The object to search the Set for.
579
+ #
580
+ # Returns a boolean.
581
+ def member?(member)
582
+ @hash.key?(member)
583
+ end
584
+
585
+ # Internal: Returns true if there are no entries in the Set.
586
+ #
587
+ # Returns a boolean.
588
+ def empty?
589
+ @hash.empty?
590
+ end
591
+
592
+ # Internal: Returns the intersection of two Sets.
593
+ #
594
+ # other - The Set to intersect with
595
+ #
596
+ # Returns a new Set of all of the common entries.
597
+ def &(other)
598
+ response = Set.new
599
+ each do |key, _|
600
+ response.add(key) if other.member?(key)
601
+ end
602
+
603
+ response
604
+ end
605
+
606
+ def ==(other)
607
+ hash == other.hash
608
+ end
609
+
610
+ # Internal: Returns a unique hash value of the Set.
611
+ def hash
612
+ @hash.hash
613
+ end
614
+
615
+ # Internal: Returns an Array representation of the Set.
616
+ def to_a
617
+ @hash.keys
618
+ end
619
+
620
+ # Internal: Serializes the Set.
621
+ def serialize
622
+ to_a.inspect
623
+ end
624
+
625
+ # Internal: Inspects the Set.
626
+ def inspect
627
+ to_a.inspect
628
+ end
629
+
630
+ # Internal: Returns a String representation of the Set.
631
+ def to_s
632
+ to_a.to_s
633
+ end
634
+ end
635
+
636
+ # Internal: Converts a camel cased string to an underscored string.
637
+ #
638
+ # Examples
639
+ #
640
+ # underscore("CamelCase")
641
+ # # => "camel_case"
642
+ #
643
+ # Returns a String.
644
+ def self.underscore(string)
645
+ string.split("::").last.bytes.map.with_index do |byte, i|
646
+ if byte > 64 && byte < 97
647
+ downcased = byte + 32 # gemspec
648
+ i.zero? ? downcased.chr : "_#{downcased.chr}"
649
+ else
650
+ byte.chr
651
+ end
652
+ end.join
343
653
  end
344
654
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: draco
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Matt Pruitt
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2020-11-04 00:00:00.000000000 Z
11
+ date: 2020-11-08 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: A library for Entities, Components, and Systems in games.
14
14
  email:
@@ -21,7 +21,7 @@ files:
21
21
  - ".github/workflows/ruby.yml"
22
22
  - ".gitignore"
23
23
  - ".rspec"
24
- - ".travis.yml"
24
+ - ".rubocop.yml"
25
25
  - CODE_OF_CONDUCT.md
26
26
  - COMM-LICENSE
27
27
  - Gemfile
@@ -29,14 +29,15 @@ files:
29
29
  - LICENSE
30
30
  - README.md
31
31
  - Rakefile
32
+ - benchmark.rb
32
33
  - bin/console
33
34
  - bin/setup
34
35
  - draco.gemspec
35
36
  - lib/draco.rb
36
- homepage: https://mattpruitt.com
37
+ homepage: https://github.com/guitsaru/draco
37
38
  licenses: []
38
39
  metadata:
39
- homepage_uri: https://mattpruitt.com
40
+ homepage_uri: https://github.com/guitsaru/draco
40
41
  source_code_uri: https://github.com/guitsaru/draco
41
42
  post_install_message:
42
43
  rdoc_options: []
@@ -46,7 +47,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
46
47
  requirements:
47
48
  - - ">="
48
49
  - !ruby/object:Gem::Version
49
- version: 2.3.0
50
+ version: 2.4.0
50
51
  required_rubygems_version: !ruby/object:Gem::Requirement
51
52
  requirements:
52
53
  - - ">="
@@ -1,6 +0,0 @@
1
- ---
2
- language: ruby
3
- cache: bundler
4
- rvm:
5
- - 2.7.1
6
- before_install: gem install bundler -v 2.1.4