dcl-npc-toolkit-ai-version
Version:
A collection of tools for creating Non-Player-Characters (NPCs). These are capable of having conversations with the player, and play different animations. AI usage is added atop of it
245 lines (175 loc) • 7.28 kB
Markdown
<div align="center">
<img src="logo.png?raw=true" />
<br>
<br>
<p>
An incremental binary state serializer with delta encoding for games.<br>
Although it was born to be used on <a href="https://github.com/colyseus/colyseus">Colyseus</a>, this library can be used as standalone.
</p>
</div>
## Defining Schema
As Colyseus is written in TypeScript, the schema is defined as type annotations inside the state class. Additional server logic may be added to that class, but client-side generated (not implemented) files will consider only the schema itself.
```typescript
import { Schema, type, ArraySchema, MapSchema } from '@colyseus/schema';
export class Player extends Schema {
@type("string")
name: string;
@type("number")
x: number;
@type("number")
y: number;
}
export class State extends Schema {
@type('string')
fieldString: string;
@type('number') // varint
fieldNumber: number;
@type(Player)
player: Player;
@type([ Player ])
arrayOfPlayers: ArraySchema<Player>;
@type({ map: Player })
mapOfPlayers: MapSchema<Player>;
}
```
See [example](test/Schema.ts).
## Supported types
## Primitive Types
| Type | Description | Limitation |
|------|-------------|------------|
| string | utf8 strings | maximum byte size of `4294967295` |
| number | auto-detects `int` or `float` type. (extra byte on output) | `0` to `18446744073709551615` |
| boolean | `true` or `false` | `0` or `1` |
| int8 | signed 8-bit integer | `-128` to `127` |
| uint8 | unsigned 8-bit integer | `0` to `255` |
| int16 | signed 16-bit integer | `-32768` to `32767` |
| uint16 | unsigned 16-bit integer | `0` to `65535` |
| int32 | signed 32-bit integer | `-2147483648` to `2147483647` |
| uint32 | unsigned 32-bit integer | `0` to `4294967295` |
| int64 | signed 64-bit integer | `-9223372036854775808` to `9223372036854775807` |
| uint64 | unsigned 64-bit integer | `0` to `18446744073709551615` |
| float32 | single-precision floating-point number | `-3.40282347e+38` to `3.40282347e+38`|
| float64 | double-precision floating-point number | `-1.7976931348623157e+308` to `1.7976931348623157e+308` |
### Declaration:
#### Primitive types (`string`, `number`, `boolean`, etc)
```typescript
@type("string")
name: string;
@type("int32")
name: number;
```
#### Custom `Schema` type
```typescript
@type(Player)
player: Player;
```
#### Array of custom `Schema` type
```typescript
@type([ Player ])
arrayOfPlayers: ArraySchema<Player>;
```
#### Array of a primitive type
You can't mix types inside arrays.
```typescript
@type([ "number" ])
arrayOfNumbers: ArraySchema<number>;
@type([ "string" ])
arrayOfStrings: ArraySchema<string>;
```
#### Map of custom `Schema` type
```typescript
@type({ map: Player })
mapOfPlayers: MapSchema<Player>;
```
#### Map of a primitive type
You can't mix types inside maps.
```typescript
@type({ map: "number" })
mapOfNumbers: MapSchema<number>;
@type({ map: "string" })
mapOfStrings: MapSchema<string>;
```
### Backwards/forwards compability
Backwards/fowards compatibility is possible by declaring new fields at the
end of existing structures, and earlier declarations to not be removed, but
be marked `@deprecated()` when needed.
This is particularly useful for native-compiled targets, such as C#, C++,
Haxe, etc - where the client-side can potentially not have the most
up-to-date version of the schema definitions.
### Reflection
The Schema definitions can encode itself through `Reflection`. You can have the
definition implementation in the server-side, and just send the encoded
reflection to the client-side, for example:
```typescript
import { Schema, type, Reflection } from "@colyseus/schema";
class MyState extends Schema {
@type("string")
currentTurn: string;
// more definitions relating to more Schema types.
}
// send `encodedStateSchema` across the network
const encodedStateSchema = Reflection.encode(new MyState());
// instantiate `MyState` in the client-side, without having its definition:
const myState = Reflection.decode(encodedStateSchema);
```
### Data filters
On the example below, considering we're making a card game, we are filtering the cards to be available only for the owner of the cards, or if the card has been flagged as `"revealed"`.
```typescript
import { Schema, type, filter } from "@colyseus/schema";
export class State extends Schema {
@filterChildren(function(client: any, key: string, value: Card, root: State) {
return (value.ownerId === client.sessionId) || value.revealed;
})
@type({ map: Card })
cards = new MapSchema<Card>();
}
```
## Limitations and best practices
- Each `Schema` structure can hold up to `64` fields. If you need more fields, use nested structures.
- `NaN` or `null` numbers are encoded as `0`
- `null` strings are encoded as `""`
- `Infinity` numbers are encoded as `Number.MAX_SAFE_INTEGER`
- Multi-dimensional arrays are not supported.
- Items inside Arrays and Maps must be all instance of the same type.
- `@colyseus/schema` encodes only field values in the specified order.
- Both encoder (server) and decoder (client) must have same schema definition.
- The order of the fields must be the same.
- Avoid manipulating indexes of an array. This result in at least `2` extra bytes for each index change. **Example:** If you have an array of 20 items, and remove the first item (through `shift()`) this means `38` extra bytes to be serialized.
## Generating client-side schema files (for strictly typed languages)
> If you're using JavaScript or LUA, there's no need to bother about this.
> Interpreted programming languages are able to re-build the Schema locally through the use of `Reflection`.
You can generate the client-side schema files based on the TypeScript schema definitions automatically.
```
# C#/Unity
schema-codegen ./schemas/State.ts --output ./unity-project/ --csharp
# C/C++
schema-codegen ./schemas/State.ts --output ./cpp-project/ --cpp
# Haxe
schema-codegen ./schemas/State.ts --output ./haxe-project/ --haxe
```
## Benchmarks:
| Scenario | `@colyseus/schema` | `msgpack` + `fossil-delta` |
|---|---|---|
| Initial state size (100 entities) | 2671 | 3283 |
| Updating x/y of 1 entity after initial state | 9 | 26 |
| Updating x/y of 50 entities after initial state | 342 | 684 |
| Updating x/y of 100 entities after initial state | 668 | 1529 |
## Decoder implementations
Decoders for each target language are located at [`/decoders/`](decoders). They have no third party dependencies.
## Why
Initial thoghts/assumptions, for Colyseus:
- little to no bottleneck for detecting state changes.
- have a schema definition on both server and client
- better experience on staticaly-typed languages (C#, C++)
- mutations should be cheap.
Practical Colyseus issues this should solve:
- Avoid decoding large objects that haven't been patched
- Allow to send different patches for each client
- Better developer experience on statically-typed languages
## Inspiration:
- [Protocol Buffers](https://developers.google.com/protocol-buffers)
- [flatbuffers](https://google.github.io/flatbuffers/flatbuffers_white_paper.html)
- [schemapack](https://github.com/phretaddin/schemapack/)
- [avro](https://avro.apache.org/docs/current/spec.html)
## License
MIT