Megadata is a library you can use to serialize/deserialize network game data.
This library will help you deal with:
npm install --save megadata
You will also need to make sure that the following configuration is set in your tsconfig.json:
{
"compilerOptions": {
"experimentalDecorators": true
}
}
shared/messages/index.ts
import megadata, { TypeDecorator } from 'megadata'
export enum TypeIds {
Join,
Leave
}
export const Type: TypeDecorator<TypeIds> = megadata(module)
We first create a list of message types as a const enum
and
list the messages we will send and receive in our game. We then
generate a @Type
decorator
shared/messages/types/Join.ts
import { Type, TypeIds } from '../'
import MessageType from 'megadata/classes/MessageType'
import Binary, { Uint32 } from 'megadata/classes/BinarySerializationFormat'
@Type(TypeIds.Join, Binary)
export default class Join extends MessageType {
@Uint32
public time: number
}
shared/messages/types/Leave.ts
import { Type, TypeIds } from '../'
import MessageType from 'megadata/classes/MessageType'
import Json from 'megadata/classes/JsonSerializationFormat'
@Type(TypeIds.Leave, Json)
export default class Leave extends MessageType {
public time: number
}
Next, we define what our Join
and Leave
messages type will look like, and
how we should serialize and deserialize it.
Megadata ships with two serialization formats:
You may notice that the Binary
serialization format requires additional annotations
this is required to define (and optimized) the size and speed of serialization and
deserialization.
server/classes/Player.ts
import Connection from '...'
import MessageEmitter from 'megadata/classes/MessageType'
import MessageType, { IMessageType, MessageTypeData } from 'megadata/classes/MessageType'
export class Player extends MessageEmitter {
constructor(private connection: Connection) {
connection.on('message', async (buffer: ArrayBuffer) => {
const message = MessageType.parse(buffer)
await this.emitAsync(message)
message.release()
})
}
public async function send<T extends MessageType>(type: IMessageType<T>, data: MessageTypeData<T>) {
const message = type.create<T>(data)
const buffer = message.pack()
await this.connection.write(buffer)
message.release()
}
}
Messages are recycled from an object pool to reduce the impact of garbage collection; therefore, it is important to remember to release messages back into the object pool once you are done with them.
server/classes/Game.ts
import Player from './Player'
import Join from 'shared/messages/types/Join.ts'
export default class Game {
// ...
public addPlayer(player: Player) {
player.on(Join, ({ time }) => console.log('Join time:', time))
player.on(Leave, ({ time }) => console.log('Leave time:', time))
}
}
Auto-loading uses require.context
, which is a webpack-specific
API. When using Megadata with auto-loading in Node.js, you will
therefore need to load the mock provided by the library.
ts-node -r megadata/register index.ts
shared/messages/index.ts
import megadata, { TypeDecorator } from 'megadata'
const types = require.context('./types/')
export enum TypeIds {
Join,
[...]
}
export const Type: TypeDecorator<TypeIds> = megadata(module, types)
The following code will dynamically load type classes on demand from
the shared/messages/types
folder. If no listeners were ever set to
listen for messages of this type, an Event.Unknown
event will be
emitted (see below).
server/classes/Player.ts
import MessageEmitter, { AutoloadEvents } from 'megadata/classes/MessageEmitter'
const events = require.context('../events/')
@AutoloadEvents(events)
export default class Player extends MessageEmitter {}
A given message emitter may end up handling a large number or events. Event
handlers auto-loading provides a mechanism for breaking event handling
down into event handler files that are auto-loaded on demand. In this
case, we will auto-load all events under server/events
.
server/events/Join.ts
import Player from '../classes/Player'
import Join from 'shared/messages/types/Join'
export default function (player: Player) {
player.on(Join, (message) => console.log('Received join event', message))
}
Event handler files export a single default function which will receive a message emitter instance; you may then set the even listeners according to your needs.
shared/messages/classes/CustomSerializationFormat.ts
import MessageType from './MessageType'
import SerializationFormat, { ISerializerFunctions } from './SerializationFormat'
export default class CustomSerializationFormat extends SerializationFormat {
public create<I, T extends MessageType>(id: I, size: number, attributes: any) {
return {
create: (...),
pack: (...),
unpack: (...),
} as ISerializerFunctions<I, T>
}
}
You can create your own custom serialization format. Above is a quick stub, but you should have a look at the default serialization formats provided by megadata.
MIT License. Copyright (c) Wizcorp Inc.