Example Client
Bundle with esbuild, webpack, bun build, or another bundler.
import EchoD from 'echo-d';Prepare the events object to be used with Echo-D.
import { events } from './events.js';Include the listenToHost and sendToHost methods from the transport layer.
import { listToHost, sendToHost } from './transport.js';Include the listenToControls method from the controller layer.
import { listenToControls } from './client/controls.js';Need to be able to update the views when Echo-D is updated.
import { updateRender } from './update.js';Delare an onUpdate function to be used by Echo-D when it is updated.
This will call the updateRender function to update the views, but it
should be throttled to 30 frames per second. So that it doesn’t occur too often.
let renderTimer = nullfunction onUpdate (message) { if ( renderTimer !== null ) { clearTimeout( renderTimer ); } const update = () => updateRender( echoD, events ) renderTimer = setTimeout(update, 1000 / 30 );}Create an instance of Echo-D with the events object and the echoOptions object.
const echoOptions = { compressStringsAsInts: true, types: { position: [ 'f32', 3 ] }, responder: sendToHost, updateOptions: { mask: { entity: true, component: true }, validkeys: { } }, onUpdate};const echoD = new EchoD( { events }, echoOptions );Listen to Host
Listen to host and handle incoming messages with Echo-D.
const ctx = { id: null };listenToHost( echoD, ctx );List to controls and send inputs to the host.
listenToControls( echoD, ctx );Seupt a handler to remove the actor when the window is closed or refreshed.
window.addEventListener('beforeunload', ( event ) => { echoD.removeActor( ctx.id ); echoD.updater( );});Create react three fiber Canvas in document root
import React from 'react';import { createRoot } from 'react-dom/client';import { Render } from './client/Render';
const root = document.getElementById( 'root' );createRoot( root ).render( <Render events={ events } /> );Actor Controller
Setup event listener on the window to listen for keydown events. Move the actor based on the keydown event. Then send the input to the host.
export function listenToControls ( echoD, ctx ) { const handler = ({ code }) => { if ( !ctx.id ) { return; } const move = { x: 0, y: 0, z: 0 } const step = 0.1; switch (code) { case 'KeyW': case 'ArrowUp': move.y = step; break; case 'KeyS': case 'ArrowDown': move.y = -step; break; case 'KeyA': case 'ArrowLeft': move.x = -step; break; case 'KeyD': case 'ArrowRight': move.x = step; break; case 'KeyQ': move.z = step; break; case 'KeyE': move.z = -step; break; default: return; } echoD.actorInput( ctx.id, move ); echoD.updater({ updateOptions: { mask: { actors: true, entities: true, components: true, symbols: true } } } ); };
window.document.addEventListener( 'keydown', handler ); return () => { window.document.removeEventListener( 'keydown', handler ); };}On Init
When the client is initialized, it should send a message to the host to get the symbols, actors, entities, and components. Then it should spawn an actor for the client.
import { nanoid } from 'nanoid';export function onInit ( echoD, sendToHost ) { sendToHost( [ 'symbols' ] ); sendToHost( [ 'actors' ] ); sendToHost( [ 'entities' ] ); sendToHost( [ 'components' ] ); const id = nanoid( ); echoD.spawnActor( id ); echoD.updater( ); return id;}Client Transport Layer
Choose a network transport layer such as
BroadcastChannel, WebSocket, or WebRTC.
Import the onInit function for initializing the client.
import { onInit } from './transport/init.js';Create a BroadcastChannel for the game clients and the game host.
const bcGameClients = new BroadcastChannel( 'game-clients' );const bcGameHost = new BroadcastChannel( 'game-host' );Now setup a message handler for the bcGameClients.
This example only supports one host and multiple clients.
Handle messages by passing them to the Echo-D instance.
Automatically call the onInit function when the client begins listening.
export function listenToHost ( echoD, ctx ) { if ( !ctx.id ) { ctx.id = onInit( echoD, sendToHost ); } bcGameClients.onmessage = ( { data } ) => { if ( !ctx.id && data[ 0 ] === 'init' ) { ctx.id = onInit( echoD, sendToHost ); } else { echoD.many( data ); } };}Implement the sendToHost function to send messages to the host.
export function sendToHost ( message ) { bcGameHost.postMessage( message );}Or use WebSocket in a supported environment such as a web browser.
msgpack is also recommended for reducing the size of messages.
Especially when transporting over the network.
import { decode, encode } from 'msgpack';import { onInit } from './transport/init.js';Create a websocket client, and ensure errors are handled.
export function createWebSocket( url ) { url = url || 'ws://localhost:8080'; const ws = new WebSocket( url ); ws.on( 'error' , console.error ); return ws;}Listen to the host and handle incoming messages with Echo-D using the WebSocket.
Call the onInit function when the client is initialized.
export function listenToHost( echoD, ctx, ws ) { ctx = ctx || { id: null }; ws = ws || createWebSocket( ); ctx.ws = ws; ws.on( 'message', ( data ) => { data = decode( data ); if ( data[ 0 ] === 'init' ) { ctx.id = onInit( echoD, sendToHost ); } else echoD.many( data ) } ); return ctx;}Implement the sendToHost function to send messages to the host.
export function sendToHost ( message ) { const data = encode( message ); ws.send( data );}Default Client Transport
Provide a way to import the sendToHost method from ./client/transport.js.
Set the default transport to BroadcastChannel.
export * from './transport/bc.js';Set the default transport to WebSocket.
export * from './transport/ws.js';Update Render Function
This will use the echoD store to create a list of render views, and update
them when the store is updated.
The renderViews object is used to store the react three fiber views,
and the renderObjects object is used to store the three objects.
const renderViews = { };const renderObjects = { };The updateRender function is called after every update, and should be used
to update the render views.
export function updateRender ( echoD, events ) { const componentPages = echoD.store.getComponents( ) const removedIds = Object.keys( renderViews ) let updated = false for ( const componentsPage of componentsPages ) { for ( const entities of componentsPage ) { for ( const id in entities) { const entity = entities[ id ] if ( updateRenderEntity( id, entity, removedIds ) ) { updated = true } for ( const component of entity ) { console.log( id, component, entity[ component ]) } } } } if ( removedIds.length ) { for ( const id of removedIds ) { delete renderViews[ id ]; delete renderObjects[ id ]; updated = true; } } if ( updated ) { const views = Object.keys( renderViews ).map( ( id ) => renderViews[ id ] ); events.emit( 'render', views ); }}Update Render Entity
The updateRenderEntity function is called for every entity in the echoD store,
and should be used to update the render views.
It will also emit a render event when the views are updated.
It uses React Fiber to render the views.
export function updateRenderEntity ( id, entity, removedIds = [ ] ) { // Create react three fiber view if it doesn't exist if ( !renderViews[ id ] ) { renderViews[ id ] = ( <Ball key={id} ref={ ( ref ) => ( renderObjects[id] = ref ) } position={ entity.position } /> ); return true; } else { // Remove id from removedIds if it exists const index = removedIds.indexOf( id ) if ( index !== -1 ) { removedIds.splice( index, 1 ); }
if ( renderObjects[ id ] && entity.position ) { renderObjects[ id ].position.x = entity.position[ 0 ] || 0; renderObjects[ id ].position.y = entity.position[ 1 ] || 0; renderObjects[ id ].position.z = entity.position[ 2 ] || 0; // return true; } } return false;}Client Components
The client components are used to render the 3D objects in the React Fiber tree.
Ball Component
Use <Ball /> to create a 3D ball in the React Fiber tree.
import React, { forwardRef } from 'react';export const Ball = forwardRef( function Ball ( props, ref ) { const { children, position = [ 0, 0, 0 ], size = [ 0.5 ], color = 0xff4444 } = props; return ( <mesh ref={ ref } position={ position } > <sphereGeometry args={ size } /> <meshStandardMaterial color={ color } /> { children } </mesh> );} );EchoD Component
Use <EchoD /> to embed 3D entities into React Fiber tree
import { useEffect, useState } from 'react';export function EchoD ({ events: localEvents = events } ) { const [ views, setViews ] = useState( [ ] ); useEffect( () => { localEvents.on( 'render', setViews ); return () => localEvents.off( 'render', setViews ); }, [ localEvents ] ); return views;}Render Component
Use <Render /> to create a React Fiber tree with a 3D canvas amd embed 3D entities.
import React from 'react';import { Canvas } from '@react-three/fiber';import { EchoD } from './EchoD.js';export function Render ( props ) { const { children, events } = props; return ( <Canvas> <ambientLight intensity={ Math.PI / 2 } /> <spotLight position={ [ 10, 10, 10 ] } angle={ 0.15 } penumbra={ 1 } decay={ 0 } intensity={ Math.PI } /> <pointLight position={ [ -10, -10, -10 ] } decay={ 0 } intensity={ Math.PI } /> <EchoD events={ events } /> {children} </Canvas> )}