Application Structure
Every Multisynq app requires exactly two classes: one Model and one View.
- Basic Structure
- Complete Game Example
Essential application template
Copy
Ask AI
// 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");
Critical: Every Model subclass must call
.register("ClassName")
with the exact class name.Launching a Session
You need an API key from multisynq.io/coder to launch sessions.
- Basic Session Launch
- Production Session Setup
Standard session setup
Copy
Ask AI
// 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
});
The
autoSession()
and autoPassword()
helpers parse URL parameters and create random values if needed.Session Lifecycle
Understanding the session lifecycle helps you build robust applications that handle joining, leaving, and persistence correctly.
- Session Startup Process
- Handling User Joins/Leaves
What happens when you join a session
Copy
Ask AI
// 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);
Critical:
model.init()
only runs for the first user in a session. Subsequent users get the model from a snapshot.Main Loop Execution
Multisynq automatically manages the main loop, processing model events first, then view events, then rendering.
🔄 Automatic Main Loop
🔄 Automatic Main Loop
Default behavior (recommended)
Copy
Ask AI
// 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 Main Loop
Custom control for advanced applications
Copy
Ask AI
// 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
🎮 Game-Specific Loops
Specialized main loops for different game types
Copy
Ask AI
// 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();
}
Best Practices
🏗️ Application Structure
Organize your code effectively
Copy
Ask AI
// ✅ 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);
}
}
🔄 Session Management
Handle session lifecycle properly
Copy
Ask AI
// ✅ 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);
}
🎯 Performance
Optimize for smooth gameplay
Copy
Ask AI
// ✅ Efficient rendering
render() {
if (this.needsRedraw) {
this.drawGame();
this.needsRedraw = false;
}
}
// ✅ Batch operations
handleMultipleInputs(inputs) {
// Process inputs in batches
this.publish("input", "batch", inputs);
}
🛡️ Error Handling
Build resilient applications
Copy
Ask AI
// ✅ 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);
}
}
Common Patterns
🎮 Game State Management
🎮 Game State Management
Copy
Ask AI
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
🔧 Configuration Management
Copy
Ask AI
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
});
Next Steps
Writing a Multisynq Model
Deep dive into model development and constraints
Writing a Multisynq View
Learn advanced view patterns and user interface development
Events & Pub-Sub
Master communication between models and views
Simple Animation
See complete applications in action
Building a complete Multisynq application requires understanding both the technical structure (Model + View + Session) and the patterns for managing user interaction, state changes, and session lifecycle. Start with simple applications and gradually add complexity as you master these fundamentals.