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 +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
|
- - ">="
|