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> )}