kiwi-ecs 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
[![Tests](https://github.com/Jomy10/kiwi-ecs-ruby/actions/workflows/tests.yml/badge.svg)](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: []
|