draco 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/ruby.yml +22 -21
- data/.rubocop.yml +12 -0
- data/Gemfile +6 -2
- data/Gemfile.lock +21 -6
- data/README.md +16 -7
- data/Rakefile +3 -1
- data/benchmark.rb +84 -0
- data/bin/console +1 -0
- data/draco.gemspec +8 -6
- data/lib/draco.rb +349 -39
- metadata +7 -6
- data/.travis.yml +0 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: fabf6d184411f5d8dc5782499dfd2e7d07174333f6927566fb3fe51d59ae0df3
|
4
|
+
data.tar.gz: 5a647308729162793c8d4841b86f954298640d319dbfa836f8cb072f491c04cb
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6e754598051acec8e35c62d40d52c255bf2187fa68b6e2e18112de90661af7faa53b9bb7fc402b8425df99bac3fb6d9d6a3cf15dabb0f956004dd43a63d04ae7
|
7
|
+
data.tar.gz: a558139ee8f227e9307c406453b18a5280ceac36ee0386fbd3998eebfe975cb0fc6e21effff52292aab4b594fbef56498a69623aa75586d6229a0b34740613a2
|
data/.github/workflows/ruby.yml
CHANGED
@@ -9,31 +9,32 @@ name: Ruby
|
|
9
9
|
|
10
10
|
on:
|
11
11
|
push:
|
12
|
-
branches: [
|
12
|
+
branches: [main]
|
13
13
|
pull_request:
|
14
|
-
branches: [
|
14
|
+
branches: [main]
|
15
15
|
|
16
16
|
jobs:
|
17
17
|
test:
|
18
|
-
|
19
18
|
runs-on: ubuntu-latest
|
20
19
|
|
21
20
|
steps:
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
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
|
data/.rubocop.yml
ADDED
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
|
11
|
+
gem "rubocop", require: false
|
12
|
+
gem "simplecov", require: false
|
data/Gemfile.lock
CHANGED
@@ -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
|
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.
|
99
|
-
|
100
|
-
|
101
|
-
|
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
data/benchmark.rb
ADDED
@@ -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)
|
data/bin/console
CHANGED
data/draco.gemspec
CHANGED
@@ -1,4 +1,6 @@
|
|
1
|
-
|
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 =
|
10
|
-
spec.description =
|
11
|
-
spec.homepage = "https://
|
12
|
-
spec.required_ruby_version = Gem::Requirement.new(">= 2.
|
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
|
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"
|
data/lib/draco.rb
CHANGED
@@ -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
|
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.
|
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
|
-
|
46
|
-
|
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(
|
115
|
-
component = components.
|
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
|
-
|
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
|
-
|
168
|
-
|
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.
|
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
|
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.
|
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-
|
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
|
-
- ".
|
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://
|
37
|
+
homepage: https://github.com/guitsaru/draco
|
37
38
|
licenses: []
|
38
39
|
metadata:
|
39
|
-
homepage_uri: https://
|
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.
|
50
|
+
version: 2.4.0
|
50
51
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
51
52
|
requirements:
|
52
53
|
- - ">="
|