
Behold! My new game!
If only it were that easy. What you see above is a simple test client which allows me to enter a sector ID, click ENTER, and enter the associated Colyseus Room on the server. The act of joining the room returns the initial room state as it understands it after loading info from the data store, and I display the ID from the data store on the screen. The EXIT TEXT fires off the room.leave event to gracefully exit the room and close the server connection.
A Single Component – BasicRoom.js
To get our test component started, I created the basicroom.js file. Aside from the usual React imports (as well as Boostrap for some formatting assistance), there’s nothing super unusual about the setup here.
export const BasicRoom = (props) => { ... }
We expect some props, so we’re provisioning an argument for them.
const [ currentSectorId, setCurrentSectorId ] = useState(0);
const [ currentSectorRoom, setCurrentSectorRoom ] = useState(null);
const [ currentSector, setCurrentSector ] = useState(null);
const [ isConnected, setIsConnected ] = useState(false);
const { client, playerId } = props;
const allowEnter = currentSectorId > 0;
The first four definitions are for our React state items. currentSectorId will remember which ID the player wants to enter. currentSectorRoom will hold the room object returned from the server when we successfully enter a room. currentSector is a custom object which will hold the serialized state information that the server returns to us. isConnected is a sanity check so we can tell when we’ve successfully entered a room on the server side.
We’re destructuring two attributes that we’ll expect on the implementation of this component. client is the Colyseus client that we define in App.js which is the foot we stick in the server’s door. playerId is a shim; at some point in the process the player will have to log in and select his player. From that, we get the playerId which we’ll need to pass to the createPlayer method in the server SectorState.
allowEnter is another sanity check that won’t allow us to use the ENTER button unless there’s a value in the textbox.
const handleChange = (e) =>{
const { name, value } = e.target;
setCurrentSectorId(value);
}
This handler takes the value entered into the textbox and stores it in the currentSectorId state variable. Nothing fancy here.
const handleEnterRoom = () =>{
client.joinOrCreate(`sector`, {"sectorId":currentSectorId, "playerId": playerId})
.then(sector=>{
setCurrentSectorRoom(sector);
sector.onStateChange.once((state) => {
const info = {
sectorId: state.id,
designation: state.designation,
name: state.name,
x: state.x,
y: state.y,
isMajor: state.isMajor
}
setCurrentSector(info);
});
setIsConnected(true);
})
.catch(err => {
console.log(`joinOrCreate error: ${err}`);
setIsConnected(false);
})
}
handleEnterRoom is fired when the ENTER button is clicked. Using the client reference that we pulled from props, we call joinOrCreate. This client-side method will attempt to join the room specified as the first argument, but if the room does not currently exist, the server will create it, firing the onCreate and then onJoin event handlers. If you remember the signature of the server-side onCreate handler, you’ll remember that in addition to the client, we accept an argument options. Our options, then, are to pass in sectorId as well as the playerId. We use the first to query the database, and the second to pull character data and create an instance of Player within SectorState.
Once we have successfully connected to the room, we store the sector room reference into currentSectorRoom and then call sector.onStateChange.once(). This method asks the server for the entire state. Later, if we want to manually get a state refresh, we can raise the same event, but without the once. Otherwise, changes to the server state are sent with every patchRate tick interval, so we don’t need to ask for state updates manually. We are taking the state data and are building an object which we store in the currentSector client-side state object. Finally, we are setting isConnected to true.
const handleDisconnect = () => {
if (isConnected){
currentSectorRoom.leave();
setIsConnected(false);
}
}
If the user clicks the EXIT TEST button, we will gracefully disconnect from the server by calling currentSectorRoom.leave. currentSectorRoom refers to the Room instance we received from the server side.
let sectorInfo;
if(isConnected && currentSector !== null){
sectorInfo =
<React.Fragment>
<Row>
<Col>Sector:</Col>
<Col>{currentSector.sectorId}</Col>
</Row>
</React.Fragment>
}
Once we have both isConnected and a valid value for currentSector, we build a small snippet that uses the data from currentSector to display the sectorId value passed over from the server.
return(
<Container>
<Row>
<Col><label>Enter which sector: </label><input type="text" name="sectorId" onChange={handleChange}></input></Col>
<Col><input type="button" name="enterSector" value="Enter" onClick={handleEnterRoom} disabled={!allowEnter} /></Col>
</Row>
{sectorInfo}
<Row>
<Col><input type="button" name="disconnect" value="Exit Test" onClick={handleDisconnect} /></Col>
</Row>
</Container>
)
Finally, we have our output to screen.
App.js
import React, { useEffect } from 'react';
import * as Colyseus from 'colyseus.js';
import { BasicRoom } from './components/basicroom';
import 'bootstrap/dist/css/bootstrap.css';
function App() {
const port = process.env.GAMEPORT || 3030;
const client = new Colyseus.Client(`ws://localhost:${port}`);
return (
<div className="App">
<BasicRoom client={client} playerId="12345ABCDE" />
</div>
);
}
export default App;
This is a very simple App.js implementation. We import the needed package for Colyseus, set the port, and instantiate the client. In the render portion of the component, we insert the BasicRoom component, passing in client and playerId as props.
Looking Ahead
I needed to test the ability to enter a single room. Overall, it’s been fairly easy; most of the server side implementation was me getting ahead of myself. I could have created just a Room class, handled the required events, made a similar test client, and gotten much the same results. But I needed to see if my test data implementation would also work.
What concerns me, though, is data availability. On the client side, I am considering a higher order component wrapper for the entire UI that would contain a context which has a factory class implementation as it’s value. Then, whenever I needed to call a method to deal with the server, I could do so through the context, and any data sent back to the client would be accessible through that same context. That may be a misunderstanding on my part, because I can’t imagine something as complex as client-server communication in a stateless web app would be that conceptually simple. We shall see.
In terms of the server, I figure the best way forward is simply baby steps. I do not want to wholesale copy the Mazmorra example, although it’s a bit hard not to considering that it’s core functionality would support the exact thing I am trying to do, and while I might be able to figure it out and do something similar without the sample code, I am sure I would miss something crucial, get frustrated, and bail on the project…again.
This concludes this series on my progress so far. I think I have spent more time writing these posts than I have actually creating the current project test. I’m going to put my head down and continue on to see if I can set up my jumpgate network, and then test inter-sector movement.