This is a follow-up to the previous post regarding my attempts to centralize a React app connection to a Colyseus game server.

Earlier I had come up with what I had thought was a great way to manage a connection between a website and the game server by using a Context wrapper. Of special note was the implementation of a 3AM epiphany which saw me registering callbacks to a collection which was iterated over when a message is passed from the server to the client. This would, in theory, allow me to write disconnected message handlers and then load them into the collection when I needed them so I didn’t have to write long, single-minded scripts that could only handle a narrow band of messages.

It’s been some time since I’ve visited this project, but I’ve been getting back to it and last week I discovered that my earlier tests were a bit too contrived to work in the wild. For some reason, I can’t get the previous code to work.

export const ColyseusProvider = (props) => {
    const [ currentRoom, setCurrentRoom ] = useState(null);
    const [ callbackQueue, setCallbackQueue ] = useState([])

    const client = new Colyseus.Client('ws://localhost:3553');

    const EnterRoom = ({roomName}) =>{
        client.joinOrCreate(roomName, { user:"Scopique"})
        .then(room => {
            setCurrentRoom(room);
            room.onMessage("*", (type, message)=>{
                HandleServerMessage(type, message);
            })
        })
        .catch(err=>{
            console.log(err);
        })
    }

    const RegisterMessageCallback = (tag, callback) => {
        if (callbackQueue.length > 0){
            const haveIt = callbackQueue.filter(itm=>{return(itm.tag === tag && itm.callback === callback)});
            //If false, add it.
            if (haveIt.length === 0){
                setCallbackQueue(callbackQueue.push({tag:tag, callback:callback}));
            }
        }else{
            //This is the first
            const newQueue = [{tag:tag, callback:callback}]
            setCallbackQueue(newQueue);

        }
    }

    const HandleServerMessage = (tag, message)=> {
        //Loop through all callbacks in the callbackQueue and match by tag
        //Send message to each callback
        if (callbackQueue.length > 0){
            const sendTo = callbackQueue.filter(itm=>{return (itm.tag.toUpperCase() === tag.toUpperCase())});
            if (sendTo.length > 0){
                sendTo.forEach(itm=>{
                    if ((itm.callback && typeof itm.callback === 'function')){
                        itm.callback(message);
                    }
                })
            }
        }
    }

    const colyseus = {
        client: client,
        room: currentRoom,
        EnterRoom: props.EnterRoom || EnterRoom,
        RegisterMessageCallback: props.RegisterMessageCallback || RegisterMessageCallback
    }

    return(
        <ColyseusContext.Provider value={colyseus}>
            {props.children}
        </ColyseusContext.Provider>
    )
}

The above code is defined on the server. I’ve gotten the EnterRoom method working just fine, but when it came time to respond to a message from the server, the HandleServerMessage was bombing out because the callbackQueue state collection was always empty.

I blame React for this. As is my M.O., almost as soon as I get comfortable with a process, I start to play fast and loose with my understanding of said process. While my logic looks sound — adding functions to the callbackQueue and then assuming they’ll persist because of my “insightful design” — it turned out that this component was reloading itself a few times at best. There may be other issues, but I identified the fact that even if a function was added to the queue, it was probably getting erased because the component was resetting itself.

Technically the whole point of this HOC and context design was to try and avoid getting caught in the refresh cycle of components by making the Colyseus code a kind of singleton placed out of reach of the children except by reference through the HCO and context system. It does not seem to work this way, or my execution is wrong; in either case, the problem lies with me, but rather than try and rebuild this part of the client app, I went after the smallest problem of the bushel: an alternative to that function queue.

I was last-week-years-old when I learned about how Javascript allows us to execute functions by string-name.

window['MyFunctionName'](args);

This signature opens up a whole world of possibilities for abuse which I’m sure every experienced programmer is itching to school me on, but I know that convenience always comes at a price. Thing is, I’m broke right now so take my IOU and let me get on with the post.

Back when I was coding DIscord bots, I followed a pattern laid down by the DiscordJS library docs which allowed me to write command handlers in one-off files, load them when the bot spun up, and then call the appropriate file content by parsing the command and looping through a collection of methods. This is similar to what I had set out to achieve with my callbackQueue. Since Javascript is a client-side language and therefor has limited-to-no access to files on the server, I had to find a different way, and this “execute by string name” was my foot in the door.

//Handler file enterRoom.js
export const EnterRoom = (msg) => { console.log(msg); }

//Handler collection named index.js for easy reference
import { EnterRoom } from './enterRoom';

const MessageHandlers = {
    roomEntered: EnterRoom,
}

export default MessageHandlers;

First, I can define any number of files which have the signature FUNCTION_NAME(MESSAGE). Each message that the server emits that the client needs to listen for should have a command listener file. While the names of the file and the name of the function do not need to match the tag of the server message, it helps to keep them very close for easy reference.

Second, the MessageHandlers is nothing but an easy way to associate a server tag with the message handler function. Here, roomEntered is what the server sends when a client enters a room:

//Colyseus server onJoin handler
onJoin(client: Client, options: any, auth: any){
    client.send('roomEntered', `OK`);
}

As the client message handlers pile up, they can be added to MessageHandlers using the same tag: function pattern.

To use this on the client, I have modified the original EnterRoom method:

//Get the MessageHandlers object
import MessageHandlers from '../Colyseus/Handlers';

//Call this method to send a player to a specific room.
const EnterRoom = (roomName, token) =>{
    client.joinOrCreate(roomName, { token: token})
    .then(room => {
        room.onMessage("*", (type, message)=>{
            MessageHandlers[type](message);
        })
    })
    .catch(err=>{
        console.log(err);
    })
}

The bolded line above is a generic way to listen to all server messages. The type property is the tag received from the server — roomEntered — and the message prop is self explanatory. MessageHandlers receives the type as a string to look up the associated method, and message is passed as the argument.

While this does work, there’s a few gotchas that I am keeping an eye on (aside from the general “looseness” of executing methods by string name, of course).

First, is this really necessary? Messages sent from the server are not going to contain processing directives. At best, they will only be conveying status updates that a process did or did not work out as expected. Colyseus sends all of the heavy-duty data in state frame updates which is completely outside of this concern. I expect message handlers to be small and relatively inconsequential as a result.

Second, could I get it even more compact? Possibly, by grouping similar message handlers into a single object:

//Bundle up small message handlers that share similar concerns 
//into RoomMessageHandlers.js
module.exports = {
    EnterRoom(msg) { console.log(msg); },
    LeaveRoom(msg) { console.log(msg); }
}

//In helper file
const { RoomMessages } = require('../Colyseus/Handlers/RoomMessageHandlers';
...
const MessageHandlers= (tag, message) => {
    roomEntered: RoomMessages.EnterRoom,
    roomLeft: RoomMessages.LeaveRoom
}

//Use the MessageHandlers the same way as before:
MessageHandlers[type](message);

The only thing this does, really, is keep handlers organized into fewer files. meaning that the helper file would have fewer handlers to import. But the system seems to work now, albeit in very specific, low-rent tests.

Since this is working right now, I’m going to run with it as I need to get working on the actual operation of the game server.

Leave a Reply

Your email address will not be published. Required fields are marked *