draco 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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).
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
@@ -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__)
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -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
@@ -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