darkholme 0.9.1 → 1.0.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/README.md +76 -4
- data/VERSION +1 -1
- data/darkholme.gemspec +3 -3
- data/lib/darkholme.rb +1 -1
- data/lib/darkholme/bitset.rb +82 -15
- data/lib/darkholme/component.rb +10 -1
- data/lib/darkholme/engine.rb +86 -6
- data/lib/darkholme/entity.rb +36 -2
- data/lib/darkholme/family.rb +14 -1
- data/lib/darkholme/iterating_system.rb +19 -0
- data/lib/darkholme/system.rb +31 -1
- data/spec/lib/darkholme/engine_spec.rb +26 -4
- data/spec/lib/darkholme/entity_spec.rb +20 -2
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e7887db34f894712a0b13a7b0efc0371af0983a4
|
4
|
+
data.tar.gz: 44a523114c61788c2c11d450df41e90179f3867d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ecdfaa52cc78ef4e681a84b4dc8a734a9a61c239171e75931fa7a5b756fa1a30626de6dc7274c94faba5215ad6836952ab2f9e87d05685ac01858b283b12ac3d
|
7
|
+
data.tar.gz: 4e83e7a54bdf7eeec2c35cc8d4c28d35087a26c6124899b509351c8db1bdb404f62a93954c11ea6537ec151b1fe977b16b163d8289525ab34fc45b7cc023b68e
|
data/README.md
CHANGED
@@ -2,14 +2,86 @@
|
|
2
2
|
|
3
3
|
[](https://travis-ci.org/massivedanger/darkholme)
|
4
4
|
|
5
|
-
> Who am I? That, my dear, is an excellent question. Though not one easily answered.
|
5
|
+
> Who am I? That, my dear, is an excellent question. Though not one easily answered.
|
6
6
|
|
7
|
-
Darkholme is an [entity-component system](http://en.wikipedia.org/wiki/Entity_component_system)
|
7
|
+
Darkholme is an [entity-component system](http://en.wikipedia.org/wiki/Entity_component_system)
|
8
8
|
written in Ruby. It's still early days for it, but I think it's ready for most basic use cases.
|
9
9
|
|
10
10
|
## Usage
|
11
11
|
|
12
|
-
|
12
|
+
First, you need to create an **Engine** to hold all the other parts of Darkholme.
|
13
|
+
|
14
|
+
```ruby
|
15
|
+
@engine = Darkholme::Engine.new
|
16
|
+
```
|
17
|
+
|
18
|
+
All of your **Systems** and **Entities** will be added to your engine, which will update
|
19
|
+
them once per frame. Make sure you put an Engine#update inside of your game's update loop, like
|
20
|
+
so:
|
21
|
+
|
22
|
+
```ruby
|
23
|
+
def update(delta)
|
24
|
+
@engine.update(delta)
|
25
|
+
end
|
26
|
+
```
|
27
|
+
|
28
|
+
Now, you can define an Entity and add **Components** to it, which hold data for your systems
|
29
|
+
to use. In this case, let's assume you've made a component called `Spatial` that holds an entity's
|
30
|
+
position along with its velocity. Something like this:
|
31
|
+
|
32
|
+
```ruby
|
33
|
+
class Spatial < Darkholme::Component
|
34
|
+
attr_accessor :position, :velocity
|
35
|
+
|
36
|
+
def initialize
|
37
|
+
@position = { x: 0, y: 0 }
|
38
|
+
@velocity = { x: 0, y: 0 }
|
39
|
+
end
|
40
|
+
end
|
41
|
+
```
|
42
|
+
|
43
|
+
And adding it:
|
44
|
+
|
45
|
+
```ruby
|
46
|
+
new_entity = Darkhole::Entity.new
|
47
|
+
new_entity.add_component Spatial.new
|
48
|
+
```
|
49
|
+
|
50
|
+
Next is adding your entity to the engine and adding a new system that'll use the Spatial
|
51
|
+
component and move the entity according to its velocity.
|
52
|
+
|
53
|
+
Here's what the system could look like:
|
54
|
+
|
55
|
+
```ruby
|
56
|
+
class PositionSystem < Darkholme::System
|
57
|
+
has_family Spatial
|
58
|
+
|
59
|
+
def update(delta)
|
60
|
+
entities.each do |entity|
|
61
|
+
spatial = entity.component_for Spatial
|
62
|
+
|
63
|
+
spatial.position.x += spatial.velocity.x * delta
|
64
|
+
spatial.position.y += spatial.velocity.y * delta
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
```
|
69
|
+
|
70
|
+
And adding it and the entity from before:
|
71
|
+
|
72
|
+
```ruby
|
73
|
+
engine.add_system PositionSystem.new
|
74
|
+
engine.add_entity new_entity
|
75
|
+
```
|
76
|
+
|
77
|
+
Now, as another system modifies the entity's Spatial component velocity, the
|
78
|
+
PositionSystem will position it according to its velocity, but properly using
|
79
|
+
the delta between frames for smooth movement.
|
80
|
+
|
81
|
+
It's highly encouraged to [read through the documentation](http://rdoc.info/github/massivedanger/darkholme/master/frames)
|
82
|
+
for the gem, as it can answer most questions you might have about each individual method. Please
|
83
|
+
[file an issue](https://github.com/massivedanger/darkholme/issues) if you think our documentation is
|
84
|
+
lacking in some way. Thanks!
|
13
85
|
|
14
86
|
## Key concepts
|
15
87
|
|
@@ -40,7 +112,7 @@ means that systems won't loop through every single entity on update. They only l
|
|
40
112
|
relevant entities. This is _awesome_. Maybe.
|
41
113
|
|
42
114
|
## Contributing to darkholme
|
43
|
-
|
115
|
+
|
44
116
|
- Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet.
|
45
117
|
- Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it.
|
46
118
|
- Fork the project.
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.
|
1
|
+
1.0.0
|
data/darkholme.gemspec
CHANGED
@@ -2,15 +2,15 @@
|
|
2
2
|
# DO NOT EDIT THIS FILE DIRECTLY
|
3
3
|
# Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
|
4
4
|
# -*- encoding: utf-8 -*-
|
5
|
-
# stub: darkholme 0.
|
5
|
+
# stub: darkholme 1.0.0 ruby lib
|
6
6
|
|
7
7
|
Gem::Specification.new do |s|
|
8
8
|
s.name = "darkholme"
|
9
|
-
s.version = "0.
|
9
|
+
s.version = "1.0.0"
|
10
10
|
|
11
11
|
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
12
12
|
s.authors = ["Massive Danger"]
|
13
|
-
s.date = "2014-02-
|
13
|
+
s.date = "2014-02-25"
|
14
14
|
s.description = "An entity-component system in Ruby"
|
15
15
|
s.email = "evan@massivedanger.com"
|
16
16
|
s.extra_rdoc_files = [
|
data/lib/darkholme.rb
CHANGED
data/lib/darkholme/bitset.rb
CHANGED
@@ -1,7 +1,17 @@
|
|
1
1
|
module Darkholme
|
2
|
+
# A class for easier bitset management with handy bitwise methods
|
2
3
|
class Bitset
|
3
|
-
attr_reader :bits
|
4
|
-
|
4
|
+
attr_reader :bits
|
5
|
+
attr_reader :set_indexes
|
6
|
+
|
7
|
+
# Create a new Bitset with optional initial bits
|
8
|
+
#
|
9
|
+
# @param initial_bits [Array<Fixnum>] The initial bit indexes to set
|
10
|
+
#
|
11
|
+
# @example Setting multiple bits initially
|
12
|
+
# Bitset.new 1, 2, 10, 30
|
13
|
+
#
|
14
|
+
# @return [Bitset] The new bitset
|
5
15
|
def initialize(*initial_bits)
|
6
16
|
@bits = initial_bits.map {|bit| convert_bit(bit) }.inject(:+) || 0
|
7
17
|
|
@@ -9,6 +19,13 @@ module Darkholme
|
|
9
19
|
@set_indexes += initial_bits.uniq if initial_bits
|
10
20
|
end
|
11
21
|
|
22
|
+
# Set the bit at an index to true or false.
|
23
|
+
# Sets the bit to 1, by default
|
24
|
+
#
|
25
|
+
# @param bit [Fixnum] Index of bitset to set
|
26
|
+
# @param value [Boolean] Whether to set the bit to 0 or 1
|
27
|
+
#
|
28
|
+
# @return [Fixnum] The bitset's integer representation after being modified
|
12
29
|
def set(bit, value = true)
|
13
30
|
if value
|
14
31
|
@bits = union convert_bit(bit)
|
@@ -21,67 +38,117 @@ module Darkholme
|
|
21
38
|
@bits
|
22
39
|
end
|
23
40
|
|
41
|
+
# Set a bit at supplied index to 0
|
42
|
+
#
|
43
|
+
# @param bit [Fixnum] The index of the bit to modify
|
44
|
+
#
|
45
|
+
# @return [Fixnum] The bitset's integer representation after being modified
|
24
46
|
def clear(bit)
|
25
|
-
set(bit, false)
|
47
|
+
set(bit, false)
|
26
48
|
end
|
27
49
|
|
50
|
+
# Checks if a bit at an index is set to 1
|
51
|
+
#
|
52
|
+
# @param bit [Fixnum] Index of bit to check
|
53
|
+
#
|
54
|
+
# @return [Boolean] Whether or not the bit is set to one
|
28
55
|
def set?(bit)
|
29
|
-
bit = convert_bit(bit)
|
56
|
+
bit = convert_bit(bit)
|
30
57
|
intersect(bit) == bit
|
31
58
|
end
|
32
59
|
|
60
|
+
# Checks if all of another bitset's flipped bits are flipped in this one
|
61
|
+
#
|
62
|
+
# @param other [Bitset] Bitset use for inclusion check
|
63
|
+
#
|
64
|
+
# @return [Boolean] Whether or not all bits are flipped in both
|
33
65
|
def include?(other)
|
34
66
|
set_indexes & other.set_indexes == other.set_indexes
|
35
67
|
end
|
36
68
|
|
69
|
+
# Perform a bitwise AND
|
70
|
+
#
|
71
|
+
# @param other [Bitset or Fixnum] Other object to use for AND
|
72
|
+
#
|
73
|
+
# @return [Fixnum] Bits after operation
|
37
74
|
def intersect(other)
|
38
|
-
@bits & bits_from_object(other)
|
75
|
+
@bits & bits_from_object(other)
|
39
76
|
end
|
40
77
|
alias_method :&, :intersect
|
41
78
|
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
else
|
46
|
-
false
|
47
|
-
end
|
48
|
-
end
|
49
|
-
|
79
|
+
# Perform a bitwise NOT on the current bitset
|
80
|
+
#
|
81
|
+
# @return [Fixnum] Bits after operation
|
50
82
|
def not
|
51
83
|
~@bits
|
52
84
|
end
|
53
85
|
alias_method :~, :not
|
54
86
|
|
87
|
+
# Perform a bitwise XOR
|
88
|
+
#
|
89
|
+
# @param other [Bitset or Fixnum] Other object to use for XOR
|
90
|
+
#
|
91
|
+
# @return [Fixnum] Bits after operation
|
55
92
|
def xor(other)
|
56
|
-
@bits ^ bits_from_object(other)
|
93
|
+
@bits ^ bits_from_object(other)
|
57
94
|
end
|
58
95
|
alias_method :^, :xor
|
59
96
|
|
97
|
+
# Perform a bitwise OR
|
98
|
+
#
|
99
|
+
# @param other [Bitset or Fixnum] Other object to use for OR
|
100
|
+
#
|
101
|
+
# @return [Fixnum] Bits after operation
|
60
102
|
def union(other)
|
61
103
|
@bits | bits_from_object(other)
|
62
104
|
end
|
63
105
|
alias_method :|, :union
|
64
106
|
|
107
|
+
# Return the binary representation of the bitset
|
108
|
+
#
|
109
|
+
# @return [String] Binary of current bits
|
65
110
|
def to_s
|
66
|
-
@bits.to_s(2)
|
111
|
+
@bits.to_s(2)
|
67
112
|
end
|
68
113
|
|
114
|
+
# Return Integer representation of the bitset
|
115
|
+
#
|
116
|
+
# @return [Fixnum] Integer of current bits
|
69
117
|
def to_i
|
70
118
|
@bits.to_i
|
71
119
|
end
|
72
120
|
|
121
|
+
# Return Float representation of the bitset
|
122
|
+
#
|
123
|
+
# @return [Float] Float of current bits
|
73
124
|
def to_f
|
74
125
|
@bits.to_f
|
75
126
|
end
|
76
127
|
|
128
|
+
# Check for a bit at an index to be set to 1
|
129
|
+
#
|
130
|
+
# @param index [Fixnum] Index of bit to check
|
131
|
+
#
|
132
|
+
# @return [Boolean] Whether or not the bit is set to 1
|
77
133
|
def [](index)
|
78
134
|
set?(index)
|
79
135
|
end
|
80
136
|
|
137
|
+
# Set a bit at an index to 0 or 1
|
138
|
+
#
|
139
|
+
# @param index [Fixnum] Index of bitset to set
|
140
|
+
# @param value [Boolean] Whether to set the bit to 0 or 1
|
141
|
+
#
|
142
|
+
# @return [Fixnum] The bitset's integer representation after being modified
|
81
143
|
def []=(index, value)
|
82
144
|
set(index, value)
|
83
145
|
end
|
84
146
|
|
147
|
+
# Check if other bitset's bits match the current ones
|
148
|
+
#
|
149
|
+
# @param other [Bitset] Other bitset
|
150
|
+
#
|
151
|
+
# @return [Boolean] Whether or not the bits match
|
85
152
|
def ==(other)
|
86
153
|
@bits == other.bits
|
87
154
|
end
|
data/lib/darkholme/component.rb
CHANGED
@@ -1,14 +1,23 @@
|
|
1
1
|
module Darkholme
|
2
|
+
# Top-level class for storing data on an Entity that a System uses
|
2
3
|
class Component
|
3
4
|
@next_bit = 0
|
4
5
|
@bits = {}
|
5
6
|
|
7
|
+
# Get a unique bit for a Component class
|
8
|
+
#
|
9
|
+
# @param klass [Class] The class to generate (or look up) the bit for
|
10
|
+
#
|
11
|
+
# @return [Fixnum] The bit for that class
|
6
12
|
def self.bit_for(klass)
|
7
13
|
@bits[klass] ||= @next_bit += 1
|
8
14
|
end
|
9
15
|
|
16
|
+
# Get the current instance's class' bit
|
17
|
+
#
|
18
|
+
# @return [Fixnum] The bit for the instance's class
|
10
19
|
def bit
|
11
|
-
Component.bit_for(self.class)
|
20
|
+
Component.bit_for(self.class)
|
12
21
|
end
|
13
22
|
end
|
14
23
|
end
|
data/lib/darkholme/engine.rb
CHANGED
@@ -1,39 +1,87 @@
|
|
1
1
|
module Darkholme
|
2
|
+
# An Engine contains all of a game's entities, components, and systems.
|
3
|
+
# Usually only one is needed for your entire game, although no limitation
|
4
|
+
# is hardcoded. Use at your discretion.
|
2
5
|
class Engine
|
3
|
-
attr_reader :systems, :entities
|
6
|
+
attr_reader :systems, :entities, :families
|
4
7
|
|
8
|
+
# Create a new engine with empty systems and entities
|
9
|
+
#
|
10
|
+
# @return [Engine] The new Engine
|
5
11
|
def initialize
|
6
12
|
@systems = {}
|
7
13
|
@families = {}
|
8
14
|
@entities = Set.new
|
9
15
|
end
|
10
16
|
|
17
|
+
# Add an entity to the engine (with callbacks). Once added,
|
18
|
+
# the entity is available for use by systems during each
|
19
|
+
# update loop.
|
20
|
+
#
|
21
|
+
# @param entity [Entity] The entity to add
|
22
|
+
#
|
23
|
+
# @return Result of entity#added_to_engine
|
11
24
|
def add_entity(entity)
|
12
25
|
@entities << entity
|
13
26
|
entity.added_to_engine self
|
14
27
|
end
|
15
28
|
|
29
|
+
# Remove an entity from the engine (with callbacks). Once removed,
|
30
|
+
# the entity will no longer be updated during the update loop
|
31
|
+
#
|
32
|
+
# @param entity [Entity] The entity to remove
|
33
|
+
#
|
34
|
+
# @return Result of entity#removed_from_engine
|
16
35
|
def remove_entity(entity)
|
17
36
|
@entities.delete entity
|
18
37
|
entity.removed_from_engine self
|
19
38
|
end
|
20
39
|
|
40
|
+
# Add a system to be updated each frame
|
41
|
+
#
|
42
|
+
# @param system [System] The system to add
|
43
|
+
#
|
44
|
+
# @return Result of system#added_to_engine
|
21
45
|
def add_system(system)
|
22
46
|
@systems[system.class] = system
|
23
47
|
system.added_to_engine self
|
24
48
|
end
|
25
49
|
|
26
|
-
|
27
|
-
|
50
|
+
# Remove a system from the engine. It will no longer be updated
|
51
|
+
# each frame
|
52
|
+
#
|
53
|
+
# @param system_class [Class] Class of System to remove
|
54
|
+
#
|
55
|
+
# @return Result of system#removed_from_engine
|
56
|
+
def remove_system(system_class)
|
57
|
+
system = @systems.delete system_class
|
28
58
|
system.removed_from_engine(self) if system
|
29
59
|
end
|
30
60
|
|
31
|
-
|
32
|
-
|
61
|
+
# Retrieve a system added to the engine by its class
|
62
|
+
#
|
63
|
+
# @param system_class [Class] Class of the system to get
|
64
|
+
#
|
65
|
+
# @example Standard usage
|
66
|
+
# engine.system_for(MySystem)
|
67
|
+
#
|
68
|
+
# @return [System] A System instance, if found
|
69
|
+
def system_for(system_class)
|
70
|
+
@systems[system_class]
|
33
71
|
end
|
34
72
|
|
73
|
+
# Retrieve all entities matching Family provided
|
74
|
+
#
|
75
|
+
# @param family [Family] Family to use when checking entities
|
76
|
+
#
|
77
|
+
# @example Standard usage
|
78
|
+
# engine.entities_for_family(
|
79
|
+
# Family.for(MyComponent, AnotherComponent)
|
80
|
+
# )
|
81
|
+
#
|
82
|
+
# @return [Array<Entity>] All entities matching provided Family
|
35
83
|
def entities_for_family(family)
|
36
|
-
@families[family
|
84
|
+
@families[family] ||= begin
|
37
85
|
Set.new.tap do |entities|
|
38
86
|
@entities.each do |entity|
|
39
87
|
entities << entity if family.member?(entity)
|
@@ -42,9 +90,41 @@ module Darkholme
|
|
42
90
|
end
|
43
91
|
end
|
44
92
|
|
93
|
+
# Update all systems within engine. This should called once per
|
94
|
+
# frame in game.
|
95
|
+
#
|
96
|
+
# @param delta [Float] Time between last frame and this one
|
97
|
+
#
|
98
|
+
# @example Standard usage
|
99
|
+
# engine.update(0.0008)
|
45
100
|
def update(delta)
|
46
101
|
@systems.each {|klass, system| system.update(delta) }
|
47
102
|
end
|
48
103
|
|
104
|
+
# A callback called every time an entity added to the engine
|
105
|
+
# has a component added. Updates all associated families
|
106
|
+
def component_added(entity, component)
|
107
|
+
@families.each do |family, entities|
|
108
|
+
unless entity.family_bits.set?(family.index)
|
109
|
+
if family.member?(entity)
|
110
|
+
entities << entity
|
111
|
+
entity.family_bits.set(family.index)
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
# A callback called every time an entity added to the engine
|
118
|
+
# has a component removed. Updates all associated families
|
119
|
+
def component_removed(entity, component)
|
120
|
+
@families.each do |family, entities|
|
121
|
+
if entity.family_bits.set?(family.index)
|
122
|
+
unless family.member?(entity)
|
123
|
+
entities.delete(entity)
|
124
|
+
entity.family_bits.clear(family.index)
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
49
129
|
end
|
50
130
|
end
|
data/lib/darkholme/entity.rb
CHANGED
@@ -1,41 +1,75 @@
|
|
1
1
|
module Darkholme
|
2
|
+
# An Entity contains Components, which hold data which is manipulated
|
3
|
+
# by a System
|
2
4
|
class Entity
|
3
|
-
attr_accessor :engine, :components, :component_bits
|
5
|
+
attr_accessor :engine, :components, :component_bits, :family_bits
|
4
6
|
|
7
|
+
# Create a new Entity
|
8
|
+
#
|
9
|
+
# @return [Entity] The new Entity
|
5
10
|
def initialize
|
6
11
|
self.components = {}
|
7
12
|
self.component_bits = Bitset.new
|
13
|
+
self.family_bits = Bitset.new
|
8
14
|
self.engine = nil
|
9
15
|
end
|
10
16
|
|
17
|
+
# Callback called after the entity has been added to an Engine
|
18
|
+
#
|
19
|
+
# @param engine [Engine] The engine the system was added to
|
11
20
|
def added_to_engine(engine)
|
12
21
|
self.engine = engine
|
13
22
|
end
|
14
23
|
|
24
|
+
# Callback called after the engine has been removed from an Engine
|
25
|
+
#
|
26
|
+
# @param engine [Engine] The engine the entity was removed from
|
15
27
|
def removed_from_engine(engine)
|
16
28
|
self.engine = nil
|
17
29
|
end
|
18
30
|
|
31
|
+
# Add a component to an entity
|
32
|
+
#
|
33
|
+
# @param component [Component] The component being added
|
34
|
+
#
|
35
|
+
# @return [Component] The component that was added
|
19
36
|
def add_component(component)
|
20
37
|
self.components[component.class] = component
|
21
38
|
self.component_bits.set(component.bit)
|
39
|
+
self.engine.component_added(self, component) if self.engine
|
22
40
|
|
23
|
-
component
|
41
|
+
component
|
24
42
|
end
|
25
43
|
|
44
|
+
# Remove a component from an entity
|
45
|
+
#
|
46
|
+
# @param component_class [Class] The class of the component being removed
|
47
|
+
#
|
48
|
+
# @return [Component] The component that was removed
|
26
49
|
def remove_component(component_class)
|
27
50
|
if removed_component = component_for(component_class)
|
28
51
|
self.component_bits.clear(removed_component.bit)
|
29
52
|
self.components.delete(component_class)
|
53
|
+
self.engine.component_removed(self, removed_component) if self.engine
|
30
54
|
end
|
31
55
|
|
32
56
|
removed_component
|
33
57
|
end
|
34
58
|
|
59
|
+
# Check to see if an entity contains a type of component
|
60
|
+
#
|
61
|
+
# @param component_class [Class] The class of the component to check for
|
62
|
+
#
|
63
|
+
# @return [Boolean] Whether or not the entity has the component
|
35
64
|
def has_component?(component_class)
|
36
65
|
self.components.include? component_class
|
37
66
|
end
|
38
67
|
|
68
|
+
# Retrieve a component of a certain class from an entity
|
69
|
+
#
|
70
|
+
# @param component_class [Class] The class of the component to get
|
71
|
+
#
|
72
|
+
# @return [Component] The component, if present
|
39
73
|
def component_for(component_class)
|
40
74
|
self.components[component_class]
|
41
75
|
end
|
data/lib/darkholme/family.rb
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
module Darkholme
|
2
|
-
|
2
|
+
# Used by systems and engines to get related entities for processing
|
3
|
+
class Family
|
3
4
|
@next_index = 0
|
4
5
|
@families = {}
|
5
6
|
|
@@ -9,6 +10,13 @@ module Darkholme
|
|
9
10
|
|
10
11
|
attr_reader :index, :bits
|
11
12
|
|
13
|
+
# Get a family for matching entities with a specific
|
14
|
+
# list of components present
|
15
|
+
#
|
16
|
+
# @param component_classes [Array<Class>] List of Component classes to
|
17
|
+
# check for
|
18
|
+
#
|
19
|
+
# @return [Family] The Family instance for that list of Components
|
12
20
|
def self.for(*component_classes)
|
13
21
|
bits = Bitset.new
|
14
22
|
component_classes.each do |klass|
|
@@ -21,6 +29,11 @@ module Darkholme
|
|
21
29
|
end
|
22
30
|
end
|
23
31
|
|
32
|
+
# Check if an Entity has all the required Components for the Family
|
33
|
+
#
|
34
|
+
# @param entity [Entity] The entity to check for membership
|
35
|
+
#
|
36
|
+
# @return [Boolean] Whether or not the entity is a member of the Family
|
24
37
|
def member?(entity)
|
25
38
|
entity.component_bits.include?(bits)
|
26
39
|
end
|
@@ -1,5 +1,15 @@
|
|
1
1
|
module Darkholme
|
2
|
+
# A subclass of System, IteratingSystem automatically loops through entities
|
3
|
+
# belonging to it and calls #process on them, along with a before and after
|
4
|
+
# callback
|
2
5
|
class IteratingSystem < System
|
6
|
+
# Called by the engine, this calls #before_processing, then #process on
|
7
|
+
# each entity, then #after_processing
|
8
|
+
#
|
9
|
+
# @param delta [Float] The difference in time between the last frame
|
10
|
+
# and this one
|
11
|
+
#
|
12
|
+
# @return The result of after_processing
|
3
13
|
def update(delta)
|
4
14
|
before_processing
|
5
15
|
entities.each do |entity|
|
@@ -8,13 +18,22 @@ module Darkholme
|
|
8
18
|
after_processing
|
9
19
|
end
|
10
20
|
|
21
|
+
# Called on each entity in the collection. This where the various
|
22
|
+
# components' data will be manipulated. This must be overridden by
|
23
|
+
# the subclass.
|
24
|
+
#
|
25
|
+
# @param entity [Entity] The entity being manipulated
|
26
|
+
# @param delta [Float] The difference in time between the last frame
|
27
|
+
# and this one
|
11
28
|
def process(entity, delta)
|
12
29
|
raise NotImplementedError.new("You must override #process(entity, delta)")
|
13
30
|
end
|
14
31
|
|
32
|
+
# Called once a frame before the entities are looped over and processed
|
15
33
|
def before_processing
|
16
34
|
end
|
17
35
|
|
36
|
+
# Called once a frame after the entities are looped over and processed
|
18
37
|
def after_processing
|
19
38
|
end
|
20
39
|
end
|
data/lib/darkholme/system.rb
CHANGED
@@ -1,31 +1,61 @@
|
|
1
1
|
module Darkholme
|
2
|
+
# Updated every frame by the engine, a System manipulates and uses the
|
3
|
+
# data contained within a component
|
2
4
|
class System
|
3
5
|
class << self
|
4
6
|
attr_reader :family
|
5
7
|
|
8
|
+
# Set the family for a System. Call this from the body of the
|
9
|
+
# class.
|
10
|
+
#
|
11
|
+
# @param component_classes [Array<Class>] List of component classes the system is
|
12
|
+
#
|
13
|
+
# @example Standard usage
|
14
|
+
# class MySystem < Darkholme::System
|
15
|
+
# has_family MyComponent, AnotherComponent, LastComponent
|
16
|
+
# end
|
17
|
+
#
|
18
|
+
# @return [Family] The family for the classes
|
6
19
|
def has_family(*component_classes)
|
7
|
-
@family = Family.for(*component_classes)
|
20
|
+
@family = Family.for(*component_classes)
|
8
21
|
end
|
9
22
|
end
|
10
23
|
|
24
|
+
# @!attribute engine [Engine] The engine this system belongs to
|
11
25
|
attr_accessor :engine
|
12
26
|
|
27
|
+
# Called once per frame by the engine. This must be overriden
|
28
|
+
# by the subclass.
|
29
|
+
#
|
30
|
+
# @param delta [Float] Difference in time between the last frame and this one
|
13
31
|
def update(delta)
|
14
32
|
raise NotImplementedError.new("You must override #update(delta)")
|
15
33
|
end
|
16
34
|
|
35
|
+
# Callback called after the system has been added to an Engine
|
36
|
+
#
|
37
|
+
# @param engine [Engine] The engine the system was added to
|
17
38
|
def added_to_engine(engine)
|
18
39
|
self.engine = engine
|
19
40
|
end
|
20
41
|
|
42
|
+
# Callback called after the system has been removed from an Engine
|
43
|
+
#
|
44
|
+
# @param engine [Engine] The engine the system was removed from
|
21
45
|
def removed_from_engine(engine)
|
22
46
|
self.engine = nil if self.engine == engine
|
23
47
|
end
|
24
48
|
|
49
|
+
# An Array of all the entities this system is interested in
|
50
|
+
#
|
51
|
+
# @return [Array<Entity>] All the entities with matching components
|
25
52
|
def entities
|
26
53
|
engine.entities_for_family(family)
|
27
54
|
end
|
28
55
|
|
56
|
+
# Get the system's Family instance
|
57
|
+
#
|
58
|
+
# @return [Family] The system's family, as defined by #has_family
|
29
59
|
def family
|
30
60
|
self.class.family
|
31
61
|
end
|
@@ -4,23 +4,23 @@ module Darkholme
|
|
4
4
|
describe Engine do
|
5
5
|
subject { Engine.new }
|
6
6
|
let(:entity) { MockEntity.new }
|
7
|
-
let(:system) { MockSystem.new }
|
7
|
+
let(:system) { MockSystem.new }
|
8
8
|
|
9
9
|
describe "with entities" do
|
10
10
|
it "can add them" do
|
11
11
|
expect {
|
12
12
|
subject.add_entity(entity)
|
13
|
-
}.to change { subject.entities.count }.from(0).to(1)
|
13
|
+
}.to change { subject.entities.count }.from(0).to(1)
|
14
14
|
end
|
15
15
|
|
16
16
|
it "can remove them" do
|
17
17
|
subject.add_entity(entity)
|
18
18
|
|
19
|
-
expect {
|
19
|
+
expect {
|
20
20
|
subject.remove_entity(entity)
|
21
21
|
}.to change { subject.entities.count }.from(1).to(0)
|
22
22
|
end
|
23
|
-
|
23
|
+
|
24
24
|
it "can find them by family" do
|
25
25
|
entity.add_component(MockComponent.new)
|
26
26
|
subject.add_entity(entity)
|
@@ -53,6 +53,28 @@ module Darkholme
|
|
53
53
|
subject.update(:delta)
|
54
54
|
end
|
55
55
|
end
|
56
|
+
|
57
|
+
describe "with callbacks" do
|
58
|
+
let(:family) { Family.for(MockComponent) }
|
59
|
+
it "updates families upon component addition" do
|
60
|
+
subject.families[family] = []
|
61
|
+
entity.engine = subject
|
62
|
+
|
63
|
+
expect(subject.families[family]).not_to include(entity)
|
64
|
+
entity.add_component MockComponent.new
|
65
|
+
expect(subject.families[family]).to include(entity)
|
66
|
+
end
|
67
|
+
|
68
|
+
it "updates families upon component removal" do
|
69
|
+
subject.families[family] = [entity]
|
70
|
+
entity.engine = subject
|
71
|
+
entity.add_component MockComponent.new
|
72
|
+
|
73
|
+
expect(subject.families[family]).to include(entity)
|
74
|
+
entity.remove_component MockComponent
|
75
|
+
expect(subject.families[family]).not_to include(entity)
|
76
|
+
end
|
77
|
+
end
|
56
78
|
end
|
57
79
|
end
|
58
80
|
|
@@ -41,8 +41,17 @@ module Darkholme
|
|
41
41
|
|
42
42
|
subject.add_component(component)
|
43
43
|
end
|
44
|
+
|
45
|
+
it "notifies the engine" do
|
46
|
+
subject.engine = Engine.new
|
47
|
+
expect(subject.engine).to receive(:component_added).with(
|
48
|
+
subject, component
|
49
|
+
)
|
50
|
+
|
51
|
+
subject.add_component(component)
|
52
|
+
end
|
44
53
|
end
|
45
|
-
|
54
|
+
|
46
55
|
describe "removing one" do
|
47
56
|
let!(:added) { subject.add_component(component) }
|
48
57
|
|
@@ -62,8 +71,17 @@ module Darkholme
|
|
62
71
|
|
63
72
|
subject.remove_component(component.class)
|
64
73
|
end
|
74
|
+
|
75
|
+
it "notifies the engine" do
|
76
|
+
subject.engine = Engine.new
|
77
|
+
expect(subject.engine).to receive(:component_removed).with(
|
78
|
+
subject, component
|
79
|
+
)
|
80
|
+
|
81
|
+
subject.remove_component(component.class)
|
82
|
+
end
|
65
83
|
end
|
66
|
-
|
84
|
+
|
67
85
|
it "returns a component instance when asked for one" do
|
68
86
|
subject.add_component(component)
|
69
87
|
expect(subject.component_for(component.class)).to be_a MockComponent
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: darkholme
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 1.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Massive Danger
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2014-02-
|
11
|
+
date: 2014-02-25 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: pry
|