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.
@@ -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