darkholme 0.9.1 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
[![Build Status](https://travis-ci.org/massivedanger/darkholme.png?branch=master)](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
|