This document explains how the Spacewar example leverages RCWeb technology to enable multi-player gaming across devices and provides a guide for building similar real-time distributed applications.
The Spacewar game relies on a decoupled architecture, splitting the logic into two seperate apps. The
main spacewar game and the spacewar controller.
The spacewar game app acts as the viewer, and the
spacewar-control app acts as the controller.
The two apps communicate within the same virtual room as facilitated by comms.js.
abcd-efgh).
The comms.js library manages a persistent connection via WebSockets.
Multiple clients in the same room can communicate.
/spacewar-control/?r=abcd-efgh referencing the control app and room so players can
start controlling a new ship.
The game app exposes spacewar.controlShip(rc.client, direction).
rc.client it hasn't seen before, it dynamically
instantiates a new ship locally with getShip(rc.client). This stateless approach avoids
complex "join" or "leave" handshakes.
To recreate this pattern for a new multi-player game, follow these implementation steps:
Create two directories, one for the game app and one for the control app. For example:
app/mygameapp/mygame-controlBoth apps require an index.html with rc
environment variables set in the <head> section of the HTML.
Variables are injected by RCWeb server.
<script>
var rc = {
"version": "${version}",
"app": "${app}",
"room": "${roomId}",
"client": "${clientId}",
"commsWebSocket": "${websocket}"
};
</script>;
Then include the comms.js library and your game code.
<script src="/assets/core/comms.js">
<script src="script.js">
/app/mygame/script.js)
The game app should run an update loop, handle rendering, and expose a global API for controllers.
var myGame = (function() {
var players = {};
// Core game loop using requestAnimationFrame
var tick = function() {
// ... update physics based on frame delta ...
// ... render changes ...
window.requestAnimationFrame(tick);
}
return {
// Public API to be executed via eval() over RCWeb
movePlayer: function(playerId, direction) {
if (!players[playerId]) {
// Initialize player dynamically upon first interaction
players[playerId] = { x: 0, y: 0 };
}
if (direction === 'up') players[playerId].y -= 10;
},
start: function() {
// Setup game space
tick();
}
};
})();
// Provide diagnostic callback overrides
rc.onUpdateNetworkStatus = function(heading, info) {
console.log("Network:", heading, info);
};
rc.onUpdateError = function(error) {
console.error("Eval Error:", error);
};
// Start the game, optionally initialize local keyboard listeners for testing
window.addEventListener("keydown", localKeyHandler);
// Display a QR Code pointing to your controller's path using the unique rc.room:
// "https://" + location.host + "/app-control/?r=" + rc.room
// Connect the to RCWeb room
rc.connect();
/app/mygame-control/script.js)
The controller evaluates user input, and pushes remote function calls using
rc.sendFunctionCall(target, functionName, ...args).
function sendMove(direction) {
// Construct the remote function call to manipulate the viewer's state.
// The first parameter specifies the target app(s) running in the room.
// The second parameter is the name of the function to execute on those targets.
// Subsequent parameters are passed as arguments to that function.
rc.sendFunctionCall("mygame", "myGame.movePlayer", rc.client, direction);
}
document.getElementById('upButton').addEventListener('touchstart', function() {
sendMove('up');
});
// Connect the to RCWeb room
rc.connect();
This approach bypasses traditional serialization/deserialization message parsing required in most multiplayer
architectures. Instead of sending rigid JSON structures that the server has to validate, the controller
constructs a
direct function call to actuate the remote game's state. The comms.js framework manages network
resilience, proxying real-time JavaScript from controllers to viewers.