Skip to content

Commit

Permalink
Merge pull request #41 from ayebear/v1
Browse files Browse the repository at this point in the history
1.0.0-alpha1
  • Loading branch information
ayebear authored Oct 13, 2020
2 parents 83a19e9 + f5f69c7 commit 2afae6d
Show file tree
Hide file tree
Showing 7 changed files with 463 additions and 355 deletions.
183 changes: 94 additions & 89 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,15 @@

### Table Of Contents

* [About](#about)
* [Features](#features)
* [Terminology](#terminology)
* [License](#license)
* [Author](#author)
* [Instructions](#instructions)
* [Setup](#setup)
* [Documentation](#documentation)
* [Examples](#examples)
- [About](#about)
- [Features](#features)
- [Terminology](#terminology)
- [License](#license)
- [Author](#author)
- [Instructions](#instructions)
- [Setup](#setup)
- [Documentation](#documentation)
- [Examples](#examples)

## About

Expand All @@ -24,34 +24,37 @@ This entity system is designed to be as simple as possible, while still having u

### Features

* **High performance indexing options**
* SimpleIndex (Default): O(1) component add/remove, O(m) query time
* Where `m` is the smallest size component index
* MemoizedQueryIndex: O(q) component add/remove, O(1) average query time (memoized), O(n) worst query time (initial)
* Where `q` is the total number of memoized queries
* And `n` is the total number of entities
* *Note: Above time complexities are amortized assuming the number of components used is a known constant*
* Can also write your own and pass it to the World constructor! Needs clear, add, remove, and query.
* **No formal declarations required**
* Can create components and entities in a world and query on them, without needing to define structured systems and components
* **Strings as component keys**
* No need to manually track component keys like many libraries
* **JSON serialization**
* Useful for save data and networked applications
* **Prototypes**
* Allows entity definitions to be data-driven, outside of code
- **Simple query syntax**
- `world.each('a', 'b', ({a, b}) => { a.foo = b.bar })`
- See the examples below for more advanced usage, or the [reference docs](https://ayebear.com/picoes/class/src/world.js~World.html#instance-method-each)
- **No formal declarations required**
- Can create components and entities in a world and query on them, without needing to define structured systems and components
- **Strings as component keys**
- No need to manually define component keys, or manually include component classes to use them
- **Automatic dependency injection for systems**
- No need to pass state to each system, can have a single context that gets injected into all systems automatically
- **High performance indexing options**
- SimpleIndex (Default): O(1) component add/remove, O(m) query time
- Where `m` is the smallest size component index
- MemoizedQueryIndex: O(q) component add/remove, O(1) average query time (memoized), O(n) worst query time (initial)
- Where `q` is the total number of memoized queries
- And `n` is the total number of entities
- _Note: Above time complexities are amortized assuming the number of components used is a known constant_
- Can also write your own and pass it to the World constructor! Needs clear, add, remove, and query.
- **Prototypes**
- Allows entity definitions to be data-driven, outside of code

### Terminology

* **Component:** Holds some related data
* Example: Position, Velocity, Health
* **Entity:** Refers to a collection of components
* Example: Position + Health could represent a player
* **Prototype:** A template of components used for creating entities
* Example: Player could contain Position, Velocity, and Health
* **System:** Logic loop that processes entities
* Example: Movement system which handles positions and velocities
* **World:** Lets you register components, systems, and prototypes in a self-contained object - which avoids the use of singletons. This is also where you can create entities from.
- **Component:** Holds some related data
- Example: Position, Velocity, Health
- **Entity:** Refers to a collection of components
- Example: Position + Health could represent a player
- **Prototype:** A template of components used for creating entities
- Example: Player could contain Position, Velocity, and Health
- **System:** Logic loop that processes entities
- Example: Movement system which handles positions and velocities
- **World:** Lets you register components, systems, and prototypes in a self-contained object - which avoids the use of singletons. This is also where you can create entities from.

### License

Expand Down Expand Up @@ -81,7 +84,7 @@ npm i -D picoes

### Documentation

[PicoES Documentation](http://ayebear.com/picoes)
[PicoES Documentation](https://ayebear.com/picoes)

### Examples

Expand All @@ -92,18 +95,18 @@ npm i -D picoes
const { World } = require('picoes')

// Create a world to store entities in
let world = new World()
const world = new World()

// Create player with anonymous health component
let player = world.entity().set('health', { value: 100 })
const player = world.entity().set('health', { value: 100 })

// Create enemies
world.entity().set('damages', 10)
world.entity().set('damages', 30)

// Apply damage
world.every(['damages'], amount => {
player.get('health').value -= amount
world.each('damages', ({ damages }) => {
player.get('health').value -= damages
})

// Player now has reduced health
Expand All @@ -113,68 +116,75 @@ console.assert(player.get('health').value === 60)
#### Full component and system definitions

```javascript
// import { World } from 'picoes'
const { World } = require('picoes')
// const { World } = require('picoes')
import { World } from 'picoes'

// Create a world to store entities in
let world = new World()

// Define position and velocity components
world.component('position', class {
onCreate(entity, x = 0, y = 0) {
this.x = x
this.y = y
}
})
const world = new World()

world.component('velocity', class {
onCreate(entity, x = 0, y = 0) {
// Define and register components
class Vec2 {
constructor(x = 0, y = 0) {
this.x = x
this.y = y
}
})

// Create movable prototype
world.prototype({
Movable: {
position: {},
velocity: {}
}
world.component('position', Vec2)
world.component('velocity', Vec2)
world.component('health', class {
constructor(start = 100) {
this.value = start
}
})

// Define movement system
// Note: All system methods are optional, but they are included here to show the flow
world.system(['position', 'velocity'], class {
constructor() {
console.log('constructor() called')
// Example of using onCreate and onRemove
world.component('sprite', class {
onCreate(entity, texture) {
// Entity is always passed as first param to onCreate
// Remaining parameters are from entity.set()
this.container = entity.get('gameContainer')
this.sprite = new Sprite(texture)
this.container.add(this.sprite)
}

initialize() {
console.log('initialize() called')
onRemove() {
this.container.remove(this.sprite)
}
})

pre() {
console.log('pre() called')
// Define systems
// Log statements are to show flow order below
class MovementSystem {
constructor(...args) {
console.log('constructor() called with args:', ...args)
}

every(position, velocity, entity) {
console.log(`every() called for entity ${entity.id}`)
position.x += velocity.x
position.y += velocity.y
run(dt) {
console.log(`run(${dt}) called`)
world.each('position', 'velocity', ({ position, velocity }, entity) => {
console.log(`each() called for entity ${entity.id}`)
position.x += velocity.x * dt
position.y += velocity.y * dt
})
}
}

post() {
console.log('post() called')
}
})
// Register systems
world.system(MovementSystem, 'extra', 'args')

// Create entity without prototype
let entityA = world.entity().set('position').set('velocity')
const entityA = world.entity().set('position').set('velocity')
console.assert(entityA.has('position'))
console.assert(entityA.has('velocity'))

// Create entity with prototype (results are the same as above)
let entityB = world.entity('Movable')
world.prototype({
Movable: {
position: {},
velocity: {},
},
})
const entityB = world.entity('Movable')
console.assert(entityB.has('position'))
console.assert(entityB.has('velocity'))

Expand All @@ -188,11 +198,8 @@ entityA.get('position').x = 100
entityA.update('velocity', { x: 10, y: 10 })
entityB.update('velocity', { x: -10, y: -10 })

// Initialize systems
world.initialize()

// Run systems
world.run()
// Run systems (pass one second for dt)
world.run(1.0)

// Since the movement system ran once, the positions changed by the amount of their velocity
console.assert(entityA.get('position').x === 110)
Expand All @@ -204,10 +211,8 @@ console.assert(entityB.get('position').y === 90)
Expected output:

```
constructor() called
initialize() called
pre() called
every() called for entity 1
every() called for entity 2
post() called
constructor() called with args: extra args
run(1) called
each() called for entity 1
each() called for entity 2
```
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "picoes",
"version": "0.5.3",
"version": "1.0.0-alpha1",
"description": "Pico Entity System for JavaScript (ES6).",
"main": "./index.js",
"scripts": {
Expand Down
18 changes: 9 additions & 9 deletions src/benchmarks.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,21 +51,21 @@ function runBenchmarks() {
let systems = 50
let results = null
for (let i = 0; i < systems; ++i) {
results = world.every(['compA', 'compB'])
results = world.every(['compA', 'compB', 'compC', 'comp' + i])
results = world.every(['compA', 'compC', 'comp5', 'comp6', 'comp7'])
results = world.every(['compB', 'compC', 'comp10'])
results = world.every(['compB', 'compC', 'comp30'])
results = world.every(['compC', 'comp30'])
results = world.every(['compC'])
results = world.each(['compA', 'compB'])
results = world.each(['compA', 'compB', 'compC', 'comp' + i])
results = world.each(['compA', 'compC', 'comp5', 'comp6', 'comp7'])
results = world.each(['compB', 'compC', 'comp10'])
results = world.each(['compB', 'compC', 'comp30'])
results = world.each(['compC', 'comp30'])
results = world.each(['compC'])

// Simulate a real system
world.every(['comp45', 'compB'], (f, b) => b.val += f)
world.each(['comp45', 'compB'], ({comp45: f, compB: b}) => b.val += f)
}

// Destroy all numbered components
for (let i = 0; i < systems; ++i) {
world.every(['comp' + i], (c, e) => e.destroy())
world.each('comp' + i, (_, e) => e.destroy())
}
}

Expand Down
18 changes: 11 additions & 7 deletions src/entity.js
Original file line number Diff line number Diff line change
Expand Up @@ -107,9 +107,11 @@ class Entity {
* @example
* entity.set('anotherAnonymousComponent', 'Any type of any value')
*
* @param {string} component - The component name to create. If there is a registered component for this name, then
* its constructor will be called with (entity, ...args) and an object of that type will be created. The onCreate method
* gets called after the component is added to the entity. This method also gets passed the same parameters.
* @param {string} component - The component name to create. If there is a registered component for this name,
* then its constructor will be called with (...args) and an object of that type will be created. The parent
* entity reference gets injected into registered components after they are constructed. The onCreate method
* gets called after the component is added to the entity, and after the entity is injected. This method
* also gets passed the same parameters.
* @param {...Object} [args] - The arguments to forward to the registered component type. If the component type is
* registered, then only the first additional argument will be used as the value of the entire component.
*
Expand All @@ -118,8 +120,10 @@ class Entity {
set(component, ...args) {
if (this.valid() && component in this.world.components) {
// Create component and store in entity
// Note: The entity parameter is dangerous to use, since the component hasn't been added to the entity yet
this.data[component] = new this.world.components[component](this, ...args)
this.data[component] = new this.world.components[component](...args)

// Inject parent entity into component
this.data[component].entity = this
} else if (args.length > 0) {
// Use first argument as component value
this.data[component] = args[0]
Expand All @@ -133,8 +137,8 @@ class Entity {
this.world.index.add(this, component)
}

// Call custom onCreate to initialize component, pass the entity (this), and any additional arguments passed into set()
invoke(this.data[component], 'onCreate', this, ...args)
// Call custom onCreate to initialize component, and any additional arguments passed into set()
invoke(this.data[component], 'onCreate', ...args)

return this
}
Expand Down
Loading

0 comments on commit 2afae6d

Please sign in to comment.