Message Producer
A message producer lets you build messages between ticks, prioritize updates to certain component types, and divide messages based on certain criteria (like a maximum size).
Building Messages#
Message producers maintain a queue of messages. New messages are enqueued when the most current message reaches a maximum byte length (default Infinity
). The maxmum byte length is specified using the options
argument to createMessageProducer
.
The easiest way to consume a message producer in a system is to wrap an instance in a ref:
const useProducer = createRef(
() => createMessageProducer({ maxByteLength: 1000 }), // limit each message to 1kb
)
Message producers expose methods that correspond to each of the operations described in the Javelin protocol.
const producer = useProducer()
producer.attach(e, [c])
producer.update(e, [c])
producer.detach(e, [b])
producer.destroy(e)
The take
method will dequeue a message, or null, if no changes were written.
const message = producer.take() // Message | null
useMonitor
can be used to conveniently write attach and destroy operations.
const net = () => {
const { attach, destroy, take } = useProducer().value
// write attach/destroy operations for players
useMonitor(players, attach, destroy)
// every 50ms
if (useInterval(1 / 20) * 1000) {
// dequeue and encode a message
const encoded = encode(take())
// and send it to each client
send(encoded)
}
}
Component Model#
take
accepts a single boolean parameter that instructs the message producer to include a serialized component model in the next message. This must be done at least once, usually in the first message sent to a client. For example:
const getInitialMessage = () => {
producer.attach(...)
// ...
return producer.take(true) // include component model
}
The component model does not have to be included with each message. MessageHandler
will re-use the last encountered component model if a message is not self-describing.
Sending Entity Changes#
Below is example that demonstrates how you might write attach/detach operations while an entity continues to match a query:
const players = createQuery(Player)
const burning = createQuery(Player, Burn)
const net = () => {
const { destroy, attach, detach, take } = useProducer().value
// spawn newly created players on client
useMonitor(players, attach, destroy)
// a burn effect may be attached/detached frequently, so we control the
// synchronization with a separate monitor
useMonitor(
burning,
(e, [, b]) => b && attach(e, b),
(e, [, b]) => b && detach(e, b),
)
if (useInterval(1 / 20) * 1000) {
send(encode(take()))
}
}
Updating Components#
Update#
Two strategies exist for synchronizing component state: updates and patches. Updates send the entire component state, which is simple to implement but uses more bandwidth.
transforms((e, [t]) => producer.update(e, [t]))
MessageHandler
simply uses Object.assign
to apply component updates in a message to their local counterparts.
Patch#
A patch operation effeciently serializes fields contained in a ChangeSet
component.
import { set } from "@javelin/track"
trackedTransforms((e, [t, changes]) => {
set(t, changes, "x", 3)
set(t, changes, "y", 4)
})
A patch operation can then be written to a message producer patch
:
import { reset } from "@javelin/track"
trackedTransforms((e, [t, changes]) => {
producer.patch(e, changes)
reset(changes)
})