peregrine 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +19 -0
- data/README.md +36 -0
- data/lib/peregrine/collections/common.rb +12 -0
- data/lib/peregrine/collections/composite.rb +33 -0
- data/lib/peregrine/collections/named.rb +34 -0
- data/lib/peregrine/collections/systemic.rb +30 -0
- data/lib/peregrine/collections/tagged.rb +38 -0
- data/lib/peregrine/collections.rb +25 -0
- data/lib/peregrine/component.rb +59 -0
- data/lib/peregrine/entity.rb +130 -0
- data/lib/peregrine/entity_manager.rb +164 -0
- data/lib/peregrine/features/configurable.rb +20 -0
- data/lib/peregrine/features/identifiable.rb +12 -0
- data/lib/peregrine/features/nameable.rb +17 -0
- data/lib/peregrine/features/taggable.rb +32 -0
- data/lib/peregrine/features.rb +20 -0
- data/lib/peregrine/system.rb +129 -0
- data/lib/peregrine/version.rb +4 -0
- data/lib/peregrine.rb +6 -0
- metadata +114 -0
data/LICENSE
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
Copyright (c) 2014 Kevin Owen <solucet@icloud.com>
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
of this software and associated documentation files (the "Software"), to deal
|
5
|
+
in the Software without restriction, including without limitation the rights
|
6
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
7
|
+
copies of the Software, and to permit persons to whom the Software is
|
8
|
+
furnished to do so, subject to the following conditions:
|
9
|
+
|
10
|
+
The above copyright notice and this permission notice shall be included in
|
11
|
+
all copies or substantial portions of the Software.
|
12
|
+
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
16
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
# Peregrine
|
2
|
+
1. _adjective: foreign; alien; wandering, traveling, or migrating._
|
3
|
+
2. _noun: a kick-ass falcon._
|
4
|
+
|
5
|
+
## Summary
|
6
|
+
Peregrine is a highly adaptive, extensible Entity-Component framework written for the Ruby programming language. Peregrine is platform and dependency agnostic, requiring _no_ additional dependencies for use as a library -- and works just as well with JRuby or Rubinius as it does with MRI Ruby.
|
7
|
+
|
8
|
+
## Installation
|
9
|
+
Peregrine is available as a library in the usual way -- just use RubyGems.
|
10
|
+
|
11
|
+
```sh
|
12
|
+
$ gem install peregrine
|
13
|
+
```
|
14
|
+
|
15
|
+
Want to help with development (or hack at the source)? Just clone [the GitHub repository][peregrine], use Bundler, and you're good to go.
|
16
|
+
|
17
|
+
```sh
|
18
|
+
$ git clone git@github.com:solucet/peregrine.git
|
19
|
+
$ gem install bundler
|
20
|
+
$ cd peregrine/ && bundle
|
21
|
+
```
|
22
|
+
|
23
|
+
## Documentation
|
24
|
+
The framework has been heavily documented with information and should serve you well. If you installed Peregrine from the GitHub repository and installed the development dependencies, you can generate a local copy of its RDoc documentation by running `rake rdoc`.
|
25
|
+
|
26
|
+
If you are unfamiliar with Entity-Component design, it is highly recommended that you read about it in addition to reading the documentation for the Peregrine framework. Chris Powell wrote a [great series of tutorials][recf-tutorial] on using EC design in JRuby on his blog which also includes links to further reading on the subject.
|
27
|
+
|
28
|
+
## Thanks
|
29
|
+
Special thanks go to Chris Powell, as the Peregrine framework was conceptually inspired by his [Ruby Entity-Component Framework][recf] and its related tutorials (which are definitely recommended reading).
|
30
|
+
|
31
|
+
## License
|
32
|
+
Peregrine is made available under the terms of the MIT License. See the included LICENSE file for more information.
|
33
|
+
|
34
|
+
[peregrine]: https://github.com/solucet/peregrine
|
35
|
+
[recf]: https://github.com/cpowell/ruby-entity-component-framework
|
36
|
+
[recf-tutorial]: http://cbpowell.wordpress.com/2012/10/30/
|
@@ -0,0 +1,12 @@
|
|
1
|
+
require 'peregrine/collections/named'
|
2
|
+
require 'peregrine/collections/tagged'
|
3
|
+
|
4
|
+
module Peregrine
|
5
|
+
module Collections
|
6
|
+
# Includes instance method definitions available in the Collections::Named
|
7
|
+
# and Collections::Tagged modules.
|
8
|
+
module Common
|
9
|
+
include Named, Tagged
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module Peregrine
|
2
|
+
module Collections
|
3
|
+
# Provides methods for filtering Entity objects contained in a collection.
|
4
|
+
# This module is intended to be an extension to existing collection
|
5
|
+
# instances.
|
6
|
+
module Composite
|
7
|
+
# Returns an array of all Entities in the collection which include all of
|
8
|
+
# the given Component classes. Yields the matching Entity instances if a
|
9
|
+
# block is given.
|
10
|
+
def with_components(*list)
|
11
|
+
entities = select do |item|
|
12
|
+
next unless item.respond_to?(:component_classes)
|
13
|
+
list.all? { |component| item.component_classes.include?(component) }
|
14
|
+
end
|
15
|
+
entities.each { |entity| yield entity } if block_given?
|
16
|
+
entities.extend(Collections)
|
17
|
+
end
|
18
|
+
alias :with_component :with_components
|
19
|
+
|
20
|
+
# Returns an array of all Entities in the collection which include _any_
|
21
|
+
# of the given Component classes. Yields the matching Entity instances if
|
22
|
+
# a block is given.
|
23
|
+
def with_any_component(*list)
|
24
|
+
entities = select do |item|
|
25
|
+
next unless item.respond_to?(:component_classes)
|
26
|
+
!(item.component_classes & list).empty?
|
27
|
+
end
|
28
|
+
entities.each { |entity| yield entity } if block_given?
|
29
|
+
entities.extend(Collections)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module Peregrine
|
2
|
+
module Collections
|
3
|
+
# Provides methods for filtering collections by item names. This module is
|
4
|
+
# intended to be an extension to existing collection instances.
|
5
|
+
module Named
|
6
|
+
# Returns an array of all items with a name that matches the given
|
7
|
+
# matcher. The matcher may be a Regexp for fine-tuned matching or any
|
8
|
+
# other object for specific equality matching. Yields the matching items
|
9
|
+
# in the collection if a block is given.
|
10
|
+
#
|
11
|
+
# ==== Examples
|
12
|
+
#
|
13
|
+
# Using a regular expression as a matcher:
|
14
|
+
# manager = Peregrine::EntityManager.new do |manager|
|
15
|
+
# manager.spawn { |entity| entity.name = 'Example' }
|
16
|
+
# end
|
17
|
+
# manager.entities.named(/^Ex/) # => [ Entity 'Example' ... ]
|
18
|
+
#
|
19
|
+
# Using a string as a matcher:
|
20
|
+
# manager = Peregrine::EntityManager.new do |manager|
|
21
|
+
# manager.spawn { |entity| entity.name = 'Example' }
|
22
|
+
# end
|
23
|
+
# manager.entities.named('Example') # => [ Entity 'Example' ... ]
|
24
|
+
def named(matcher)
|
25
|
+
items = select do |item|
|
26
|
+
next unless item.respond_to?(:name)
|
27
|
+
matcher.is_a?(Regexp) ? item.name[matcher] : item.name == matcher.to_s
|
28
|
+
end
|
29
|
+
items.each { |item| yield item } if block_given?
|
30
|
+
items.extend(Collections)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module Peregrine
|
2
|
+
module Collections
|
3
|
+
# Provides methods for filtering System objects contained in a collection.
|
4
|
+
# This module is intended to be an extension to existing collection
|
5
|
+
# instances.
|
6
|
+
module Systemic
|
7
|
+
# Returns an array of enabled System objects in the collection. Yields the
|
8
|
+
# matching System instances if a block is given.
|
9
|
+
def enabled
|
10
|
+
systems = select do |system|
|
11
|
+
next unless system.respond_to?(:enabled?)
|
12
|
+
system.enabled?
|
13
|
+
end
|
14
|
+
systems.each { |system| yield system } if block_given?
|
15
|
+
systems.extend(Collections)
|
16
|
+
end
|
17
|
+
|
18
|
+
# Returns an array of disabled System objects in the collection. Yields
|
19
|
+
# the matching System instances if a block is given.
|
20
|
+
def disabled
|
21
|
+
systems = select do |system|
|
22
|
+
next unless system.respond_to?(:enabled?)
|
23
|
+
!system.enabled?
|
24
|
+
end
|
25
|
+
systems.each { |system| yield system } if block_given?
|
26
|
+
systems.extend(Collections)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module Peregrine
|
2
|
+
module Collections
|
3
|
+
# Provides methods for filtering collections by item tags. This module is
|
4
|
+
# intended to be an extension to existing collection instances.
|
5
|
+
module Tagged
|
6
|
+
# Returns an array of objects within this collection with all of the given
|
7
|
+
# tags. Yields the tagged items in the collection if a block is given.
|
8
|
+
def tagged(*list)
|
9
|
+
items = select do |item|
|
10
|
+
next unless item.respond_to?(:tags)
|
11
|
+
list.all? { |tag| item.tags.include?(tag) }
|
12
|
+
end
|
13
|
+
items.each { |item| yield item } if block_given?
|
14
|
+
items.extend(Collections)
|
15
|
+
end
|
16
|
+
|
17
|
+
# Returns an array of objects within this collection that contain any of
|
18
|
+
# the given tags. Yields the tagged items in the collection if a block is
|
19
|
+
# given.
|
20
|
+
def any_tagged(*list)
|
21
|
+
items = select do |item|
|
22
|
+
next unless item.respond_to?(:tags)
|
23
|
+
!(item.tags & list).empty?
|
24
|
+
end
|
25
|
+
items.each { |item| yield item } if block_given?
|
26
|
+
items.extend(Collections)
|
27
|
+
end
|
28
|
+
|
29
|
+
# Returns an array of objects in this collection which are not tagged.
|
30
|
+
# Yields the untagged items in the collection if a block is given.
|
31
|
+
def untagged
|
32
|
+
items = select { |i| i.respond_to?(:tags) ? i.tags.empty? : true }
|
33
|
+
items.each { |item| yield item } if block_given?
|
34
|
+
items.extend(Collections)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# Require all Ruby files in the 'collections/' directory. Using +File.join+ to
|
2
|
+
# ensure that all files are properly required regardless of the directory
|
3
|
+
# separator in use.
|
4
|
+
Dir[File.join(File.dirname(__FILE__), 'collections', '*.rb')].each do |file|
|
5
|
+
require file
|
6
|
+
end
|
7
|
+
|
8
|
+
module Peregrine
|
9
|
+
# This module provides modules which define instance methods to be used by
|
10
|
+
# collections (such as arrays or hashes) with methods for filtering thier
|
11
|
+
# collected items. These modules are designed to be extensions to instances of
|
12
|
+
# collections so as not to pollute the default Ruby collections.
|
13
|
+
#
|
14
|
+
# Note that arrays returned from any of these modules are already extended
|
15
|
+
# with all of the defined collection modules, facilitating chaining of
|
16
|
+
# collection filtering methods.
|
17
|
+
module Collections
|
18
|
+
# Extends all of the constants defined within this module to the parent
|
19
|
+
# which is extended by this module -- essentially a shortcut to extend with
|
20
|
+
# all of the defined collections.
|
21
|
+
def self.extended(parent)
|
22
|
+
parent.send(:extend, *constants.map { |const| const_get(const) })
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
require 'peregrine/features'
|
2
|
+
|
3
|
+
module Peregrine
|
4
|
+
# == Summary
|
5
|
+
#
|
6
|
+
# Components serve as data storage for Entity objects. Component objects, by
|
7
|
+
# definition, should _not_ contain any logic -- they serve only as a way to
|
8
|
+
# store information or as a "flag" for an Entity to be used by a System to
|
9
|
+
# implement the actual logic.
|
10
|
+
#
|
11
|
+
# == Usage
|
12
|
+
#
|
13
|
+
# It is expected that developers will subclass the Component class in order
|
14
|
+
# to create their own individual Components. When subclassing the Component,
|
15
|
+
# it is important to note that you should _not_ overwrite the +#initialize+
|
16
|
+
# method, and instead overwite the +#initialize_data+ method in order to
|
17
|
+
# instantiate a Component. Any additional arguments given to +Component.new+
|
18
|
+
# are passed along to the +#initialize_data+ method.
|
19
|
+
#
|
20
|
+
# == Example
|
21
|
+
#
|
22
|
+
# class Mortal < Peregrine::Component
|
23
|
+
# attr_reader :health
|
24
|
+
#
|
25
|
+
# # Initializing the data for this Component object.
|
26
|
+
# def initialize_data(health = 100, max_health = 100)
|
27
|
+
# @health = health
|
28
|
+
# @maximum = max_health
|
29
|
+
# end
|
30
|
+
#
|
31
|
+
# # Do not implement logic, but define limits for data storage.
|
32
|
+
# def health=(value)
|
33
|
+
# @health = [0, value, @maximum].sort[1]
|
34
|
+
# end
|
35
|
+
# end
|
36
|
+
class Component
|
37
|
+
include Features
|
38
|
+
|
39
|
+
# Create a new Component instance. Any arguments given to this method are
|
40
|
+
# passed to the +initialize_data+ method. Yields the newly instanced
|
41
|
+
# Component if a block is given.
|
42
|
+
def initialize(*data_args)
|
43
|
+
initialize_data(*data_args)
|
44
|
+
yield self if block_given?
|
45
|
+
end
|
46
|
+
|
47
|
+
# Intended to be overwritten by subclasses of the Component to initialize
|
48
|
+
# the data used. Additional arguments passed to +Component.new+ are passed
|
49
|
+
# to this method directly. This method does nothing on its own.
|
50
|
+
def initialize_data
|
51
|
+
end
|
52
|
+
|
53
|
+
# Presents a human-readable summary of the Component.
|
54
|
+
def to_s
|
55
|
+
"Component '#{name}' #{id}"
|
56
|
+
end
|
57
|
+
alias :inspect :to_s
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,130 @@
|
|
1
|
+
require 'peregrine/collections/common'
|
2
|
+
require 'peregrine/features'
|
3
|
+
require 'securerandom'
|
4
|
+
|
5
|
+
module Peregrine
|
6
|
+
# == Summary
|
7
|
+
#
|
8
|
+
# An Entity is any object within your project -- literally almost anything.
|
9
|
+
# Entities are designed to hold Component objects which determine what the
|
10
|
+
# Entity contains in terms of data. Components are also regularly used to
|
11
|
+
# "flag" an Entity -- for example, adding an empty Component +Renderable+
|
12
|
+
# would flag to System objects that this Entity can be rendered.
|
13
|
+
#
|
14
|
+
# Essentially, Entity objects are little more than an identifier which serves
|
15
|
+
# as storage for Component objects. They do nothing more than store
|
16
|
+
# Components and provide methods for adding, removing, and filtering the
|
17
|
+
# contained Components.
|
18
|
+
#
|
19
|
+
# == Usage
|
20
|
+
#
|
21
|
+
# In general, it's best not to use Entity objects directly, but to make use
|
22
|
+
# of EntityManager objects which serve to organize and manage collections of
|
23
|
+
# Entity instances. See the EntityManager class' documentation for more
|
24
|
+
# information on how to use them.
|
25
|
+
class Entity
|
26
|
+
include Features
|
27
|
+
|
28
|
+
# Array of Component objects attached to this Entity.
|
29
|
+
attr_reader :components
|
30
|
+
|
31
|
+
# Symbol representing the UUID of this Entity.
|
32
|
+
attr_reader :uuid
|
33
|
+
|
34
|
+
# Creates a new Entity instance and adds the given Component constants or
|
35
|
+
# instances to the Entity. Yields the newly created Entity if a block is
|
36
|
+
# given.
|
37
|
+
#
|
38
|
+
# ==== Example
|
39
|
+
#
|
40
|
+
# Peregrine::Entity.new(Peregrine::Component) do |instance|
|
41
|
+
# # Entity names are optional, but let's name this one.
|
42
|
+
# instance.name = 'Example'
|
43
|
+
# end # => Entity 'Example' 0xdf7258 (1)
|
44
|
+
def initialize(*components)
|
45
|
+
@components = [].extend(Collections::Common)
|
46
|
+
@uuid = SecureRandom.uuid.to_sym
|
47
|
+
add_components(*components)
|
48
|
+
yield self if block_given?
|
49
|
+
end
|
50
|
+
|
51
|
+
# Adds the given Component constants or instances to this Entity. Constants
|
52
|
+
# given are automatically instanced. Ignores Component classes which are
|
53
|
+
# already present in the Entity. Returns the array of Component objects
|
54
|
+
# attached to this Entity after all additions.
|
55
|
+
#
|
56
|
+
# ==== Example
|
57
|
+
#
|
58
|
+
# class Example < Peregrine::Component ; end
|
59
|
+
#
|
60
|
+
# entity = Peregrine::Entity.new
|
61
|
+
# entity.add_components(Example) # => [Component 'Example' ...]
|
62
|
+
def add_components(*components)
|
63
|
+
components.each do |component|
|
64
|
+
component = component.new if component.class == Class
|
65
|
+
unless component_classes.include?(component.class)
|
66
|
+
@components.push(component)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
@components
|
70
|
+
end
|
71
|
+
alias :add_component :add_components
|
72
|
+
|
73
|
+
# Removes Component objects of the given classes from the Entity. Returns an
|
74
|
+
# array of the removed Component objects.
|
75
|
+
def remove_components!(*components)
|
76
|
+
removed = []
|
77
|
+
components.each do |flag|
|
78
|
+
@components.reject! { |c| c.class == flag ? removed.push(c) : false }
|
79
|
+
end
|
80
|
+
removed.extend(Collections::Common)
|
81
|
+
end
|
82
|
+
alias :remove_component! :remove_components!
|
83
|
+
|
84
|
+
# Returns an array of all Component classes attached to this Entity. Yields
|
85
|
+
# the Component classes if a block is given.
|
86
|
+
def component_classes
|
87
|
+
components = @components.map { |component| component.class }.uniq
|
88
|
+
components.each { |component| yield component } if block_given?
|
89
|
+
components.extend(Collections::Common)
|
90
|
+
end
|
91
|
+
|
92
|
+
# Returns all of the Component objects attached to this Entity of the exact
|
93
|
+
# given class. Yields the Component objects if a block is given.
|
94
|
+
#
|
95
|
+
# ==== Example
|
96
|
+
#
|
97
|
+
# class Example < Peregrine::Component ; end
|
98
|
+
#
|
99
|
+
# entity = Peregrine::Entity.new(Example)
|
100
|
+
# entity.components_of_class(Example) # => [Component 'Example' ...]
|
101
|
+
def components_of_class(const)
|
102
|
+
components = @components.select { |c| c.class == const }
|
103
|
+
components.each { |component| yield component } if block_given?
|
104
|
+
components.extend(Collections::Common)
|
105
|
+
end
|
106
|
+
|
107
|
+
# Returns all of the Component objects attached to this Entity descended
|
108
|
+
# from the given class or module. Yields the Component objects if a block is
|
109
|
+
# given.
|
110
|
+
#
|
111
|
+
# ==== Example
|
112
|
+
#
|
113
|
+
# class Example < Peregrine::Component ; end
|
114
|
+
#
|
115
|
+
# entity = Peregrine::Entity.new(Peregrine::Component, Example)
|
116
|
+
# entity.components_of_kind(Peregrine::Component)
|
117
|
+
# # => [Component 'Peregrine::Component' ..., Component 'Example' ...]
|
118
|
+
def components_of_kind(const)
|
119
|
+
components = @components.select { |c| c.class.ancestors.include?(const) }
|
120
|
+
components.each { |component| yield component } if block_given?
|
121
|
+
components.extend(Collections::Common)
|
122
|
+
end
|
123
|
+
|
124
|
+
# Presents a human-readable summary of the Entity.
|
125
|
+
def to_s
|
126
|
+
"Entity '#{name}' #{uuid} (#{@components.size})"
|
127
|
+
end
|
128
|
+
alias :inspect :to_s
|
129
|
+
end
|
130
|
+
end
|
@@ -0,0 +1,164 @@
|
|
1
|
+
require 'peregrine/collections'
|
2
|
+
require 'peregrine/entity'
|
3
|
+
require 'peregrine/features'
|
4
|
+
|
5
|
+
module Peregrine
|
6
|
+
# == Summary
|
7
|
+
#
|
8
|
+
# The EntityManager is an object designed to create, organize, and filter an
|
9
|
+
# array of Entity objects and manage System objects which implement logic for
|
10
|
+
# those Entities. Essentially, the EntityManager serves to group related
|
11
|
+
# Entity objects and provide them with the Systems that act upon them.
|
12
|
+
#
|
13
|
+
# == Usage
|
14
|
+
#
|
15
|
+
# The EntityManager is designed to group related Entity objects with the
|
16
|
+
# System instances which provide their logic. As such, it is recommended that
|
17
|
+
# you utilize as many instances of EntityManager objects as your application
|
18
|
+
# requires.
|
19
|
+
#
|
20
|
+
# EntityManager objects automatically extend their arrays of Entity and System
|
21
|
+
# objects with instance methods designed to facilitate filtering of the
|
22
|
+
# managed items each respectively contain. Entities may be filtered by name,
|
23
|
+
# included (or excluded) tags, and which Component classes they may or may not
|
24
|
+
# contain. Likewise, System objects may be filtered based on any tags they do
|
25
|
+
# or do not have as well as by name. For more detailed information about the
|
26
|
+
# available filtering methods, please see the documentation for the
|
27
|
+
# Peregrine::Collections module and the modules that it contains.
|
28
|
+
#
|
29
|
+
# Note that Peregrine itself does not include any objects for grouping
|
30
|
+
# EntityManager instances together (unlike some other frameworks) -- this
|
31
|
+
# functionality is left up to individual developers and their preferred
|
32
|
+
# methods. Use your EntityManagers in whatever way you personally feel would
|
33
|
+
# best benefit _you_ and _your_ application.
|
34
|
+
#
|
35
|
+
# == Examples
|
36
|
+
#
|
37
|
+
# Naming EntityManager instances, spawning Entity objects, and filtering
|
38
|
+
# Entity objects based on tags:
|
39
|
+
#
|
40
|
+
# class Example < Peregrine::Component ; end
|
41
|
+
#
|
42
|
+
# manager = Peregrine::EntityManager.new { |m| m.name = 'Example' }
|
43
|
+
# 5.times do |iterator|
|
44
|
+
# iterator += 1
|
45
|
+
# manager.spawn(Example) do |entity|
|
46
|
+
# entity.name = iterator
|
47
|
+
# iterator.even? ? entity.add_tag(:even) : entity.add_tag(:odd)
|
48
|
+
# end
|
49
|
+
# end
|
50
|
+
# manager.entities.tagged(:even) # => [Entity '2' ..., Entity '4' ...]
|
51
|
+
#
|
52
|
+
# Finding all basic entities and adding a component to them with a block:
|
53
|
+
#
|
54
|
+
# manager = Peregrine::EntityManager.new
|
55
|
+
# rand(1..20).times { manager.spawn }
|
56
|
+
# manager.basic_entities do |entity|
|
57
|
+
# e.add_component(Peregrine::Component)
|
58
|
+
# end
|
59
|
+
class EntityManager
|
60
|
+
include Features
|
61
|
+
|
62
|
+
# Array of entities owned by this EntityManager.
|
63
|
+
attr_reader :entities
|
64
|
+
|
65
|
+
# Array of Systems operating within this EntityManager.
|
66
|
+
attr_reader :systems
|
67
|
+
|
68
|
+
# Creates a new EntityManager instance. Yields the newly created instance if
|
69
|
+
# a block is given.
|
70
|
+
def initialize
|
71
|
+
@entities = [].extend(Collections::Common, Collections::Composite)
|
72
|
+
@systems = [].extend(Collections::Common, Collections::Systemic)
|
73
|
+
yield self if block_given?
|
74
|
+
end
|
75
|
+
|
76
|
+
# Calls +process+ on each enabled System within this EntityManager. The
|
77
|
+
# +process+ method is _intended_ to be called once per tick, but the actual
|
78
|
+
# implementation is left up to the developer. Returns the array of System
|
79
|
+
# objects operating within the EntityManager.
|
80
|
+
def process
|
81
|
+
@systems.each { |system| system.process if system.enabled? }
|
82
|
+
end
|
83
|
+
|
84
|
+
# Spawns a new Entity with the given Components and pushes the new Entity
|
85
|
+
# into the array of owned entities. Returns the spawned Entity. Yields the
|
86
|
+
# new Entity before pushing it into the owned entities array if a block is
|
87
|
+
# given.
|
88
|
+
def spawn(*arguments)
|
89
|
+
entity = Peregrine::Entity.new(*arguments)
|
90
|
+
yield entity if block_given?
|
91
|
+
@entities.push(entity)
|
92
|
+
entity
|
93
|
+
end
|
94
|
+
|
95
|
+
# Adds the given Entity instances (or instantiated Entity constants) to the
|
96
|
+
# EntityManager. Returns the array of managed Entity objects.
|
97
|
+
def add_entities(*entities)
|
98
|
+
entities.each do |entity|
|
99
|
+
entity = entity.new if entity.class == Class
|
100
|
+
@entities.push(entity)
|
101
|
+
end
|
102
|
+
@entities
|
103
|
+
end
|
104
|
+
alias :add_entity :add_entities
|
105
|
+
|
106
|
+
# Removes the given Entity instances from the EntityManager. Returns the
|
107
|
+
# removed Entity objects. Intended to be used with the EntityManager's
|
108
|
+
# filtering methods to select appropriate Entity instances.
|
109
|
+
def remove_entities!(*entities)
|
110
|
+
removed = []
|
111
|
+
@entities.reject! do |entity|
|
112
|
+
entities.include?(entity) ? removed.push(entity) : false
|
113
|
+
end
|
114
|
+
removed.extend(Collections::Common, Collections::Composite)
|
115
|
+
end
|
116
|
+
alias :remove_entity! :remove_entities!
|
117
|
+
|
118
|
+
# Returns an array of all managed Entity objects without Components. Yields
|
119
|
+
# the array of basic Entities if a block is given. Note that "basic" Entity
|
120
|
+
# objects _may_ still be named or tagged.
|
121
|
+
def basic_entities
|
122
|
+
entities = @entities.select { |e| e.components.empty? }
|
123
|
+
entities.each { |entity| yield entity } if block_given?
|
124
|
+
entities.extend(Collections::Common, Collections::Composite)
|
125
|
+
end
|
126
|
+
|
127
|
+
# Adds the given System instances (or instantiated System constants) to the
|
128
|
+
# EntityManager, updating the System's manager. Returns the array of System
|
129
|
+
# objects operating in the EntityManager.
|
130
|
+
def add_systems(*systems)
|
131
|
+
systems.each do |system|
|
132
|
+
if system.class == Class
|
133
|
+
system = system.new(self)
|
134
|
+
else
|
135
|
+
@systems.push(system)
|
136
|
+
system.manager = self
|
137
|
+
end
|
138
|
+
end
|
139
|
+
@systems
|
140
|
+
end
|
141
|
+
alias :add_system :add_systems
|
142
|
+
|
143
|
+
# Removes the given System instances (or all System classes of the given
|
144
|
+
# constants) from the EntityManager, updating the manager of each System.
|
145
|
+
# Returns an array of the removed System instances.
|
146
|
+
def remove_systems!(*systems)
|
147
|
+
removed = []
|
148
|
+
@systems.reject! do |system|
|
149
|
+
if systems.include?(system) || systems.include?(system.class)
|
150
|
+
system.manager = nil
|
151
|
+
removed.push(system)
|
152
|
+
else false end
|
153
|
+
end
|
154
|
+
removed.extend(Collections::Common, Collections::Systemic)
|
155
|
+
end
|
156
|
+
|
157
|
+
# Presents a human-readable summary of the EntityManager.
|
158
|
+
def to_s
|
159
|
+
"Entity Manager '#{name}' #{id} " <<
|
160
|
+
"(#{@entities.size} Entities, #{@systems.size} Systems)"
|
161
|
+
end
|
162
|
+
alias :inspect :to_s
|
163
|
+
end
|
164
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Peregrine
|
2
|
+
module Features
|
3
|
+
# Provides the +configure+ method. Intended to be included in classes
|
4
|
+
# requiring this functionality.
|
5
|
+
module Configurable
|
6
|
+
# Yields the object this method is called on. This method is intended to
|
7
|
+
# allow configuration of Peregrine objects with other features. Returns
|
8
|
+
# the object this method was called on.
|
9
|
+
#
|
10
|
+
# ==== Example
|
11
|
+
#
|
12
|
+
# entity = Peregrine::Entity.new
|
13
|
+
# entity.configure { |e| e.name = 'Example' } # => Entity 'Example' ...
|
14
|
+
def configure
|
15
|
+
yield self if block_given?
|
16
|
+
self
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
module Peregrine
|
2
|
+
module Features
|
3
|
+
# Provides the +id+ method to extract an object's +object_id+ as a hex
|
4
|
+
# string. Intended to be included in classes requiring this functionality.
|
5
|
+
module Identifiable
|
6
|
+
# Returns the object's +object_id+ as a hexadecimal string.
|
7
|
+
def id
|
8
|
+
'0x' << (object_id << 1).to_s(16)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Peregrine
|
2
|
+
module Features
|
3
|
+
# Provides methods for naming objects. Intended to be included in classes
|
4
|
+
# requiring this functionality.
|
5
|
+
module Nameable
|
6
|
+
# Returns the name of the object. Lazily evaluated.
|
7
|
+
def name
|
8
|
+
@name ||= self.class.to_s
|
9
|
+
end
|
10
|
+
|
11
|
+
# Sets the name of the object to the given value after String coercion.
|
12
|
+
def name=(value)
|
13
|
+
@name = value.to_s
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module Peregrine
|
2
|
+
module Features
|
3
|
+
# Provides methods for adding and removing tags to and from objects. This
|
4
|
+
# essentially provides yet another method for identifying and filtering
|
5
|
+
# various objects. Intended to be included in classes requiring this
|
6
|
+
# functionality.
|
7
|
+
module Taggable
|
8
|
+
# Returns the array of tags this object contains. Lazily evaluated.
|
9
|
+
def tags
|
10
|
+
@tags ||= []
|
11
|
+
end
|
12
|
+
|
13
|
+
# Add the given tags to the object. Tags may be any kind of object. Tags
|
14
|
+
# identical to existing tags are ignored. Returns an array of all tags
|
15
|
+
# this object contains.
|
16
|
+
def add_tags(*list)
|
17
|
+
tags.push(*list).uniq!
|
18
|
+
tags
|
19
|
+
end
|
20
|
+
alias :add_tag :add_tags
|
21
|
+
|
22
|
+
# Removes the given tags from the object. Returns an array of the removed
|
23
|
+
# tags.
|
24
|
+
def remove_tags!(*list)
|
25
|
+
removed = []
|
26
|
+
tags.reject! { |tag| list.include?(tag) ? removed.push(tag) : false }
|
27
|
+
removed
|
28
|
+
end
|
29
|
+
alias :remove_tag! :remove_tags!
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# Require all Ruby files in the 'features/' directory. Using +File.join+ to
|
2
|
+
# ensure that all files are properly required regardless of the directory
|
3
|
+
# separator in use.
|
4
|
+
Dir[File.join(File.dirname(__FILE__), 'features', '*.rb')].each do |file|
|
5
|
+
require file
|
6
|
+
end
|
7
|
+
|
8
|
+
module Peregrine
|
9
|
+
# This module provides modules which add instance methods to Peregrine objects
|
10
|
+
# that provide functionality such as tags, names, and identifiers. These
|
11
|
+
# modules are designed to be included in Peregrine classes.
|
12
|
+
module Features
|
13
|
+
# Includes all of the constants defined within this module to the parent
|
14
|
+
# which includes this module -- essentially a shortcut to include all of the
|
15
|
+
# defined features.
|
16
|
+
def self.included(parent)
|
17
|
+
parent.send(:include, *constants.map { |const| const_get(const) })
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,129 @@
|
|
1
|
+
require 'peregrine/collections'
|
2
|
+
require 'peregrine/features'
|
3
|
+
|
4
|
+
module Peregrine
|
5
|
+
# == Summary
|
6
|
+
#
|
7
|
+
# The System class implements logic for a collection of Entity objects and
|
8
|
+
# resides within an EntityManager instance. The System class present in the
|
9
|
+
# Peregrine framework serves as a basis for creating subclasses to implement
|
10
|
+
# logic and perform no such logic themselves (in fact, they raise a
|
11
|
+
# +NotImplementedError+ when processed).
|
12
|
+
#
|
13
|
+
# == Usage
|
14
|
+
#
|
15
|
+
# The default System class is very minimal and performs no actual logic on its
|
16
|
+
# own. Individual developers are intended to subclass the System and implement
|
17
|
+
# the needed logic in their own +process+ methods.
|
18
|
+
#
|
19
|
+
# In addition to this, System classes may be enabled or disabled. Disabled
|
20
|
+
# System classes may still be explicitly updated by calling their +process+
|
21
|
+
# method, but will not automatically fire when the EntityManager that they
|
22
|
+
# belong to calls its +process+ method. Systems are enabled by default, but
|
23
|
+
# may be disabled when instanced by passing a block or calling the +configure+
|
24
|
+
# method with a block.
|
25
|
+
#
|
26
|
+
# Most System classes are expected to operate on a limited subset of the
|
27
|
+
# available Entity objects within the EntityManager that contains the System.
|
28
|
+
# As such, the +selector+ method has been provided to allow filtering the
|
29
|
+
# EntityManager's managed Entity objects. The +selector+ method returns a
|
30
|
+
# Proc object which is used by the +select+ method on the +entities+ array
|
31
|
+
# owned by the EntityManager. When overwritten, the +selector+ method ensures
|
32
|
+
# that the +entities+ method of the System only returns Entity objects that
|
33
|
+
# pass through the +selector+ method's block.
|
34
|
+
#
|
35
|
+
# Given the implementation of the +selector+, it is _highly_ recommended that
|
36
|
+
# you wrap your Systems' +process+ methods in the +entities+ method to make
|
37
|
+
# sure that they only implement logic for the properly selected Entities.
|
38
|
+
#
|
39
|
+
# == Example
|
40
|
+
#
|
41
|
+
# To demonstrate how to write a System subclass, we'll build a simple System
|
42
|
+
# that simply acts on Entity objects that contain Components and removes the
|
43
|
+
# Components when processed.
|
44
|
+
#
|
45
|
+
# class Remover < Peregrine::System
|
46
|
+
# def selector
|
47
|
+
# ->(entity) { !entity.components.empty? }
|
48
|
+
# end
|
49
|
+
#
|
50
|
+
# def process
|
51
|
+
# entities.each do |entity|
|
52
|
+
# entity.remove_components!(*entity.component_classes)
|
53
|
+
# end
|
54
|
+
# end
|
55
|
+
# end
|
56
|
+
#
|
57
|
+
# Now we'll create a new, disabled instance of the Remover System.
|
58
|
+
#
|
59
|
+
# manager = Peregrine::EntityManager.new
|
60
|
+
# manager.add_system(Remover.new { |system| system.disable })
|
61
|
+
class System
|
62
|
+
include Features
|
63
|
+
|
64
|
+
# The EntityManager object using this System instance.
|
65
|
+
attr_accessor :manager
|
66
|
+
|
67
|
+
# Creates a new System instance operating within the given EntityManager
|
68
|
+
# and automatically adds the System to the EntityManager. Yields the newly
|
69
|
+
# created System if a block is given.
|
70
|
+
#
|
71
|
+
# ==== Example
|
72
|
+
#
|
73
|
+
# manager = Peregrine::EntityManager.new { |m| m.name = 'Example' }
|
74
|
+
# Peregrine::System.new(manager) do |system|
|
75
|
+
# system.name = 'Example'
|
76
|
+
# # Systems are enabled by default, but we want this one disabled.
|
77
|
+
# system.disable
|
78
|
+
# end # => - System 'Example' 0x1724d80 <Entity Manager 'Example' ...>
|
79
|
+
def initialize(manager = nil)
|
80
|
+
@enabled = true
|
81
|
+
@manager = manager
|
82
|
+
@manager.systems.push(self) if @manager.respond_to?(:systems)
|
83
|
+
yield self if block_given?
|
84
|
+
end
|
85
|
+
|
86
|
+
# Provides the block to be given to the +entities+ method's +select+ call
|
87
|
+
# used to filter the Entity objects affected by the System. The default
|
88
|
+
# +selector+ implementation returns all Entity objects that are not +false+
|
89
|
+
# or +nil+. Intended to be overwritten in subclasses.
|
90
|
+
def selector
|
91
|
+
Proc.new { |entity| entity }
|
92
|
+
end
|
93
|
+
|
94
|
+
# Returns an array of all of the Entity objects that this System should act
|
95
|
+
# upon. This method is intended to be used in the body of a subclass'
|
96
|
+
# +update+ method in order to facilitate only operating on the desired
|
97
|
+
# Entity objects. Entity objects are collected by calling +select+ on the
|
98
|
+
# EntityManager's +entities+ array with a block supplied by the System's
|
99
|
+
# +selector+ method. Yields each selected Entity object if a block is given.
|
100
|
+
def entities
|
101
|
+
return [] unless @manager.respond_to?(:entities)
|
102
|
+
@manager.entities.select(&selector).each do |entity|
|
103
|
+
yield entity if block_given?
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
# Explicitly enables the System.
|
108
|
+
def enable() @enabled = true end
|
109
|
+
|
110
|
+
# Explicitly disables the System.
|
111
|
+
def disable() @enabled = false end
|
112
|
+
|
113
|
+
# Explicitly returns +true+ if the System is enabled, +false+ otherwise.
|
114
|
+
def enabled?() @enabled ? true : false end
|
115
|
+
|
116
|
+
# Called whenever the EntityManager with this System is updated. This method
|
117
|
+
# is intended to be overwritten in subclasses by developers in order to
|
118
|
+
# implement logic. Only called by the EntityManager if the System is
|
119
|
+
# enabled. Raises a +NotImplementedError+ by default.
|
120
|
+
def process() raise NotImplementedError end
|
121
|
+
|
122
|
+
# Presents a human-readable summary of the System.
|
123
|
+
def to_s
|
124
|
+
status = enabled? ? '+' : '-'
|
125
|
+
"#{status} System '#{name}' #{id} <#{manager.name} #{manager.id}>"
|
126
|
+
end
|
127
|
+
alias :inspect :to_s
|
128
|
+
end
|
129
|
+
end
|
data/lib/peregrine.rb
ADDED
@@ -0,0 +1,6 @@
|
|
1
|
+
# Require all Ruby files in the 'peregrine/' directory. Using +File.join+ to
|
2
|
+
# ensure that all files are properly required regardless of the directory
|
3
|
+
# separator in use.
|
4
|
+
Dir[File.join(File.dirname(__FILE__), 'peregrine', '*.rb')].each do |file|
|
5
|
+
require file
|
6
|
+
end
|
metadata
ADDED
@@ -0,0 +1,114 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: peregrine
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Kevin Owen
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2014-02-11 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: rake
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :development
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ! '>='
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '0'
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: rdoc
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ! '>='
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '0'
|
38
|
+
type: :development
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ! '>='
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '0'
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: rspec
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ! '>='
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0'
|
54
|
+
type: :development
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ! '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
description: Peregrine is a minimal, highly adaptive Entity-Component design framework
|
63
|
+
for the Ruby scripting language designed for general-purpose programming.
|
64
|
+
email:
|
65
|
+
- solucet@icloud.com
|
66
|
+
executables: []
|
67
|
+
extensions: []
|
68
|
+
extra_rdoc_files: []
|
69
|
+
files:
|
70
|
+
- lib/peregrine/collections/common.rb
|
71
|
+
- lib/peregrine/collections/composite.rb
|
72
|
+
- lib/peregrine/collections/named.rb
|
73
|
+
- lib/peregrine/collections/systemic.rb
|
74
|
+
- lib/peregrine/collections/tagged.rb
|
75
|
+
- lib/peregrine/collections.rb
|
76
|
+
- lib/peregrine/component.rb
|
77
|
+
- lib/peregrine/entity.rb
|
78
|
+
- lib/peregrine/entity_manager.rb
|
79
|
+
- lib/peregrine/features/configurable.rb
|
80
|
+
- lib/peregrine/features/identifiable.rb
|
81
|
+
- lib/peregrine/features/nameable.rb
|
82
|
+
- lib/peregrine/features/taggable.rb
|
83
|
+
- lib/peregrine/features.rb
|
84
|
+
- lib/peregrine/system.rb
|
85
|
+
- lib/peregrine/version.rb
|
86
|
+
- lib/peregrine.rb
|
87
|
+
- LICENSE
|
88
|
+
- README.md
|
89
|
+
homepage: https://github.com/solucet/peregrine
|
90
|
+
licenses:
|
91
|
+
- MIT
|
92
|
+
post_install_message:
|
93
|
+
rdoc_options: []
|
94
|
+
require_paths:
|
95
|
+
- lib
|
96
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
97
|
+
none: false
|
98
|
+
requirements:
|
99
|
+
- - ! '>='
|
100
|
+
- !ruby/object:Gem::Version
|
101
|
+
version: '0'
|
102
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
103
|
+
none: false
|
104
|
+
requirements:
|
105
|
+
- - ! '>='
|
106
|
+
- !ruby/object:Gem::Version
|
107
|
+
version: '0'
|
108
|
+
requirements: []
|
109
|
+
rubyforge_project:
|
110
|
+
rubygems_version: 1.8.23
|
111
|
+
signing_key:
|
112
|
+
specification_version: 3
|
113
|
+
summary: Flexible Entity-Component framework for Ruby.
|
114
|
+
test_files: []
|