Skip to content

Commit

Permalink
feat: world.snapshot (#145)
Browse files Browse the repository at this point in the history
* feat(world): snapshots

* docs: styling and content updates
  • Loading branch information
3mcd authored Mar 4, 2021
1 parent 861afcb commit 32b7b53
Show file tree
Hide file tree
Showing 47 changed files with 3,568 additions and 651 deletions.
5 changes: 2 additions & 3 deletions docs-src/config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,13 @@ compile_sass = true
# Whether to build a search index to be used later on by a JavaScript library
build_search_index = true

highlight_code = true
highlight_theme = "snow-light"

[markdown]
highlight_code = true

[extra]
# Put all your custom variables here
theme = "book"
book_numbered_chapters = false
book_numbered_chapters = true
library_version = "0.19.4"
repo_url = "https://github.com/3mcd/javelin"
47 changes: 31 additions & 16 deletions docs-src/content/ecs/_index.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,56 +5,71 @@ sort_by = "weight"
insert_anchor_links = "right"
+++

## What's an ECS?
This section aims to serve as a quick primer on Entity Component Systems (ECS) and how to think in ECS. The goal is not to belittle other methods of building games or make ECS seem like a panacea, because ECS does come with its own challenges. Godot has a great article about [why Godot does not use ECS](https://godotengine.org/article/why-isnt-godot-ecs-based-game-engine) that I recommend you read if you are trying to determine whether or not you should use Javelin.

<aside>
<p>
<strong>Note</strong> — this section contains pseudo-code. Skip to the <a href="/ecs/world">Hello World</a> section if you're ready for real examples.
<strong>Tip</strong> — this section contains pseudo-code. Skip to the <a href="/ecs/world">Hello World</a> section if you're ready for real examples.
</p>
</aside>

Entity data and behavior is often architected using class heirarchies in game development. Take the following example, where a `Player` class extends a physics `Body` class to enhance players with physics properties:
## What's an ECS?

A best practice in OOP game development is to favor composition over inheritance when designing game data and behavior. Take the following example, where a `Player` class accepts `Body` and `Input` objects to enhance players with physics properties and input control:

```typescript
class Body {
readonly velocity = { x: 0, y: 0 }
}

class Player extends Body {
class Player {
body: Body
input: Input

constructor(public input: Input) {}
constructor(public body: Body, public input: Input) {}

jump() {
this.velocity[1] += 1
this.body.velocity[1] += 1
}

update() {
if (this.input.key("space")) {
this.jump()
}
}
}

const input = new Input()
const player = new Player(input)
const player = new Player(new Body(), new Input())

setInterval(() => {
if (input.isSpacebarPressed()) {
// apply force to launch player into the air
player.jump()
}
player.update()
}, 16.66666)
```

When the player presses the spacebar on their keyboard, `player.jump()` is called, and the physics body jumps! Easy enough.

What if a player wants to spectate our game instead of controlling a character? In that scenario, it would be unnecessary for `Player` to extend `Body`, and we'd either need to modify the inheritance structure, or write defensive code to ensure that spectators shouldn't touch the physics simulation.
What if a player wants to spectate our game instead of controlling a character? In that scenario, it would be unnecessary for `Player` to care about `Body`, and we'd need to write code that makes `Body` an optional dependency of `Player`, e.g.

Data and behavior are separate concerns in an ECS. High-cohesion game objects are substituted with three distinct concerns: (1) **components** – game data, (2) **entities** – game objects (like a tree, chest, or spawn position), and (3) **systems** – game behavior.
```ts
body?: Body
...
jump() {
this.body?.velocity[1] += 1
}
```

If there are many states/dependencies a player can have (e.g. spectating, driving a vehicle, etc.), our `Player` class might explode with complexity. Going even further, `Player` would need to define all it's possible dependencies in advance, making runtime composition difficult or even impossible.

## Parts of an ECS

Data and behavior are separate concerns in an ECS. High-cohesion game objects are substituted with three distinct concerns: (1) **components** – game data, (2) **entities** – game objects (like a tree, chest, or spawn position), and (3) **systems** – game behavior. As we'll see, this architecture enables runtime composition of behavior that would be tricky to implement in the example above.
### Components

In an ECS, components are typically plain objects that contain data and no methods. Ideally all game state lives in components.

```
Player { name: string }
Body { velocity: [number, number] }
Input { space: boolean }
Player { name: string }
```

### Entities
Expand Down
2 changes: 1 addition & 1 deletion docs-src/content/ecs/components.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,6 @@ world.registerComponentType(Position, 10000)

<aside>
<p>
<strong>Note</strong> — the configured or default pool size will be used if a component type is encountered by <code>world.component()</code> prior to manual registration.
<strong>Tip</strong> — the configured or default pool size will be used if a component type is encountered by <code>world.component()</code> prior to manual registration.
</p>
</aside>
6 changes: 3 additions & 3 deletions docs-src/content/ecs/effects.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ const timer = createEffect(world => {

<aside>
<p>
<strong>Note</strong> — effects in Javelin have some similarities to React effects. They are executed each update (tick) and read/modify closed-over variables. In a way, Javelin's effects are a combination of React's <code>useEffect</code> and <code>useRef</code>.
<strong>Tip</strong> — effects in Javelin have some similarities to React effects. They are executed each update (tick) and read/modify closed-over variables. In a way, Javelin's effects are a combination of React's <code>useEffect</code> and <code>useRef</code>.
</p>
</aside>

Expand Down Expand Up @@ -99,7 +99,7 @@ const sys_fibonacci = () => {

<aside>
<p>
<strong>Note</strong> — using effects to store system state may bother the ECS purist, but it's undeniably convenient and practical, especially for simple cases where state wouldn't need to be serialized or shared with another system.
<strong>Tip</strong> — using effects to store system state may bother the ECS purist, but it's undeniably convenient and practical, especially for simple cases where state wouldn't need to be serialized or shared with another system.
</p>
</aside>

Expand Down Expand Up @@ -144,7 +144,7 @@ Some useful effects are included with the core ECS package. A few are outlined b

<aside>
<p>
<strong>Note</strong> — check the source code of this page to see a few effects in action.
<strong>Tip</strong> — check the source code of this page to see a few effects in action.
</p>
</aside>

Expand Down
4 changes: 2 additions & 2 deletions docs-src/content/ecs/entities.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ const entity = world.spawn(player, health)

<aside>
<p>
<strong>Note</strong> — although entities are simply auto-incrementing integers (starting at <code>0</code>), they should be treated as opaque values.
<strong>Tip</strong> — although entities are simply auto-incrementing integers (starting at <code>0</code>), they should be treated as opaque values.
</p>
</aside>

Expand Down Expand Up @@ -49,7 +49,7 @@ world.tick()

<aside>
<p>
<strong>Note</strong> — using <code>world.attach</code> and <code>world.detach</code> to build entities is slower than <code>world.spawn(components)</code> because the components of the affected entity must be relocated in memory each time the entity's archetype changes.
<strong>Tip</strong> — using <code>world.attach</code> and <code>world.detach</code> to build entities is slower than <code>world.spawn(components)</code> because the components of the affected entity must be relocated in memory each time the entity's archetype changes.
</p>
</aside>

Expand Down
10 changes: 2 additions & 8 deletions docs-src/content/ecs/systems.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,12 +45,6 @@ world.addSystem(sys_render)

When `world.tick()` is called, each system is executed in the order that it was registered.

<aside>
<p>
<strong>Note</strong> — a prefix like <code>sys_</code> can help distinguish systems from normal functions. But use whatever naming convention you like!
</p>
</aside>

Systems have a signature of `(world: World<T>) => void`, where the first argument is the world that is currently mid-tick. A single value can be passed to `world.tick(data)`, which is then available in each system via `world.state.currentTickData`. Often times this value holds the amount of time that has elapsed since the previous tick, but it can be any value.

The following is a world that will log the time elapsed since the last tick at around 60Hz:
Expand Down Expand Up @@ -79,7 +73,7 @@ setInterval(() => {

<aside>
<p>
<strong>Note</strong> — maintaining state using tick <code>data</code> is comparable to using global variables. Consider moving this state into a singleton component. Or, if you need inter-system communication, you can pass messages using topics, which are discussed in the <a href="/ecs/topics">Topics</a> section.
<strong>Tip</strong> — maintaining state using tick <code>data</code> is comparable to using global variables. Consider moving this state into a singleton component. Or, if you need inter-system communication, you can pass messages using topics, which are discussed in the <a href="/ecs/topics">Topics</a> section.
</p>
</aside>

Expand Down Expand Up @@ -119,7 +113,7 @@ In order to mutate game state you'll need access to the `World` that called the

The world that is currently executing a tick is passed as the system's first argument:

```
```ts
function sys_munch_doritos(world: World<number>) {
console.log(world.state.currentTickData) // logs 0.1666666667
}
Expand Down
2 changes: 1 addition & 1 deletion docs-src/content/ecs/topics.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ Sometimes messages should be handled as quickly as possible, like when processin

<aside>
<p>
<strong>Note</strong> — System registration order matters when using <code>pushImmediate</code>. Since the messages will be thrown away at the end of the tick, any systems upstream from the one that used <code>pushImmediate</code> will never have the opportunity to read the message.
<strong>Tip</strong> — System registration order matters when using <code>pushImmediate</code>. Since the messages will be thrown away at the end of the tick, any systems upstream from the one that used <code>pushImmediate</code> will never have the opportunity to read the message.
</p>
</aside>

Expand Down
2 changes: 1 addition & 1 deletion docs-src/content/ecs/world.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ weight = 2

<aside>
<p>
<strong>Note</strong> — the following chapters assume that you are familiar with basic ECS concepts discussed in the <a href="/ecs">opening section</a>.
<strong>Tip</strong> — the following chapters assume that you are familiar with basic ECS concepts discussed in the <a href="/ecs">opening section</a>.
</p>
</aside>

Expand Down
5 changes: 3 additions & 2 deletions docs-src/content/introduction/_index.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,8 @@ const effects = {
}),
click: Javelin.createEffect(() => {
const state = { clicked: false, coords: null }
document.addEventListener("click", event => {

canvas.addEventListener("click", event => {
state.clicked = true
state.coords = relMouseCoords(canvas, event)
})
Expand Down Expand Up @@ -158,7 +159,7 @@ const sys_spawn = world => {
spawnWormhole(
Math.random() * canvas.width,
Math.random() * canvas.height,
Math.random() * 60
Math.max(10, Math.random() * 60)
)
}
}
Expand Down
2 changes: 1 addition & 1 deletion docs-src/content/introduction/installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ The package.json `module` field points to the ESM build, which will be automatic
### CommonJS

<aside>
<p><strong>Note</strong> — this build does not support tree shaking.</p>
<p><strong>Tip</strong> — this build does not support tree shaking.</p>
</aside>

**Path**: `dist/cjs/index.js`
Expand Down
18 changes: 9 additions & 9 deletions docs-src/sass/_content.scss
Original file line number Diff line number Diff line change
Expand Up @@ -79,11 +79,12 @@
}

pre {
overflow: auto;
display: block;
white-space: pre;
max-width: calc(100vw - 4rem);
padding: 1rem;

span {
white-space: pre-wrap;
}
border-radius: 0;
}

blockquote {
Expand Down Expand Up @@ -127,16 +128,15 @@
}

aside {
color: #8a6c3c;
background: #ffe8c2;
border: 1px solid #d6d8d8;
border-radius: 6px;
font-size: 0.8rem;
padding: 1rem;
margin: 1.7rem 0;

code {
color: #8a6c3c;
background: #fcddad;
border: 1px solid #d1b07a;
background: #e6e8e8;
border: 1px solid #d6d8d8;
}

p {
Expand Down
5 changes: 3 additions & 2 deletions docs-src/sass/_navigation.scss
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,16 @@
overflow-y: auto;
flex: 1 0 auto;
color: #425461;
padding-top: 8px;
padding-top: 16px;
z-index: 1;
position: static;
margin-bottom: 1.7rem;
margin-bottom: 1.7rem 0;
top: 0;

@include min-screen($breakpoint--mobile) {
position: sticky;
flex: 0 0 #{$sidebar-width};
margin-top: calc(3.8rem + 0.333333rem);
}

h1 {
Expand Down
2 changes: 1 addition & 1 deletion docs-src/sass/book.scss
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ h3,
h4,
h5,
h6 {
color: #88669e;
color: #444;
}

code {
Expand Down
Loading

0 comments on commit 32b7b53

Please sign in to comment.