Learn how to structure and launch complete Multisynq applications with models, views, and session management
// 1. Define your Model class
class MyModel extends Multisynq.Model {
init() {
// Initialize shared state and logic
this.gameState = "waiting";
this.players = new Map();
this.startTime = this.now();
// Subscribe to user input events
this.subscribe("input", "player-action", this.handlePlayerAction);
// Start main game loop
this.future(1000/60).gameLoop();
}
handlePlayerAction(data) {
// Process user input and update state
console.log("Player action:", data);
}
gameLoop() {
// Main simulation logic
if (this.gameState === "playing") {
this.updateGame();
}
// Continue the loop
this.future(1000/60).gameLoop();
}
updateGame() {
// Game simulation logic here
}
}
// 2. REQUIRED: Register your model
MyModel.register("MyModel");
// 3. Define your View class
class MyView extends Multisynq.View {
init() {
// Get reference to the model
this.model = this.wellKnownModel("MyModel");
// Set up user interface
this.setupUI();
this.setupInputHandlers();
this.startRenderLoop();
// Subscribe to model events
this.subscribe("game", "state-changed", this.updateDisplay);
}
setupUI() {
// Create DOM elements
this.canvas = document.getElementById('gameCanvas');
this.ctx = this.canvas.getContext('2d');
// Style the interface
document.body.style.margin = '0';
document.body.style.backgroundColor = '#222';
}
setupInputHandlers() {
// Handle user input
this.canvas.addEventListener('click', (event) => {
const rect = this.canvas.getBoundingClientRect();
const x = event.clientX - rect.left;
const y = event.clientY - rect.top;
// Send input to model
this.publish("input", "player-action", { x, y, type: "click" });
});
document.addEventListener('keydown', (event) => {
this.publish("input", "player-action", {
key: event.key,
type: "keydown"
});
});
}
startRenderLoop() {
this.render();
}
render() {
// Clear canvas
this.ctx.fillStyle = '#222';
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
// Draw current state
this.drawGame();
// Continue render loop
requestAnimationFrame(() => this.render());
}
drawGame() {
// Read model state and draw UI
this.ctx.fillStyle = 'white';
this.ctx.font = '24px Arial';
this.ctx.fillText(
`Game State: ${this.model.gameState}`,
20, 40
);
this.ctx.fillText(
`Players: ${this.model.players.size}`,
20, 70
);
}
updateDisplay() {
// React to model changes (optional)
console.log("Model state updated");
}
}
// 4. Register your view
MyView.register("MyView");
.register("ClassName")
with the exact class name.// Configure your session
const apiKey = "your_api_key_here"; // Get from multisynq.io/coder
const appId = "com.example.myapp"; // Unique app identifier
const name = Multisynq.App.autoSession(); // Auto-generate session name
const password = Multisynq.App.autoPassword(); // Auto-generate password
// Launch the session
Multisynq.Session.join({
apiKey,
appId,
name,
password,
model: MyModel,
view: MyView
});
autoSession()
and autoPassword()
helpers parse URL parameters and create random values if needed.// When Session.join() is called, this sequence occurs:
// 1. Connect to synchronizer
console.log("Connecting to Multisynq synchronizer...");
// 2. Check for existing session state
// If session exists: Load from snapshot
// If new session: Initialize fresh
// 3. Instantiate Model
console.log("Creating model instance...");
const model = MyModel.create();
// 4. Initialize or restore model state
if (isNewSession) {
// First user - run init()
model.init();
console.log("Model initialized with fresh state");
} else {
// Joining existing session - load snapshot
// model.init() is NOT called
console.log("Model restored from snapshot");
}
// 5. Instantiate View
console.log("Creating view instance...");
const view = new MyView();
view.setModel(model);
// 6. Start main execution loop
console.log("Starting main loop...");
startMainLoop(model, view);
model.init()
only runs for the first user in a session. Subsequent users get the model from a snapshot.🔄 Automatic Main Loop
// Standard session with automatic main loop
const session = await Multisynq.Session.join({
apiKey: "your_api_key",
appId: "com.example.app",
name: "my-session",
password: "my-password",
model: MyModel,
view: MyView
// step: "auto" is the default
});
// The automatic main loop does this every frame:
// 1. Process all pending model events
// 2. Process all pending view events
// 3. Call view.render() if it exists
// 4. Repeat at ~60fps
class MyView extends Multisynq.View {
init() {
this.model = this.wellKnownModel("MyModel");
this.setupCanvas();
// Optional: Define render method for automatic calling
}
render() {
// This gets called automatically every frame
this.clearCanvas();
this.drawGame();
this.drawUI();
}
clearCanvas() {
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
}
drawGame() {
// Read model state and draw
for (const entity of this.model.entities) {
this.drawEntity(entity);
}
}
drawUI() {
// Draw user interface elements
this.drawScore();
this.drawHealth();
this.drawMinimap();
}
}
⚙️ Manual Main Loop
// Manual stepping for custom main loops
const session = await Multisynq.Session.join({
apiKey: "your_api_key",
appId: "com.example.app",
name: "my-session",
password: "my-password",
model: MyModel,
view: MyView,
step: "manual" // Disable automatic stepping
});
// Create your own main loop
window.requestAnimationFrame(mainLoop);
function mainLoop(timestamp) {
if (session.view) {
// Custom pre-processing
handleCustomInput();
updateAnimations(timestamp);
// Process Multisynq events and models
session.step(timestamp);
// Custom post-processing
renderEffects();
updateAudio();
handleNetworking();
}
// Continue the loop
window.requestAnimationFrame(mainLoop);
}
function handleCustomInput() {
// Custom input processing before model updates
if (inputBuffer.length > 0) {
const inputs = inputBuffer.splice(0, inputBuffer.length);
// Batch process inputs
session.view.publish("input", "batch-actions", inputs);
}
}
function updateAnimations(timestamp) {
// Update view-only animations
tweenManager.update(timestamp);
particleSystem.update(timestamp);
}
function renderEffects() {
// Custom rendering after model processing
effectsRenderer.render();
uiAnimator.render();
}
function updateAudio() {
// Audio processing
audioManager.update(session.model.gameState);
soundEffects.process();
}
function handleNetworking() {
// Custom networking (be careful not to break sync!)
if (shouldSendAnalytics()) {
sendAnalyticsData(session.model.getStats());
}
}
// Advanced: Custom frame rate control
let lastUpdateTime = 0;
const targetFPS = 30; // Custom frame rate
const frameTime = 1000 / targetFPS;
function customFrameRateLoop(timestamp) {
if (timestamp - lastUpdateTime >= frameTime) {
// Run update logic
session.step(timestamp);
lastUpdateTime = timestamp;
}
requestAnimationFrame(customFrameRateLoop);
}
🎮 Game-Specific Loops
// Real-time action game
class ActionGameLoop {
constructor(session) {
this.session = session;
this.fixedTimeStep = 1000 / 60; // 60fps simulation
this.accumulator = 0;
this.lastTime = 0;
}
start() {
requestAnimationFrame((time) => this.loop(time));
}
loop(currentTime) {
const deltaTime = currentTime - this.lastTime;
this.lastTime = currentTime;
this.accumulator += deltaTime;
// Fixed timestep updates for consistent physics
while (this.accumulator >= this.fixedTimeStep) {
this.session.step(currentTime);
this.accumulator -= this.fixedTimeStep;
}
// Interpolated rendering for smooth visuals
const alpha = this.accumulator / this.fixedTimeStep;
this.interpolateRender(alpha);
requestAnimationFrame((time) => this.loop(time));
}
interpolateRender(alpha) {
// Smooth rendering between fixed updates
this.session.view.renderInterpolated(alpha);
}
}
// Turn-based strategy game
class TurnBasedLoop {
constructor(session) {
this.session = session;
this.isWaitingForInput = false;
this.turnTimeLimit = 30000; // 30 seconds per turn
}
start() {
this.processLoop();
}
async processLoop() {
while (this.session.isActive) {
if (this.session.model.gameState === "waiting-for-turn") {
await this.handleTurnInput();
} else {
// Process any pending events
this.session.step(performance.now());
}
// Update display
this.session.view.render();
// Small delay to prevent busy waiting
await this.sleep(16); // ~60fps display updates
}
}
async handleTurnInput() {
this.isWaitingForInput = true;
// Wait for player input or timeout
const inputPromise = this.waitForPlayerInput();
const timeoutPromise = this.sleep(this.turnTimeLimit);
await Promise.race([inputPromise, timeoutPromise]);
this.isWaitingForInput = false;
}
waitForPlayerInput() {
return new Promise((resolve) => {
const handler = (event) => {
if (event.type === "turn-submitted") {
this.session.view.unsubscribe("input", "turn-submitted", handler);
resolve();
}
};
this.session.view.subscribe("input", "turn-submitted", handler);
});
}
sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
}
// Usage
const session = await Multisynq.Session.join({...options, step: "manual"});
// Choose appropriate loop for your game type
if (gameType === "action") {
new ActionGameLoop(session).start();
} else if (gameType === "turn-based") {
new TurnBasedLoop(session).start();
}
// ✅ Good structure
class GameModel extends Multisynq.Model {
init() {
this.initializeGameState();
this.setupEventHandlers();
this.startGameSystems();
}
initializeGameState() {
// Clear separation of concerns
}
}
// ✅ Modular views
class GameView extends Multisynq.View {
init() {
this.ui = new UIManager(this);
this.renderer = new GameRenderer(this);
this.input = new InputHandler(this);
}
}
// ✅ Robust session handling
try {
const session = await Multisynq.Session.join(options);
// Add error handling
session.on('error', handleSessionError);
session.on('disconnect', handleDisconnect);
return session;
} catch (error) {
console.error("Session failed:", error);
showErrorDialog(error);
}
// ✅ Efficient rendering
render() {
if (this.needsRedraw) {
this.drawGame();
this.needsRedraw = false;
}
}
// ✅ Batch operations
handleMultipleInputs(inputs) {
// Process inputs in batches
this.publish("input", "batch", inputs);
}
// ✅ Graceful error handling
class RobustModel extends Multisynq.Model {
init() {
try {
this.setupGame();
} catch (error) {
this.handleInitError(error);
}
}
handleInitError(error) {
console.error("Init failed:", error);
this.gameState = "error";
this.publish("error", "init-failed", error);
}
}
🎮 Game State Management
class StateManagedGame extends Multisynq.Model {
init() {
this.gameState = "menu";
this.stateData = {};
this.subscribe("game", "change-state", this.changeState);
}
changeState(newState, data = {}) {
const oldState = this.gameState;
// Exit current state
this.exitState(oldState);
// Enter new state
this.gameState = newState;
this.stateData = data;
this.enterState(newState);
this.publish("game", "state-changed", {
from: oldState,
to: newState,
data: data
});
}
enterState(state) {
switch (state) {
case "menu":
this.setupMenu();
break;
case "playing":
this.startGameplay();
break;
case "paused":
this.pauseGameplay();
break;
case "game-over":
this.endGameplay();
break;
}
}
exitState(state) {
// Cleanup state-specific resources
switch (state) {
case "playing":
this.pauseAllSounds();
break;
}
}
}
🔧 Configuration Management
class ConfigurableApp {
static async launch(userConfig = {}) {
// Merge user config with defaults
const config = {
apiKey: "",
appId: "com.example.default",
sessionName: null,
password: null,
maxPlayers: 8,
gameMode: "casual",
...userConfig
};
// Validate required config
this.validateConfig(config);
// Auto-generate missing values
if (!config.sessionName) {
config.sessionName = await Multisynq.App.autoSession();
}
if (!config.password) {
config.password = await Multisynq.App.autoPassword();
}
// Create model and view with config
const ModelClass = this.getModelClass(config.gameMode);
const ViewClass = this.getViewClass(config.gameMode);
return Multisynq.Session.join({
apiKey: config.apiKey,
appId: config.appId,
name: config.sessionName,
password: config.password,
model: ModelClass,
view: ViewClass,
options: {
maxPlayers: config.maxPlayers
}
});
}
static validateConfig(config) {
if (!config.apiKey) {
throw new Error("API key is required");
}
if (!config.appId) {
throw new Error("App ID is required");
}
}
static getModelClass(gameMode) {
const models = {
casual: CasualGameModel,
competitive: CompetitiveGameModel,
sandbox: SandboxGameModel
};
return models[gameMode] || CasualGameModel;
}
static getViewClass(gameMode) {
const views = {
casual: CasualGameView,
competitive: CompetitiveGameView,
sandbox: SandboxGameView
};
return views[gameMode] || CasualGameView;
}
}
// Usage
ConfigurableApp.launch({
apiKey: "your_key",
appId: "com.yourcompany.yourgame",
gameMode: "competitive",
maxPlayers: 16
});