Skip to content

rmvn27/software-systeme-praktikum

Repository files navigation

Software-Systeme Praktikum: Multiplayer Canvas

This project was created as part of the Software-Systeme course held by Prof. Heidegger at the THA. The goal of this Praktikum was to both to teach how to create a real-time multiplayer application using an event-based system and how SPA's can be created without having to resort external dependencies like react.

For that the task was to create a multiplayer canvas application where users can create/join sessions and draw different shapes on a canvas together. The canvas had support the drawing of 4 basic shapes (line, rectangle, circle, triangle), the option to choose a specific border and fill color (including a transparent option). Next to that the canvas had to support a z-order for the shapes, the selection of those shapes to move them, delete them or increase/decrease their z-order. Since the whole project should be a multiplayer application each of those actions should be able to performed by a user all while other users could interact with the canvas simultaneously.

Software

Requirements

  • The target for the browser funcionality is set to "ES2021"
    • The code should work with a recent version of Chrome/Firefox
  • node: Current version (v20.2.0)
  • npm: Current version (v9.6.6)

Running

  • Install all dependencies: npm install
  • Configure the software:
    • Entierly optional and the project works without configuring it
    • Create a .env file in the project dir (a .env.exaple can be found with all possible env vars)
      • Most important env var is NODE_ENV which determines how the frontend assets are served
    • Adjust the persistClientId in src/client/main.ts
      • Setting it to false allows having mutliple canvas sessions in a browser session
      • The client id for the browser client won't be persisted if this option is set to false
      • Should be only set to false when NODE_ENV is set to production otherwise the live reload can bork the canvas session
  • Build the project: npm run build (only needed for NODE_ENV=produciton)
  • Start the project: npm run start

External libraries

  • Bundler (vite)
  • Typescript (v5.0.4)
  • Linter and Formatting (eslint and prettier)
  • Http server:
    • Server itself: @tinyhttp/app
    • Serving client assets: vite (dev) and sirv (production)
    • Websockets: ws and tinyws (middleware)
  • dotenv for environment variables

Project Structure

  • common/: Common types and dependencies for both the server and client
    • common/canvas: Base canvas types (shapes, storage, colors)
    • common/{commands,events}: Typedefinitions for commands and events that are send over the websocket
    • common/flow: Simple reactive streams implementation based on the schemantics of the Flow in Kotlin
  • server/: Server application
    • server/canvas: Domain logic for the canvas
    • server/http: Http modules for handing the frontend assets and websockets
  • client/: Client SPA
    • client/canvas: Domain logic for the canvas
    • client/components: UI components

Architecture

Overview

The project follows the general advised architecture of an event based system with some minor tweaks. A websocket is used to communicate between the server and multiple clients. The messages that are sent through the websocket are categorised in commands (client sent) and events (server sent), the individual message types are explained in the next section. The client can send these commands to performs some remote action on the server while events represent that something was changed on the server.

The server manages the state of the canvases. It acts as a source of thruth for them and holds every state that belongs to the canvas. The server also manages the incoming connections and makes sure that clients can enter a cavas perform actions/receive events and then leave a canvas. One big difference that deviates from the advised architecture that a canvas in the server recevies commands rather than just events. The bigest difference is that the server not just accepts events from the clients and relays them to the other ones, but actually executes the requested actions and then emits events based on the performed actions. The commands act as the source of thruth of the canvas state and are designed to allow for a determinstic state restoration from replaying them. This allows to just persist the commands and restore a canvas out of them. The events themselves are not persisted as the can be determined from the executed commands.

The client allows the user to interact the application. The user can create new named canvases and join them by clicking on one of them in the overview or by browsing to it's url. One entered a canvas he can draw and interact with it. The user has the ability to mark a canvas as persistent to keep it open even if no user has currenly joined it and to persist it when the server stops. The user can also leave a room. If a room has no users anymore and is not persistent the room will be automatically removed. For every action the user takes commands will be send to the server. Sending commands rather than just events was chosen to improve the 'security' since the user could just send bad events over the websocket (eg selecting an already selected shape by another user or just removing the selection of the other users). With the commands the client can only request a change but not perform the change itself, the server first validates the actions before executing them. The events act for the client as a source of thruth as a complete canvas (on the side of the client) can be just restored from the received events.

