draco 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.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
|