Skip to content

Example Client

Bundle with esbuild, webpack, bun build, or another bundler.

client.js
import EchoD from 'echo-d';

Prepare the events object to be used with Echo-D.

client.js
import { events } from './events.js';

Include the listenToHost and sendToHost methods from the transport layer.

client.js
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.

client.js
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.

client.js
let renderTimer = null
function 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.

client.js
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.

client.js
const ctx = { id: null };
listenToHost( echoD, ctx );

List to controls and send inputs to the host.

client.js
listenToControls( echoD, ctx );

Seupt a handler to remove the actor when the window is closed or refreshed.

client.js
window.addEventListener('beforeunload', ( event ) => {
echoD.removeActor( ctx.id );
echoD.updater( );
});

Create react three fiber Canvas in document root

client.js
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.

controls.js
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.

client/transport/init.js
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.

client/transport/bc.js
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.

client/transport/bc.js
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.

client/transport/bc.js
export function sendToHost ( message ) {
bcGameHost.postMessage( message );
}

Default Client Transport

Provide a way to import the sendToHost method from ./client/transport.js.

Set the default transport to BroadcastChannel.

client/transport.js
export * from './transport/bc.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.

client/update.js
const renderViews = { };
const renderObjects = { };

The updateRender function is called after every update, and should be used to update the render views.

client/update.js
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.

client/update.js
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.

components/Ball.js
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

components/EchoD.js
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.

components/Render.js
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>
)
}