Commands/Events

We differentiate between two types of commands/events: Client commands/events and Canvas commands/events. The commands are always send by the client and the server will respond with events.

Client commands/events

These commands will be send to the server to initialize the client and to manage the canvases on the server. The client can get notified on the available canvases, join or leave them.

Commands

  • ConnectCommand(clientId: string | null)
    • Initialize the connect handshake with the server
    • If the client has already a clientId it will be passed to the event
    • Server responds with a ConnectionEstablishedEvent(clientId: string)
    • That contains either the passed clientId or a generated one if no clientId was provided
  • CreateCanvasCommand(canvasName: string)
    • Request the creation of a new canvas
    • Server responds with CanvasCreatedEvent(canvas: SerializedCanvas) that contains the canvas metainformation
  • JoinCanvasCommand(canvasId: string): Join a canvas and start receiving events from it
  • LeaveCanvasCommand: Leave the joined canvas and stop receiving events
  • ToggleCanvasPersistance: Toggle the persistent setting in the currently joined canvas
  • PushCanvasCmdsCommands(cmds: CanvasCommand[])
    • Request new commands to the joined canvas
    • Commands are batched to improve performance

Events

  • ConnectionEstablishedEvent: See ConnectCommand
  • CanvasesChangedEvent(canvases: Record<string, SerializedCanvas>):
    • Metainformation about the available canvases has changed
    • Is recveied on every change
  • CanvasCreatedEvent: See CreateCanvasCommand
  • ReceiveCanvasEventsEvent(events: CanvasEvent[]):
    • Receive events for a currenlty joined canvas
    • Either pushed because we requested some commands to be executed
    • Or another client executed some commands

Canvas commands/events

The canvas commands/events are performed on the currently joined canvas of the client. Both commands and events are batched to improve the performance. This means we will be sending less messages and redraws of the client's canvas should happen less often. Since the node event loop is single threaded this also means that the batched commands of a client is executed sequentialle before another batch can be executed allowing to not care about any race conditions that could happen.

Commands

  • AddShapeCommand(clientId: string, shape: SerializedShape)
    • Add a new shape to the canvas
    • Since this is a new shape no checks need to be performed
    • Server returns a ShapeAddedEvent
  • RemoveShapeCommand(clientId: string, shapeId: string)
    • Request the deletion of a shape with the id shapeId
    • For the deletin the shape has not to be selected by another user
    • If successfull returns ShapeRemovedEvent
  • SelectShapesCommand(clientId: string, point: Point2D, cycle: boolean, multiselect: boolean)
    • Perform a selection
    • The client can't perform add or remove individual shapes from the seletion to mitigate bad input
    • The server selects or unselects shapes based on the point and passed options
    • Shapes of other users won't be selected or unselected
    • Server responds with: ShapeSelectedEvent and ShapesUnselectedEvent events
  • SelectByShapeIdsCommand(clientId: string, shapeIds: string[])
    • Try to select indivial shapes
    • Used for the drag and drop feature
    • The server still checks if the provided shapeIds are selected by other clients
    • Server responds with: ShapeSelectedEvent and ShapesUnselectedEvent events
  • UnselectAllShapesCommand(clientId: string)
    • Remove the complete selection of the user
    • The server will just unselect all shapes of the user
    • Server responds with: ShapesUnselectedEvent events

Events

  • ShapeAddedEvent(shape: SerializedShape): See AddShapeCommand
  • ShapeRemovedEvent(shapeId: string): See RemoveShapeCommand
  • ShapeSelectedEvent(shapeId: string, clientId: string)
    • Shape has been successfully selected by clientId
  • ShapeUnselectedEvent(shapeId: string, clientId: string)
    • Shape has been successfully unselected by clientId

About

Real-time multiplayer drawing application built with vanilla JS

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors