draco 0.1.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 +7 -0
- data/.github/contributing.md +7 -0
- data/.github/workflows/ruby.yml +39 -0
- data/.gitignore +11 -0
- data/.rspec +3 -0
- data/.travis.yml +6 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/COMM-LICENSE +129 -0
- data/Gemfile +8 -0
- data/Gemfile.lock +40 -0
- data/LICENSE +661 -0
- data/README.md +150 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/draco.gemspec +25 -0
- data/lib/draco.rb +344 -0
- metadata +60 -0
data/README.md
ADDED
@@ -0,0 +1,150 @@
|
|
1
|
+
# Draco
|
2
|
+
|
3
|
+
Draco is an Entity Component System for DragonRuby GTK.
|
4
|
+
|
5
|
+
An Entity Component System is an architectural framework that decouples game objects from game logic. An allows you to
|
6
|
+
build game objects through composition. This allows you to easily share small logic components between different game
|
7
|
+
objects.
|
8
|
+
|
9
|
+
## Installation
|
10
|
+
|
11
|
+
1. Create a `lib` directory inside your game's `app` directory.
|
12
|
+
2. Copy `lib/draco.rb` into your new `lib` directory.
|
13
|
+
3. In your `main.rb` file, require `app/lib/draco.rb`.
|
14
|
+
|
15
|
+
## Usage
|
16
|
+
|
17
|
+
### Components
|
18
|
+
|
19
|
+
Components represent small atomic bits of state. It's better to use many small components than few large components.
|
20
|
+
These can be shared across many different types of game objects.
|
21
|
+
|
22
|
+
```ruby
|
23
|
+
class Visible < Draco::Component; end
|
24
|
+
```
|
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
|
27
|
+
components.
|
28
|
+
|
29
|
+
```ruby
|
30
|
+
class Position < Draco::Component
|
31
|
+
attribute :x, default: 0
|
32
|
+
attribute :y, default: 0
|
33
|
+
end
|
34
|
+
|
35
|
+
component = Position.new
|
36
|
+
```
|
37
|
+
|
38
|
+
The component's attributes add a getter and setter on the component for ease of use.
|
39
|
+
|
40
|
+
```ruby
|
41
|
+
component.x = 110
|
42
|
+
|
43
|
+
component.x
|
44
|
+
# => 110
|
45
|
+
```
|
46
|
+
|
47
|
+
### Entities
|
48
|
+
|
49
|
+
Entities are independant game objects. They consist of a unique id and a list of components.
|
50
|
+
|
51
|
+
```ruby
|
52
|
+
entity = Draco::Entity.new
|
53
|
+
entity.components << Position.new(x: 50, y: 50)
|
54
|
+
```
|
55
|
+
|
56
|
+
Often we have types of entities that are reused throughout the game. We can define our own subclass in order to automate creating these entities.
|
57
|
+
|
58
|
+
```ruby
|
59
|
+
class Goblin < Draco::Entity
|
60
|
+
component Position, x: 50, y: 50
|
61
|
+
component Visible
|
62
|
+
end
|
63
|
+
|
64
|
+
goblin = Goblin.new
|
65
|
+
```
|
66
|
+
|
67
|
+
We can override the default values for the given components when initializing a new entity.
|
68
|
+
|
69
|
+
```ruby
|
70
|
+
goblin = Goblin.new(position: {x: 100, y: 100})
|
71
|
+
```
|
72
|
+
|
73
|
+
In order to access the data within our entity's components, the entity has a method named after that component. This is generated based on the
|
74
|
+
underscored name of the component's class (e.g. `MapLayer` would be `map_layer`).
|
75
|
+
|
76
|
+
```ruby
|
77
|
+
goblin.position.x
|
78
|
+
# => 100
|
79
|
+
```
|
80
|
+
|
81
|
+
### Systems
|
82
|
+
|
83
|
+
Systems encapsulate all of the logic of your game. The system runs on every tick and it's job is to update the state of the entities.
|
84
|
+
|
85
|
+
#### Filters
|
86
|
+
|
87
|
+
Each system can set a default filter by passing in a list of components. When the world runs the system, it will set the system's entities to the
|
88
|
+
entities that include all of the given components.
|
89
|
+
|
90
|
+
```ruby
|
91
|
+
class RenderSpriteSystem < Draco::System
|
92
|
+
filter Visible, Position, Sprite
|
93
|
+
|
94
|
+
def tick(args)
|
95
|
+
# You can also access the world that called the system.
|
96
|
+
camera = world.filter([Camera]).first
|
97
|
+
|
98
|
+
entities.each do |entity|
|
99
|
+
next unless
|
100
|
+
|
101
|
+
args.outputs.sprites << {
|
102
|
+
x: entity.position.x,
|
103
|
+
y: entity.position.y,
|
104
|
+
w: entity.sprite.w,
|
105
|
+
h: entity.sprite.h,
|
106
|
+
path: entity.sprite.path
|
107
|
+
}
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
def entity_in_camera?(entity, camera)
|
112
|
+
camera_rect = {x: camera.x, y: camera.y, w: camera.w, h: camera.h}
|
113
|
+
entity_rect = {x: entity.position.x, y: entity.position.y, w: entity.sprite.w, h: entity.sprite.h}
|
114
|
+
|
115
|
+
entity_rect.intersect_rect?(camera_rect)
|
116
|
+
end
|
117
|
+
end
|
118
|
+
```
|
119
|
+
|
120
|
+
### Worlds
|
121
|
+
|
122
|
+
A world keeps track of all current entities and runs all of the systems on every tick.
|
123
|
+
|
124
|
+
```ruby
|
125
|
+
world = Draco::World.new
|
126
|
+
world.entities << goblin
|
127
|
+
world.systems << RenderSpriteSystem
|
128
|
+
|
129
|
+
world.tick(args)
|
130
|
+
```
|
131
|
+
|
132
|
+
## Commercial License
|
133
|
+
|
134
|
+
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
|
+
|
136
|
+
## Development
|
137
|
+
|
138
|
+
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.
|
139
|
+
|
140
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
141
|
+
|
142
|
+
## Contributing
|
143
|
+
|
144
|
+
Before submitting a pull request, please read the [contribution guidelines](https://github.com/guitsaru/draco/blob/master/.github/contributing.md).
|
145
|
+
|
146
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/guitsaru/draco. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/guitsaru/draco/blob/master/CODE_OF_CONDUCT.md).
|
147
|
+
|
148
|
+
## Code of Conduct
|
149
|
+
|
150
|
+
Everyone interacting in the Draco project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/guitsaru/draco/blob/master/CODE_OF_CONDUCT.md).
|
data/Rakefile
ADDED
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "draco"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start(__FILE__)
|
data/bin/setup
ADDED
data/draco.gemspec
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
require_relative 'lib/draco'
|
2
|
+
|
3
|
+
Gem::Specification.new do |spec|
|
4
|
+
spec.name = "draco"
|
5
|
+
spec.version = Draco::VERSION
|
6
|
+
spec.authors = ["Matt Pruitt"]
|
7
|
+
spec.email = ["matt@guitsaru.com"]
|
8
|
+
|
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")
|
13
|
+
|
14
|
+
spec.metadata["homepage_uri"] = spec.homepage
|
15
|
+
spec.metadata["source_code_uri"] = "https://github.com/guitsaru/draco"
|
16
|
+
|
17
|
+
# Specify which files should be added to the gem when it is released.
|
18
|
+
# 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
|
20
|
+
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
21
|
+
end
|
22
|
+
spec.bindir = "exe"
|
23
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
24
|
+
spec.require_paths = ["lib"]
|
25
|
+
end
|
data/lib/draco.rb
ADDED
@@ -0,0 +1,344 @@
|
|
1
|
+
# Public: Draco is an Entity Component System for use in game engines like DragonRuby.
|
2
|
+
#
|
3
|
+
# An Entity Component System is an architectural pattern used in game development to decouple behaviour from game objects.
|
4
|
+
module Draco
|
5
|
+
# Public: The version of the library. Draco uses semver to version releases.
|
6
|
+
VERSION = "0.1.0"
|
7
|
+
|
8
|
+
# Public: A general purpose game object that consists of a unique id and a collection of Components.
|
9
|
+
class Entity
|
10
|
+
@default_components = {}
|
11
|
+
@@next_id = 1
|
12
|
+
|
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
|
+
# Internal: Resets the default components for each class that inherites Entity.
|
20
|
+
#
|
21
|
+
# sub - The class that is inheriting Entity.
|
22
|
+
#
|
23
|
+
# Returns nothing.
|
24
|
+
def self.inherited(sub)
|
25
|
+
sub.instance_variable_set(:@default_components, {})
|
26
|
+
end
|
27
|
+
|
28
|
+
# Public: Adds a default component to the Entity.
|
29
|
+
#
|
30
|
+
# component - The class of the Component to add by default.
|
31
|
+
# defaults - The Hash of default values for the Component data. (default: {})
|
32
|
+
#
|
33
|
+
# Examples
|
34
|
+
#
|
35
|
+
# component(Visible)
|
36
|
+
#
|
37
|
+
# component(Position, x: 0, y: 0)
|
38
|
+
#
|
39
|
+
# Returns nothing.
|
40
|
+
def self.component(component, defaults = {})
|
41
|
+
@default_components[component] = defaults
|
42
|
+
end
|
43
|
+
|
44
|
+
# Internal: Returns the default components for the class.
|
45
|
+
def self.default_components
|
46
|
+
@default_components
|
47
|
+
end
|
48
|
+
|
49
|
+
# Public: Initialize a new Entity.
|
50
|
+
#
|
51
|
+
# args - A Hash of arguments to pass into the components.
|
52
|
+
#
|
53
|
+
# Examples
|
54
|
+
#
|
55
|
+
# class Player < Draco::Entity
|
56
|
+
# component Position, x: 0, y: 0
|
57
|
+
# end
|
58
|
+
#
|
59
|
+
# Player.new(position: {x: 100, y: 100})
|
60
|
+
def initialize(args = {})
|
61
|
+
@id = args.fetch(:id, @@next_id)
|
62
|
+
@@next_id = [@id + 1, @@next_id].max
|
63
|
+
@components = []
|
64
|
+
|
65
|
+
self.class.default_components.each do |component, default_args|
|
66
|
+
arguments = default_args.merge(args[underscore(component.name.to_s).to_sym] || {})
|
67
|
+
@components << component.new(arguments)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
# Public: Serializes the Entity to save the current state.
|
72
|
+
#
|
73
|
+
# Returns a Hash representing the Entity.
|
74
|
+
def serialize
|
75
|
+
serialized = {id: id}
|
76
|
+
|
77
|
+
components.each do |component|
|
78
|
+
serialized[underscore(component.class.name.to_s).to_sym] = component.serialize
|
79
|
+
end
|
80
|
+
|
81
|
+
serialized
|
82
|
+
end
|
83
|
+
|
84
|
+
# Public: Returns a String representation of the Entity.
|
85
|
+
def inspect
|
86
|
+
serialize.to_s
|
87
|
+
end
|
88
|
+
|
89
|
+
# Public: Returns a String representation of the Entity.
|
90
|
+
def to_s
|
91
|
+
serialize.to_s
|
92
|
+
end
|
93
|
+
|
94
|
+
# Signature
|
95
|
+
#
|
96
|
+
# <underscored_component_name>
|
97
|
+
#
|
98
|
+
# underscored_component_name - The component to access the data from
|
99
|
+
#
|
100
|
+
# Public: Get the component associated with this Entity.
|
101
|
+
# This method will be available for each component.
|
102
|
+
#
|
103
|
+
# Examples
|
104
|
+
#
|
105
|
+
# class Creature < Draco::Entity
|
106
|
+
# component CreatureStats, strength: 10
|
107
|
+
# end
|
108
|
+
#
|
109
|
+
# creature = Creature.new
|
110
|
+
# creature.creature_stats
|
111
|
+
#
|
112
|
+
# Returns the Component instance.
|
113
|
+
|
114
|
+
def method_missing(m, *args, &block)
|
115
|
+
component = components.find { |c| underscore(c.class.name.to_s) == m.to_s }
|
116
|
+
return component if component
|
117
|
+
|
118
|
+
super
|
119
|
+
end
|
120
|
+
|
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
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
# Public: The data to associate with an Entity.
|
142
|
+
class Component
|
143
|
+
@attribute_options = {}
|
144
|
+
|
145
|
+
# Internal: Resets the attribute options for each class that inherits Component.
|
146
|
+
#
|
147
|
+
# sub - The class that is inheriting Entity.
|
148
|
+
#
|
149
|
+
# Returns nothing.
|
150
|
+
def self.inherited(sub)
|
151
|
+
sub.instance_variable_set(:@attribute_options, {})
|
152
|
+
end
|
153
|
+
|
154
|
+
# Public: Defines an attribute for the Component.
|
155
|
+
#
|
156
|
+
# name - The Symbol name of the attribute.
|
157
|
+
# options - The Hash options for the Component (default: {}):
|
158
|
+
# :default - The initial value for the attribute if one is not provided.
|
159
|
+
#
|
160
|
+
# Returns nothing.
|
161
|
+
def self.attribute(name, options = {})
|
162
|
+
attr_accessor name
|
163
|
+
@attribute_options[name] = options
|
164
|
+
end
|
165
|
+
|
166
|
+
# Internal: Returns the Hash attribute options for the current Class.
|
167
|
+
def self.attribute_options
|
168
|
+
@attribute_options
|
169
|
+
end
|
170
|
+
|
171
|
+
# Public: Initializes a new Component.
|
172
|
+
#
|
173
|
+
# values - The Hash of values to set for the Component instance (default: {}).
|
174
|
+
# Each key should be the Symbol name of the attribute.
|
175
|
+
#
|
176
|
+
# Examples
|
177
|
+
#
|
178
|
+
# class Position < Draco::Component
|
179
|
+
# attribute :x, default: 0
|
180
|
+
# attribute :y, default: 0
|
181
|
+
# end
|
182
|
+
#
|
183
|
+
# Position.new(x: 100, y: 100)
|
184
|
+
def initialize(values = {})
|
185
|
+
self.class.attribute_options.each do |name, options|
|
186
|
+
value = values.fetch(name.to_sym, options[:default])
|
187
|
+
instance_variable_set("@#{name}", value)
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
# Public: Serializes the Component to save the current state.
|
192
|
+
#
|
193
|
+
# Returns a Hash representing the Component.
|
194
|
+
def serialize
|
195
|
+
attrs = {}
|
196
|
+
|
197
|
+
instance_variables.each do |attr|
|
198
|
+
name = attr.to_s.gsub("@", "").to_sym
|
199
|
+
attrs[name] = instance_variable_get(attr)
|
200
|
+
end
|
201
|
+
|
202
|
+
attrs
|
203
|
+
end
|
204
|
+
|
205
|
+
# Public: Returns a String representation of the Component.
|
206
|
+
def inspect
|
207
|
+
serialize.to_s
|
208
|
+
end
|
209
|
+
|
210
|
+
# Public: Returns a String representation of the Component.
|
211
|
+
def to_s
|
212
|
+
serialize.to_s
|
213
|
+
end
|
214
|
+
end
|
215
|
+
|
216
|
+
# Public: Systems contain the logic of the game. The System runs on each tick and manipulates the Entities in the World.
|
217
|
+
class System
|
218
|
+
@filter = []
|
219
|
+
|
220
|
+
# Public: Returns an Array of Entities that match the filter.
|
221
|
+
attr_accessor :entities
|
222
|
+
|
223
|
+
# Public: Returns the World this System is running in.
|
224
|
+
attr_accessor :world
|
225
|
+
|
226
|
+
# Public: Adds the given Components to the default filter of the System.
|
227
|
+
#
|
228
|
+
# Returns the current filter.
|
229
|
+
def self.filter(*components)
|
230
|
+
components.each do |component|
|
231
|
+
@filter << component
|
232
|
+
end
|
233
|
+
|
234
|
+
@filter
|
235
|
+
end
|
236
|
+
|
237
|
+
# Internal: Resets the fuilter for each class that inherits System.
|
238
|
+
#
|
239
|
+
# sub - The class that is inheriting Entity.
|
240
|
+
#
|
241
|
+
# Returns nothing.
|
242
|
+
def self.inherited(sub)
|
243
|
+
sub.instance_variable_set(:@filter, [])
|
244
|
+
end
|
245
|
+
|
246
|
+
# Public: Initializes a new System.
|
247
|
+
#
|
248
|
+
# entities - The Entities to operate on (default: []).
|
249
|
+
# world - The World running the System (default: nil).
|
250
|
+
def initialize(entities: [], world: nil)
|
251
|
+
@entities = entities
|
252
|
+
@world = world
|
253
|
+
end
|
254
|
+
|
255
|
+
# Public: Runs the System logic for the current game engine tick.
|
256
|
+
#
|
257
|
+
# This is where the logic is implemented and it should be overriden for each System.
|
258
|
+
#
|
259
|
+
# context - The context object of the current tick from the game engine. In DragonRuby this is `args`.
|
260
|
+
#
|
261
|
+
# Returns nothing
|
262
|
+
def tick(context); end
|
263
|
+
|
264
|
+
# Public: Serializes the System to save the current state.
|
265
|
+
#
|
266
|
+
# Returns a Hash representing the System.
|
267
|
+
def serialize
|
268
|
+
{
|
269
|
+
entities: entities.map(&:serialize),
|
270
|
+
world: world ? world.serialize : nil
|
271
|
+
}
|
272
|
+
end
|
273
|
+
|
274
|
+
# Public: Returns a String representation of the System.
|
275
|
+
def inspect
|
276
|
+
serialize.to_s
|
277
|
+
end
|
278
|
+
|
279
|
+
# Public: Returns a String representation of the System.
|
280
|
+
def to_s
|
281
|
+
serialize.to_s
|
282
|
+
end
|
283
|
+
end
|
284
|
+
|
285
|
+
# Public: The container for current Entities and Systems.
|
286
|
+
class World
|
287
|
+
# Public: Returns the Array of Systems.
|
288
|
+
attr_reader :systems
|
289
|
+
|
290
|
+
# Public: Returns the Array of Entities.
|
291
|
+
attr_reader :entities
|
292
|
+
|
293
|
+
# Public: Initializes a World.
|
294
|
+
#
|
295
|
+
# entities - The Array of Entities for the World (default: []).
|
296
|
+
# systems - The Array of System Classes for the World (default: []).
|
297
|
+
def initialize(entities: [], systems: [])
|
298
|
+
@entities = entities
|
299
|
+
@systems = systems
|
300
|
+
end
|
301
|
+
|
302
|
+
# Public: Runs all of the Systems every tick.
|
303
|
+
#
|
304
|
+
# context - The context object of the current tick from the game engine. In DragonRuby this is `args`.
|
305
|
+
#
|
306
|
+
# Returns nothing
|
307
|
+
def tick(context)
|
308
|
+
systems.each do |system|
|
309
|
+
entities = filter(system.filter)
|
310
|
+
|
311
|
+
system.new(entities: entities, world: self).tick(context)
|
312
|
+
end
|
313
|
+
end
|
314
|
+
|
315
|
+
# Public: Finds all Entities that contain all of the given Components.
|
316
|
+
#
|
317
|
+
# components - An Array of Component classes to match.
|
318
|
+
#
|
319
|
+
# Returns an Array of matching Entities.
|
320
|
+
def filter(components)
|
321
|
+
entities.select { |e| (components - e.components.map(&:class)).empty? }
|
322
|
+
end
|
323
|
+
|
324
|
+
# Public: Serializes the World to save the current state.
|
325
|
+
#
|
326
|
+
# Returns a Hash representing the World.
|
327
|
+
def serialize
|
328
|
+
{
|
329
|
+
entities: @entities.map(&:serialize),
|
330
|
+
systems: @systems.map { |system| system.name.to_s }
|
331
|
+
}
|
332
|
+
end
|
333
|
+
|
334
|
+
# Public: Returns a String representation of the World.
|
335
|
+
def inspect
|
336
|
+
serialize.to_s
|
337
|
+
end
|
338
|
+
|
339
|
+
# Public: Returns a String representation of the World.
|
340
|
+
def to_s
|
341
|
+
serialize.to_s
|
342
|
+
end
|
343
|
+
end
|
344
|
+
end
|