kiwi-ecs 0.0.2
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 +7 -0
- data/lib/arch_store.rb +30 -0
- data/lib/archetype.rb +93 -0
- data/lib/bitmap.rb +32 -0
- data/lib/entity.rb +102 -0
- data/lib/kiwi-ecs.rb +20 -0
- data/lib/query.rb +56 -0
- data/lib/world.rb +64 -0
- metadata +186 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: b17b772e56b3533bac84c71c609eb8544908667c4cac99725327399d1fd44fed
|
4
|
+
data.tar.gz: d8669b034e3db56f038919b60aa8f7f2b36eb6b98e7f12d22f533ac2c2680454
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: f5e2f0b3770ffe2eeebd64f590aec863fc8074f4ec1331578f1a7f5ee42ac69e8ca257c3cf9717f8e7b43c876dbe80f946e9e74045e0a4efa9feaeb78b8b05c7
|
7
|
+
data.tar.gz: a42b7f00ee919b7cd1d37f70c0a19f1b217d429f604f67e0a99734c4de4336d7bcecea9235e244fb884c93288f06df38c9b9e4a453520e7ce86809c92ad5a6e0
|
data/lib/arch_store.rb
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
require_relative 'archetype'
|
2
|
+
|
3
|
+
class ArchStore
|
4
|
+
# [[Integer]:Integer]
|
5
|
+
attr_accessor :compMap
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
# [Archetype]
|
9
|
+
@archetypes = []
|
10
|
+
# [[ComponentId]:ArchetypeId]
|
11
|
+
@compMap = Hash.new
|
12
|
+
end
|
13
|
+
|
14
|
+
def get(archetypeId)
|
15
|
+
return @archetypes[archetypeId]
|
16
|
+
end
|
17
|
+
|
18
|
+
# @params [Array<Integer>] componentIds: a sorted array of component ids
|
19
|
+
def get_archetype_id(componentIds)
|
20
|
+
archId = @compMap[componentIds]
|
21
|
+
if archId != nil
|
22
|
+
return archId
|
23
|
+
else
|
24
|
+
id = @archetypes.size
|
25
|
+
@archetypes.push Archetype.new(componentIds)
|
26
|
+
@compMap[componentIds] = id
|
27
|
+
return id
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
data/lib/archetype.rb
ADDED
@@ -0,0 +1,93 @@
|
|
1
|
+
ComponentColumn = Struct.new(:components)
|
2
|
+
|
3
|
+
class Archetype
|
4
|
+
# @params [Array<Integer>] componentIds
|
5
|
+
def initialize(componentIds)
|
6
|
+
# [Integer(componentId): ComponentColumn]
|
7
|
+
@components = componentIds.map do |compId|
|
8
|
+
[compId, ComponentColumn.new([])]
|
9
|
+
end.to_h
|
10
|
+
@availableEntityRows = []
|
11
|
+
@entities = []
|
12
|
+
end
|
13
|
+
|
14
|
+
# @return [int]
|
15
|
+
def new_arch_row_id(entityId)
|
16
|
+
id = @availableEntityRows.pop
|
17
|
+
if id != nil
|
18
|
+
@entities[id] = entityId
|
19
|
+
return id
|
20
|
+
else
|
21
|
+
id = @entities.size
|
22
|
+
@entities.push entityId
|
23
|
+
return id
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def set_component(archRowId, component)
|
28
|
+
compCol = @components[component.class.object_id]
|
29
|
+
if compCol.components.size <= archRowId
|
30
|
+
compCol.components.push component
|
31
|
+
else
|
32
|
+
compCol.components[archRowId] = component
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def get_component(archRowId, componentId)
|
37
|
+
return @components[componentId].components[archRowId]
|
38
|
+
end
|
39
|
+
|
40
|
+
def has_component(componentId)
|
41
|
+
return @components[componentId] != nil
|
42
|
+
end
|
43
|
+
|
44
|
+
def remove_entity(archRowId)
|
45
|
+
@availableEntityRows.push(archRowId)
|
46
|
+
@entities[archRowId] = nil
|
47
|
+
end
|
48
|
+
|
49
|
+
# @return [[Component, EntityId]]
|
50
|
+
def active_components(componentIds)
|
51
|
+
# [comp1a, comp1b]
|
52
|
+
# [comp2a, comp2b]
|
53
|
+
# ...
|
54
|
+
components = componentIds
|
55
|
+
.map do |compId|
|
56
|
+
@components[compId].components
|
57
|
+
end
|
58
|
+
compCount = componentIds.size
|
59
|
+
|
60
|
+
@entities
|
61
|
+
.each_with_index
|
62
|
+
.filter do |entId, _|
|
63
|
+
entId != nil
|
64
|
+
end
|
65
|
+
.map do |_, rowIdx|
|
66
|
+
(0...compCount).map do |compRowId|
|
67
|
+
components[compRowId][rowIdx]
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def components_and_ids(componentIds)
|
73
|
+
# [comp1a, comp1b]
|
74
|
+
# [comp2a, comp2b]
|
75
|
+
# ...
|
76
|
+
components = componentIds
|
77
|
+
.map do |compId|
|
78
|
+
@components[compId].components
|
79
|
+
end
|
80
|
+
compCount = componentIds.size
|
81
|
+
|
82
|
+
@entities
|
83
|
+
.each_with_index
|
84
|
+
.filter do |entId, _|
|
85
|
+
entId != nil
|
86
|
+
end
|
87
|
+
.map do |entId, rowIdx|
|
88
|
+
[entId, *(0...compCount).map do |compRowId|
|
89
|
+
components[compRowId][rowIdx]
|
90
|
+
end]
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
data/lib/bitmap.rb
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
# TODO: actual bitmap implementation
|
2
|
+
class Bitmap
|
3
|
+
def initialize
|
4
|
+
@flags = []
|
5
|
+
end
|
6
|
+
|
7
|
+
def contains(id)
|
8
|
+
return (@flags.size > id) && @flags[id]
|
9
|
+
end
|
10
|
+
|
11
|
+
def set(id)
|
12
|
+
if @flags.size < id
|
13
|
+
(@flags.size...id).each do |i|
|
14
|
+
@flags.push false
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
@flags[id] = true
|
19
|
+
end
|
20
|
+
|
21
|
+
def remove(id)
|
22
|
+
if @flags.size > id
|
23
|
+
@flags[id] = false
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def clear
|
28
|
+
(0...@flags.size).each do |i|
|
29
|
+
@flags[i] = false
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
data/lib/entity.rb
ADDED
@@ -0,0 +1,102 @@
|
|
1
|
+
require 'set'
|
2
|
+
require_relative 'bitmap'
|
3
|
+
|
4
|
+
Entity = Struct.new(:archId, :archRow)
|
5
|
+
|
6
|
+
class EntityStore
|
7
|
+
def initialize
|
8
|
+
# [Entity]
|
9
|
+
@entities = []
|
10
|
+
# [Bitmap]
|
11
|
+
@flags = []
|
12
|
+
@nextId = 0
|
13
|
+
@deadEntities = Set.new
|
14
|
+
end
|
15
|
+
|
16
|
+
# Returns a new entity id (int)
|
17
|
+
def new_id
|
18
|
+
id = @deadEntities.first
|
19
|
+
if id != nil
|
20
|
+
@deadEntities.delete id
|
21
|
+
return id
|
22
|
+
else
|
23
|
+
id = @nextId
|
24
|
+
@nextId += 1
|
25
|
+
return id
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
# Spawn a new entity with the given ids
|
30
|
+
def spawn(entityId, archId, archRowId)
|
31
|
+
if @entities.count <= entityId
|
32
|
+
@entities.push(Entity.new(archId, archRowId))
|
33
|
+
else
|
34
|
+
@entities[entityId] = Entity.new(archId, archRowId)
|
35
|
+
self.reset_flags(entityId)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
# Returns an entity
|
40
|
+
# @param [Integer] entityId
|
41
|
+
def get(entityId)
|
42
|
+
return @entities[entityId]
|
43
|
+
end
|
44
|
+
|
45
|
+
# def exists?(entityId)
|
46
|
+
# return @entities.size > entityId
|
47
|
+
# end
|
48
|
+
|
49
|
+
# Mark an entity as dead
|
50
|
+
def kill(entityId)
|
51
|
+
@deadEntities.add entityId
|
52
|
+
end
|
53
|
+
|
54
|
+
def entity_count
|
55
|
+
return ((0...@entities.count).reduce(0) do |count, id|
|
56
|
+
if @deadEntities.include? id
|
57
|
+
return count
|
58
|
+
else
|
59
|
+
return count + 1
|
60
|
+
end
|
61
|
+
end)
|
62
|
+
end
|
63
|
+
|
64
|
+
# @param [Integer, #read] entityId
|
65
|
+
# @param [Integer, #read] flagId
|
66
|
+
# @return [bool]
|
67
|
+
def has_flag(entityId, flagId)
|
68
|
+
if @flags.size <= entityId
|
69
|
+
return false
|
70
|
+
else
|
71
|
+
return @flags[entityId].contains(flagId)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def set_flag(entityId, flagId)
|
76
|
+
if @flags.size <= entityId
|
77
|
+
(@flags.size..entityId).each do |_|
|
78
|
+
@flags.push(Bitmap.new)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
@flags[entityId].set(flagId)
|
83
|
+
end
|
84
|
+
|
85
|
+
def remove_flag(entityId, flagId)
|
86
|
+
if @flags.size <= entityId
|
87
|
+
return
|
88
|
+
end
|
89
|
+
|
90
|
+
@flags[entityId].remove(flagId)
|
91
|
+
end
|
92
|
+
|
93
|
+
def reset_flags(entityId)
|
94
|
+
@flags[entityId].clear()
|
95
|
+
end
|
96
|
+
|
97
|
+
def entity_ids
|
98
|
+
return (0...@nextId).filter do |id|
|
99
|
+
!@deadEntities.include?(id)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
data/lib/kiwi-ecs.rb
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
# Kiwi ecs
|
2
|
+
#
|
3
|
+
# An easy-to-use ECS implementation focussed on fast iteration of components
|
4
|
+
#
|
5
|
+
# Copyright (C) 2023 Jonas Everaert
|
6
|
+
#
|
7
|
+
# This program is free software: you can redistribute it and/or modify
|
8
|
+
# it under the terms of the GNU General Public License as published by
|
9
|
+
# the Free Software Foundation, either version 3 of the License, or
|
10
|
+
# (at your option) any later version.
|
11
|
+
#
|
12
|
+
# This program is distributed in the hope that it will be useful,
|
13
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
14
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
15
|
+
# GNU General Public License for more details.
|
16
|
+
#
|
17
|
+
# You should have received a copy of the GNU General Public License
|
18
|
+
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
19
|
+
|
20
|
+
require_relative 'world'
|
data/lib/query.rb
ADDED
@@ -0,0 +1,56 @@
|
|
1
|
+
module Query
|
2
|
+
# @return [enumerator<Integer>]
|
3
|
+
def query_ids
|
4
|
+
if block_given?
|
5
|
+
@entityStore.entity_ids.each do |id|
|
6
|
+
yield id
|
7
|
+
end
|
8
|
+
else
|
9
|
+
return @entityStore.entity_ids
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def query(*componentType)
|
14
|
+
componentIds = componentType.map { |type| type.object_id }
|
15
|
+
|
16
|
+
result = @archStore.compMap
|
17
|
+
.filter do |archComponents, _|
|
18
|
+
(componentIds - archComponents).empty?
|
19
|
+
end
|
20
|
+
.flat_map do |_, archId|
|
21
|
+
@archStore
|
22
|
+
.get(archId)
|
23
|
+
.active_components(componentIds)
|
24
|
+
end
|
25
|
+
|
26
|
+
if block_given?
|
27
|
+
result.each do |a|
|
28
|
+
yield a
|
29
|
+
end
|
30
|
+
else
|
31
|
+
return result.to_enum
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def query_with_ids(*componentType)
|
36
|
+
componentIds = componentType.map { |type| type.object_id }
|
37
|
+
|
38
|
+
result = @archStore.compMap
|
39
|
+
.filter do |archComponents, _|
|
40
|
+
(componentIds - archComponents).empty?
|
41
|
+
end
|
42
|
+
.flat_map do |_, archId|
|
43
|
+
@archStore
|
44
|
+
.get(archId)
|
45
|
+
.components_and_ids(componentIds)
|
46
|
+
end
|
47
|
+
|
48
|
+
if block_given?
|
49
|
+
result.each do |a|
|
50
|
+
yield a
|
51
|
+
end
|
52
|
+
else
|
53
|
+
return result.to_enum
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
data/lib/world.rb
ADDED
@@ -0,0 +1,64 @@
|
|
1
|
+
require_relative 'entity'
|
2
|
+
require_relative 'arch_store'
|
3
|
+
require_relative 'query'
|
4
|
+
|
5
|
+
class World
|
6
|
+
def initialize
|
7
|
+
@entityStore = EntityStore.new
|
8
|
+
@archStore = ArchStore.new
|
9
|
+
end
|
10
|
+
|
11
|
+
def spawn(*components)
|
12
|
+
compIds = components.map { |c| c.class.object_id }.sort
|
13
|
+
archId = @archStore.get_archetype_id(compIds)
|
14
|
+
entId = @entityStore.new_id
|
15
|
+
archetype = @archStore.get(archId)
|
16
|
+
archRowId = archetype.new_arch_row_id(entId)
|
17
|
+
@entityStore.spawn(entId, archId, archRowId)
|
18
|
+
for component in components
|
19
|
+
archetype.set_component(archRowId, component)
|
20
|
+
end
|
21
|
+
return entId
|
22
|
+
end
|
23
|
+
|
24
|
+
def get_component(entityId, componentType)
|
25
|
+
entity = @entityStore.get(entityId)
|
26
|
+
archetype = @archStore.get(entity.archId)
|
27
|
+
archetype.get_component(entity.archRow, componentType.object_id)
|
28
|
+
end
|
29
|
+
|
30
|
+
def kill(entityId)
|
31
|
+
entity = @entityStore.get entityId
|
32
|
+
@entityStore.kill(entityId)
|
33
|
+
archetype = @archStore.get(entity.archId)
|
34
|
+
archetype.remove_entity(entity.archRow)
|
35
|
+
end
|
36
|
+
|
37
|
+
def entity_count
|
38
|
+
return @entityStore.entity_count
|
39
|
+
end
|
40
|
+
|
41
|
+
def has_component(entityId, componentType)
|
42
|
+
entity = @entityStore.get(entityId)
|
43
|
+
archetype = @archStore.get(entity.archId)
|
44
|
+
return archetype.has_component(componentType.object_id)
|
45
|
+
end
|
46
|
+
|
47
|
+
def has_flag(entityId, flagId)
|
48
|
+
return @entityStore.has_flag entityId, flagId
|
49
|
+
end
|
50
|
+
|
51
|
+
def has_flags(entityId, *flagIds)
|
52
|
+
return flagIds.filter { |flagId| !@entityStore.has_flag entityId, flagId }.count == 0
|
53
|
+
end
|
54
|
+
|
55
|
+
def set_flag(entityId, flagId)
|
56
|
+
@entityStore.set_flag(entityId, flagId)
|
57
|
+
end
|
58
|
+
|
59
|
+
def remove_flag(entityId, flagId)
|
60
|
+
@entityStore.remove_flag(entityId, flagId)
|
61
|
+
end
|
62
|
+
|
63
|
+
include Query
|
64
|
+
end
|
metadata
ADDED
@@ -0,0 +1,186 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: kiwi-ecs
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.2
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Jonas Everaert
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2023-08-11 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description: |
|
14
|
+
# Kiwi
|
15
|
+
|
16
|
+
Kiwi is a versatile entity component system focussing on fast iteration and a nice api.
|
17
|
+
|
18
|
+
To get started, read the [usage guide](#usage) below.
|
19
|
+
|
20
|
+
[](https://github.com/Jomy10/kiwi-ecs-ruby/actions/workflows/tests.yml)
|
21
|
+
|
22
|
+
## Installation
|
23
|
+
|
24
|
+
This library is currently not on ruby gems, but will arrive shortly.
|
25
|
+
|
26
|
+
To use it now, simple `git clone https://github.com/jomy10/kiwi-ecs-ruby`.
|
27
|
+
|
28
|
+
Then incude the world.rb file in your ruby files like so:
|
29
|
+
|
30
|
+
```ruby
|
31
|
+
require_relative 'kiwi-ecs-ruby/src/world.rb'
|
32
|
+
```
|
33
|
+
|
34
|
+
## Usage
|
35
|
+
|
36
|
+
### The world
|
37
|
+
|
38
|
+
The world is the main object that controls the ecs.
|
39
|
+
|
40
|
+
```ruby
|
41
|
+
world = World.new
|
42
|
+
```
|
43
|
+
|
44
|
+
### Components
|
45
|
+
|
46
|
+
Creating a component is as simple as declaring a struct:
|
47
|
+
|
48
|
+
```ruby
|
49
|
+
Position = Struct.new :x, :y
|
50
|
+
```
|
51
|
+
|
52
|
+
Classes can also be used instead of structs
|
53
|
+
|
54
|
+
```ruby
|
55
|
+
class Velocity
|
56
|
+
attr_accessor :x
|
57
|
+
attr_accessor :y
|
58
|
+
end
|
59
|
+
```
|
60
|
+
|
61
|
+
### Entities
|
62
|
+
|
63
|
+
An entity is spawned with a set of components:
|
64
|
+
|
65
|
+
```ruby
|
66
|
+
entityId = world.spawn(Position.new(10, 10))
|
67
|
+
|
68
|
+
world.spawn(Position.new(3, 5), Velocity.new(1.5, 0.0))
|
69
|
+
```
|
70
|
+
|
71
|
+
The `world.spawn(*components)` function will return the id of the spawned entity.
|
72
|
+
|
73
|
+
Killing an entity can be done using `world.kill(entityId)`:
|
74
|
+
|
75
|
+
```ruby
|
76
|
+
world.kill(entityId)
|
77
|
+
```
|
78
|
+
|
79
|
+
### Systems
|
80
|
+
|
81
|
+
#### Queries
|
82
|
+
|
83
|
+
Queries can be constructed as follows:
|
84
|
+
|
85
|
+
```ruby
|
86
|
+
# Query all position componentss
|
87
|
+
world.query(Position) do |pos|
|
88
|
+
puts pos
|
89
|
+
end
|
90
|
+
|
91
|
+
# Query all entities having a position and a velocity component, and their entity ids
|
92
|
+
world.query_with_ids(Position, Velocity) do |id, pos, vel|
|
93
|
+
# ...
|
94
|
+
end
|
95
|
+
```
|
96
|
+
|
97
|
+
### Flags
|
98
|
+
|
99
|
+
Entities can be tagged using flags
|
100
|
+
|
101
|
+
#### Defining flags
|
102
|
+
|
103
|
+
A flag is an integer
|
104
|
+
|
105
|
+
```ruby
|
106
|
+
module Flags
|
107
|
+
Player = 0
|
108
|
+
Enemy = 1
|
109
|
+
end
|
110
|
+
```
|
111
|
+
|
112
|
+
#### Setting flags
|
113
|
+
|
114
|
+
```ruby
|
115
|
+
id = world.spawn
|
116
|
+
|
117
|
+
world.set_flag(id, Flags::Player)
|
118
|
+
```
|
119
|
+
|
120
|
+
#### Removing a flag
|
121
|
+
|
122
|
+
```ruby
|
123
|
+
world.remove_flag(id, Flags::Player)
|
124
|
+
```
|
125
|
+
|
126
|
+
#### Checking wether an entity has a flag
|
127
|
+
|
128
|
+
```ruby
|
129
|
+
world.has_flag(id, Flags::Player)
|
130
|
+
```
|
131
|
+
|
132
|
+
#### Filtering queries with flags
|
133
|
+
|
134
|
+
```ruby
|
135
|
+
# TODO
|
136
|
+
```
|
137
|
+
|
138
|
+
The `hasFlags` function is also available for when you want to check multiple flags.
|
139
|
+
|
140
|
+
## Road map
|
141
|
+
|
142
|
+
- [ ] System groups
|
143
|
+
|
144
|
+
## Contributing
|
145
|
+
|
146
|
+
Contributors are welcome to open an issue requesting new features or fixes or opening a pull request for them.
|
147
|
+
|
148
|
+
## License
|
149
|
+
|
150
|
+
The library is licensed under LGPLv3.
|
151
|
+
email:
|
152
|
+
executables: []
|
153
|
+
extensions: []
|
154
|
+
extra_rdoc_files: []
|
155
|
+
files:
|
156
|
+
- lib/arch_store.rb
|
157
|
+
- lib/archetype.rb
|
158
|
+
- lib/bitmap.rb
|
159
|
+
- lib/entity.rb
|
160
|
+
- lib/kiwi-ecs.rb
|
161
|
+
- lib/query.rb
|
162
|
+
- lib/world.rb
|
163
|
+
homepage: https://github.com/jomy10/kiwi-ecs-ruby
|
164
|
+
licenses:
|
165
|
+
- LGPL-3.0-or-later
|
166
|
+
metadata: {}
|
167
|
+
post_install_message:
|
168
|
+
rdoc_options: []
|
169
|
+
require_paths:
|
170
|
+
- lib
|
171
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
172
|
+
requirements:
|
173
|
+
- - ">="
|
174
|
+
- !ruby/object:Gem::Version
|
175
|
+
version: '0'
|
176
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
177
|
+
requirements:
|
178
|
+
- - ">="
|
179
|
+
- !ruby/object:Gem::Version
|
180
|
+
version: '0'
|
181
|
+
requirements: []
|
182
|
+
rubygems_version: 3.3.3
|
183
|
+
signing_key:
|
184
|
+
specification_version: 4
|
185
|
+
summary: An entity component system with a nice api, fit for a variety of use cases
|
186
|
+
test_files: []
|