This tutorial teaches you how to create multi-user shared animations and interactions using Multisynq. You’ll build a simulation with 25 bouncing balls that can be stopped and started by clicking them. This demonstrates how the Model computes simulations and how the View displays and interacts with them.

Try it out!

Click or scan the QR code above to launch a new CodePen instance of this session. Compare the two sessions - you’ll see that the animated simulations are identical. The balls all move and bounce exactly the same way.

Interactive Elements

Click any ball to stop/start it - changes sync to all users

Non-interactive Elements

The rounded rectangle bounces but ignores user actions

What You’ll Learn

1. Simulation Model

How to create a model that runs physics simulations

2. Interactive View

Building views that respond to user interactions

3. Safe Communication

Properly communicating between Model and View

Architecture Overview

This application uses two Multisynq Model subclasses: MyModel and BallModel. Both classes must be registered with Multisynq for proper synchronization.

Using Multisynq Constants

The app uses Multisynq.Constants to ensure all users share the same configuration values:

const Q = Multisynq.Constants;
Q.BALL_NUM = 25;              // how many balls do we want?
Q.STEP_MS = 1000 / 30;        // bouncing ball tick interval in ms
Q.SPEED = 10;                 // max speed on a dimension, in units/s

Models must not use global variables, but global constants are fine. Multisynq.Constants is recursively frozen once a session starts to prevent accidental modification.

Simple Animation Model

MyModel - Root Model

MyModel is the root model passed to Multisynq.Session.join(). It creates and stores the BallModel objects in the MyModel.children array.

BallModel - Individual Ball Logic

Each BallModel represents a shaped, colored, bouncing ball. The model stores only the data needed for synchronization:

  • Shape: String ('circle' or 'roundRect')
  • Color: Random color value
  • Position: Current x,y coordinates
  • Speed: Velocity vector

Event Subscription

this.subscribe(this.id, 'touch-me', this.startStop);

Each BallModel subscribes to the 'touch-me' event using its own ID as scope. This ensures that only the specific ball responds to touch events intended for it.

Simulation Loop

this.future(Q.STEP_MS).step();

After initialization, each BallModel schedules its first step() method invocation. This creates a continuous simulation loop:

BallModel.step() {
    if (this.alive) this.moveBounce();
    this.future(Q.STEP_MS).step();
}

25 balls ticking at 30Hz generates 750 messages per second, but future messages are very efficient with minimal overhead.

Movement and Bouncing

BallModel.moveBounce() {
    const [x, y] = this.pos;
    if (x<=0 || x>=1000 || y<=0 || y>=1000)
        this.speed = this.randomSpeed();
    this.moveTo([x + this.speed[0], y + this.speed[1]]);
}

The moveBounce() method updates ball position and handles wall collisions. When a ball hits a wall, it gets a new random speed vector.

Deterministic Random Speed

randomSpeed() {
    const xs = this.random() * 2 - 1;
    const ys = this.random() * 2 - 1;
    const speedScale = Q.SPEED / (Math.sqrt(xs*xs + ys*ys));
    return [xs * speedScale, ys * speedScale];
}

This uses Multisynq’s replicated random number generator. Every session instance computes the same sequence of random numbers, ensuring identical bouncing behavior across all users.

Simple Animation View

The View comprises two classes: MyView and BallView.

MyView - Container View

MyView is called when a session instance starts, receiving the MyModel object as an argument. It builds the visual representation and container for all balls.

Creating Child Views

model.children.forEach(child => this.attachChild(child));

The MyView accesses the model’s children collection and creates a BallView for each BallModel.

MyView.attachChild(child) {
    this.element.appendChild(new BallView(child).element);
}

It’s fine for views to read from models directly, but views MUST NOT modify models in any way.

Responsive Scaling

MyView listens for browser “resize” events and adjusts the view scale accordingly. This ensures all users see the same scene regardless of their window size.

Cleanup on Detach

MyView.detach() {
    super.detach();
    let child;
    while (child = this.element.firstChild) this.element.removeChild(child);
}

When a session shuts down, the root view cleans up all child views and resources.

BallView - Individual Ball Display

Each BallView tracks its associated BallModel and handles visual representation and user interaction.

Position Updates

this.subscribe(model.id, { event: 'pos-changed', handling: "oncePerFrame" }, this.move);

The BallView subscribes to 'pos-changed' events from its specific model. The "oncePerFrame" handling ensures efficient rendering even with multiple position updates per frame.

Touch/Click Handling

BallView.enableTouch() {
    const el = this.element;
    if (TOUCH) el.ontouchstart = start => {
        start.preventDefault();
        this.publish(el.id, 'touch-me');
    }; else el.onmousedown = start => {
        start.preventDefault();
        this.publish(el.id, 'touch-me');
    };
}

The BallView sets up touch/click handlers that publish 'touch-me' events. The corresponding BallModel subscribes to these events and toggles ball motion on and off.

Key Concepts Demonstrated

Next Steps

Now that you understand basic animation and interaction patterns, you can explore:

Best Practices

Use Constants

Store shared configuration in Multisynq.Constants

Scope Events

Use object IDs to scope events to specific instances

Efficient Updates

Use "oncePerFrame" handling for frequent updates

Clean Resources

Implement proper cleanup in detach() methods

This tutorial demonstrates the power of Multisynq’s deterministic synchronization for creating engaging multi-user interactive experiences. The same patterns can be applied to games, simulations, and collaborative tools.