├── .gitattributes ├── .gitignore ├── Procfile ├── README.md ├── data ├── UE_logo.png ├── example.png ├── p5.multiplayer_custom_host.png ├── p5.multiplayer_lan.png ├── p5.multiplayer_localhost.png ├── p5.multiplayer_remote.png ├── p5_logo.png └── p5multiplayer_diagrams_1080.ai ├── docs ├── NOTES.md └── REFERENCE.md ├── package-lock.json ├── package.json ├── public ├── host.html ├── host.js ├── index.html ├── index.js ├── index.php └── lib │ ├── p5.multiplayer.js │ ├── p5.play.js │ ├── rotation.js │ └── utilities.js ├── secureServer.js ├── server.js └── template ├── host.html ├── host.js ├── index.html ├── index.js ├── index.php └── lib └── p5.multiplayer.js /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | # Cert/Key Files 4 | *.pem -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: node server.js 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [Home]() | [Reference](docs/REFERENCE.md) | [Development Notes](docs/NOTES.md) 2 | 3 | # p5.multiplayer 4 | 5 | This repository contains a set of template files for building a multi-device, multiplayer game where multiple clients can connect to a specified host page. The clients and hosts are built using *[p5.js](https://p5js.org)*, and they communicate with each other through a *[node.js](https://nodejs.org/en/download/)* server via *[socket.io](https://socket.io/)* messages. 6 | 7 | * [Getting Started](#getting-started) 8 | * [Examples](#examples) 9 | * [Using the Template Files](#using-the-template-files) 10 | * [How does it work?](#how-does-it-work) 11 | * [Local](#local) 12 | * [Local Area Network (LAN)](#local-area-network-lan) 13 | * [Remote Server](#remote-server) 14 | * [Using p5.multiplayer with a Remote Server](#using-p5multiplayer-with-a-remote-server) 15 | * [Using with Glitch](#using-with-glitch) 16 | * [What is Glitch?](#what-is-glitch) 17 | * [Quickstart](#glitch-quickstart) 18 | * [Installation](#glitch-installation) 19 | * [Using with Heroku](#using-with-heroku) 20 | * [What is Heroku?](#what-is-heroku) 21 | * [Installation](#heroku-installation) 22 | * [Custom Hosts](#custom-hosts) 23 | * [Using an HTTPS Server](#using-an-https-server) 24 | * [Reference](docs/REFERENCE.md) 25 | * [Support](#support) 26 | 27 | ![An example image of the base project in action. It shows a host window on the left side of a screen populated by two colored squares, each matching client controller windows on the right side of the screen.](data/example.png) 28 | 29 | ## Getting Started 30 | [[Back to top]](#p5multiplayer) 31 | 32 | 1. Clone this GitHub repository on your local machine. 33 | 34 | 2. If you don't already have *node.js* installed on your machine, go [here](https://nodejs.org/en/download/) and download the version appropriate for your operating system. 35 | 36 | 3. Open a terminal window and navigate to the project directory. 37 | 38 | 4. Run the command `npm install`. 39 | 40 | 5. Next, run the command `node server.js` to start a *node.js* server. 41 | 42 | 6. Open a browser and go to `http://127.0.0.1:3000/host.html`. This will open up a host page. Make note of the URL displayed in the bottom left corner of the screen. 43 | 44 | 7. Open a second browser and go to the URL displayed on your host page. It will look something like `http://127.0.0.1:3000/?=roomId`, where `roomId` is a randomly generated name. This screen will load a controller that let's you control the movement of a colored square on the host page. 45 | 46 | 8. (OPTIONAL) The included *node.js* server cleverly lets you specify a custom room ID (think of it as a semi-private game room). You can specify your own room ID by opening a host page using `http://127.0.0.1:8080/host.html?=roomId`, where `roomId` is a string of your choice. 47 | 48 | ## Using the Template Files 49 | [[Back to top]](#p5multiplayer) 50 | 51 | The `host.js`, `host.html`, `index.js`, and `index.html` files located within the `public` directory are a basic game example and should have everything you need to start building your own browser-based game. 52 | 53 | They make use of two existing *[p5.js](https://p5js.org)* libraries: *[p5.touchgui](https://github.com/L05/p5.touchgui)*, which enables easy creation of mouse and touchscreen GUI elements, and *[p5.play](https://molleindustria.github.io/p5.play/)*, which enables easy creation of 2D sprite games within *p5.js*. 54 | 55 | If you'd like to start with a completely blank template, however, please navigate to the `template` directory. You'll see comments indicating where to add your game logic and other related code bits that will customize the project to your own specifications. Once you've modified these to your liking, you can copy them into the `public` directory, overwriting the existing files of the same names. 56 | 57 | ## Examples 58 | 59 | * [Live demo hosted on Glitch](https://p5-multiplayer.glitch.me/host.html). *Note: Once the page opens, go to the link at the bottom screen in a second browser.* 60 | * [Example video of the template in action with Unity.](https://vimeo.com/274410221) 61 | 62 | ## How does it work? 63 | [[Back to top]](#p5multiplayer) 64 | 65 | *p5.multiplayer* functions by using a *node.js* server to relay data messages between a "host" and any "clients" that connect to it. The messages are sent using socket.io as the messaging protocol. 66 | 67 | *p5.multiplayer* can be used in three possible configurations: 68 | 69 | #### Localhost (Same computer) 70 | [[Back to top]](#p5multiplayer) 71 | 72 | This type of configuration works great for testing. You can run your *node.js* server on your local machine and test the "host" and "client" pages in separate browswer windows. 73 | 74 | Diagram of p5.multiplayer running solely on a local computer. 75 | 76 | #### Local Area Network (LAN) 77 | [[Back to top]](#p5multiplayer) 78 | 79 | You can set up a multiplayer game or interactive installation on a local area network (LAN) that will enable multiple devices to connect to your machine. You'll need to know your machine's IP address and use that as your `serverIp` in order for this to work (how to find your IP address on [MacOS](https://www.macworld.co.uk/how-to/mac/ip-address-3676112/) / [Windows](https://support.microsoft.com/en-us/help/4026518/windows-10-find-your-ip-address)). 80 | 81 | In your `host.js` and `index.js` files, set `const local = true`. Then change `const serverIp = 'yourIpAddress';` so that `yourIpAddress` matches your IP address. 82 | 83 | Please be aware that your machine may be vulnerable whenever you allow other devices to connect to it. 84 | 85 | Diagram of p5.multiplayer running on a local area network (LAN) 86 | 87 | #### Remote Server 88 | [[Back to top]](#p5multiplayer) 89 | 90 | This type of configuration is great for making sure as many devices as possible are able to connect to your server. In this case, the *node.js* server is hosted on a remote server and uses [Express](https://expressjs.com/) to serve the "host" and "client" pages to connected devices. The increased connectivity afforded by using a remote server comes at the cost of latency, which may or may not be noticeable depending on server, network speed of each connected device, and user interaction design. 91 | 92 | Please see the [directions below](#using-p5multiplayer-with-a-remote-server) for information on how to set this up. 93 | 94 | Diagram of p5.multiplayer running on a remote server. 95 | 96 | ## Using p5.multiplayer with a Remote Server 97 | 98 | ### Using with Glitch 99 | 100 | #### What is Glitch? 101 | [[Back to top]](#p5multiplayer) 102 | 103 | [Glitch](https://glitch.com) is an approachable platform for creating web apps that let's users easily share, reuse, and repurpose code. You can use the service to create a dedicated URL for your game server instead of hosting it locally on your own machine. This will enable users to connect to a set URL from any device's browser as long as the device is connected to the internet, regardless of whether via ethernet, Wi-Fi, LTE, etc. 104 | 105 | **Pros:** 106 | * Can be accessed from any internet connected device. 107 | * Dedicated URL for your own server. 108 | * Free as long as you're within the platform [restrictions](https://glitch.com/help/restrictions/). 109 | 110 | **Cons:** 111 | * Not as fast as a local connection (i.e. your own computer, wireless router, etc.). 112 | * Project are put to sleep after a relatively short period of time (see platform [restrictions](https://glitch.com/help/restrictions/)). 113 | * Projects are subject to a connection limit per hour (see platform [restrictions](https://glitch.com/help/restrictions/)). 114 | * You must create a Glitch account. 115 | * A little bit extra setup (but hopefully the below steps make that easier!). 116 | 117 | #### Glitch Quickstart 118 | [[Back to top]](#p5multiplayer) 119 | 120 | It's really quick and easy to get up and running with [Glitch](https://glitch.com). 121 | 122 | 1. Create a free [Glitch](https://glitch.com) account if you don't already have one. 123 | 124 | 2. Go to [this link](https://glitch.com/~p5-multiplayer), which is a Glitch project containing all of the code from this repository and is already modified to work with Glitch. 125 | 126 | 3. Open your new project. Glitch will give your project a random name; please rename it at this time (unless you really like the randomly chosen name). 127 | 128 | 4. 6. In your `host.js` and `index.js` files, change `const serverIp = 'https://p5-multiplayer.glitch.me';` so that what was `p5-multiplayer` now matches your Glitch project name. If you're not sure of what the URL should be, click *Show > In a New Window* at the top of the screen and a new browser window will open with the URL you should use. 129 | 130 | #### Glitch Installation 131 | [[Back to top]](#p5multiplayer) 132 | 133 | 1. Create a free [Glitch](https://glitch.com) account if you don't already have one. 134 | 135 | 2. Once logged in, click *New Project > Clone from Git Repo*. 136 | 137 | 3. Copy this repository URL `https://github.com/L05/p5.multiplayer` and paste it where instructed to *Paste the full URL of your repository*. 138 | 139 | 4. You will be taken to an editor screen. If not, make sure to *Edit your project*. 140 | 141 | 5. Once in the editor, use the sidebar to go to the project's `public` directory. 142 | 143 | 6. In your `host.js` and `index.js` files, set `const local = false` as you will be running these using a remote server. Then change `const serverIp = 'https://yourprojectname.glitch.me';` so that `yourprojectname` matches your Glitch project name. If you're not sure of what the URL should be, click *Show > In a New Window* at the top of the screen and a new browser window will open with the URL you should use. 144 | 145 | 7. Click *Show > In a New Window* to open a new browser window and add `host.html` at the end of the URL. It will look something like `https://yourprojectname.glitch.me/host.html`. You should see a "host" screen with *# players* in the top left of the window and a URL displayed in the bottom left corner. Make note of this URL. 146 | 147 | 8. In a second browser window, go to the aforementioned URL. You should see a "client" screen displaying a simple controller that lets you control a colored square in your "host" screen. 148 | 149 | ### Using with Heroku 150 | 151 | #### What is Heroku? 152 | [[Back to top]](#p5multiplayer) 153 | 154 | [Heroku](https://heroku.com) is a cloud platform as a service that you can use to create a dedicated URL for your game server instead of hosting it locally on your own machine. This will enable users to connect to a set URL from any device's browser as long as the device is connected to the internet, regardless of whether via ethernet, Wi-Fi, LTE, etc. 155 | 156 | **Pros:** 157 | * Can be accessed from any internet connected device. 158 | * Dedicated, persistent URL for your own server. 159 | * Free as long as you're within the platform [limits](https://devcenter.heroku.com/articles/limits). 160 | 161 | **Cons:** 162 | * Not as fast as a local connection (i.e. your own computer, wireless router, etc.). 163 | * You must create a Heroku account. 164 | * Project are put to sleep after a period of time (see platform [limits](https://devcenter.heroku.com/articles/limits)). 165 | * Projects are subject to a connection limit per hour (see platform [limits](https://devcenter.heroku.com/articles/limits)). 166 | * A little bit extra setup (but hopefully the below steps make that easier!). 167 | 168 | #### Heroku Installation 169 | [[Back to top]](#p5multiplayer) 170 | 171 | 1. Make sure to first follow at least steps 1 through 4 in [Getting Started](#getting-started). 172 | 173 | 2. Create a free [Heroku](https://heroku.com) account if you don't already have one. 174 | 175 | 3. Open a terminal window and navigate to the project directory. 176 | 177 | 4. Create a Heroku app by running the command `heroku create yourservername`, replacing `yourservername` with a server name of your choice. 178 | 179 | 5. Go to the project's `public` directory and in your `host.js` and `index.js` files, set `const local = false` as you will be running these using a remote server. Then change `const serverIp = 'https://yourservername.herokuapp.com';` to match your Heroku server address. 180 | 181 | 6. Commit these updates. In the terminal, run the command `git add -a -m "Updating for Heroku deployment"`. 182 | 183 | 7. Next, in the terminal run the command `git push heroku master`. This will push your code to the remote Heroku server. 184 | 185 | 8. Open a browser window and go to `https://yourservername.herokuapp.com/host.html`, replacing `yourservername` with the server name you selected in step 4. You should see a "host" screen with *# players* in the top left of the window and a URL displayed in the bottom left corner. Make note of this URL. 186 | 187 | 9. In a second browser window, go to the aforementioned URL. You should see a "client" screen displaying a simple controller that lets you control a colored square in your "host" screen. 188 | 189 | ## Custom Hosts 190 | [[Back to top]](#p5multiplayer) 191 | 192 | *p5.multiplayer* can be used with custom hosts as long as they implement the core functionality of `setupHost()` and `sendData()` present in [p5.multiplayer.js](public/lib/p5.multiplayer.js). 193 | 194 | For example, the host should emit a `'join'` event with `{name: 'host', roomId: roomId}` as data, and the host should register handlers for `'hostconnect'`, `'clientConnect'`, `'clientDisconnect'`, and `'receiveData'`. 195 | 196 | Any additional game logic, rendering, etc. can be included beyond that. 197 | 198 | An [Unreal Engine](https://unrealengine.com) based custom host example and template is currently in development and will be shared as soon as it is available. 199 | 200 | Diagram of p5.multiplayer running on a remote server with a custom host. 201 | 202 | ## Using an HTTPS Server 203 | [[Back to top]](#p5multiplayer) 204 | 205 | If needed, you can run a secure HTTPS server instead of an HTTP server ([learn more about the difference here](https://www.cloudflare.com/learning/ssl/why-is-http-not-secure/)), and some applications may only work with HTTPS. For instance, TouchDesigner currently only supports [Socket IO connections over HTTPS](http://derivative.ca/Forum/viewtopic.php?f=4&t=19894). As a note, this section is generally more applicable to local or self-administered server configurations; Glitch and Heroku automatically set up HTTPS for you when using their services to run a remote server. 206 | 207 | In order to use an HTTPS server, you'll need an SSL certificate. To do this, open a terminal window and navigate to the project's root directory (where `server.js` and `secureServer.js` are located). 208 | 209 | Next, follow these instructions from [nodejs.org](https://nodejs.org/en/knowledge/HTTP/servers/how-to-create-a-HTTPS-server/): 210 | 211 | *We need to start out with a word about SSL certificates. Speaking generally, there are two kinds of certificates: those signed by a 'Certificate Authority', or CA, and 'self-signed certificates'. A Certificate Authority is a trusted source for an SSL certificate, and using a certificate from a CA allows your users to be trust the identity of your website. In most cases, you would want to use a CA-signed certificate in a production environment - for testing purposes, however, a self-signed certicate will do just fine.* 212 | 213 | *To generate a self-signed certificate, run the following in your shell:* 214 | 215 | ``` 216 | openssl genrsa -out key.pem 217 | openssl req -new -key key.pem -out csr.pem 218 | openssl x509 -req -days 9999 -in csr.pem -signkey key.pem -out cert.pem 219 | rm csr.pem 220 | ``` 221 | 222 | *This should leave you with two files, `cert.pem` (the certificate) and `key.pem` (the private key). Put these files in the same directory as your Node.js server file. This is all you need for a SSL connection.* 223 | 224 | Once you have generated an SSL certificate, you'll follow the same instructions outlined in [Getting Started](#getting-started), but instead of running `node server.js` in Step 5, you'll instead run `node secureServer.js`. 225 | 226 | Please note that some browsers may block or issue a warning when pointed to an HTTPS server with a self-signed SSL certificate. For more information on this, [please read here](https://devcenter.heroku.com/articles/ssl-certificate-self). 227 | 228 | 229 | ## Support 230 | [[Back to top]](#p5multiplayer) 231 | 232 | Please use *p5.multiplayer* and let me know if you have any feedback! 233 | 234 | * Do you **use it in a project**? What works and doesn't work? 235 | * Do you **teach it in a class**? What works and doesn't work? 236 | 237 | Any questions pertaining to this project may be communicated via Issues on the [p5.multiplayer GitHub repository](https://github.com/L05/p5.multiplayer). Simply create a new Issue and either assign or tag me in the conversation with [@L05](https://github.com/L05). 238 | -------------------------------------------------------------------------------- /data/UE_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/L05/p5.multiplayer/1dead03b1ec317d7336fb13d32566c31f56f779b/data/UE_logo.png -------------------------------------------------------------------------------- /data/example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/L05/p5.multiplayer/1dead03b1ec317d7336fb13d32566c31f56f779b/data/example.png -------------------------------------------------------------------------------- /data/p5.multiplayer_custom_host.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/L05/p5.multiplayer/1dead03b1ec317d7336fb13d32566c31f56f779b/data/p5.multiplayer_custom_host.png -------------------------------------------------------------------------------- /data/p5.multiplayer_lan.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/L05/p5.multiplayer/1dead03b1ec317d7336fb13d32566c31f56f779b/data/p5.multiplayer_lan.png -------------------------------------------------------------------------------- /data/p5.multiplayer_localhost.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/L05/p5.multiplayer/1dead03b1ec317d7336fb13d32566c31f56f779b/data/p5.multiplayer_localhost.png -------------------------------------------------------------------------------- /data/p5.multiplayer_remote.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/L05/p5.multiplayer/1dead03b1ec317d7336fb13d32566c31f56f779b/data/p5.multiplayer_remote.png -------------------------------------------------------------------------------- /data/p5_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/L05/p5.multiplayer/1dead03b1ec317d7336fb13d32566c31f56f779b/data/p5_logo.png -------------------------------------------------------------------------------- /data/p5multiplayer_diagrams_1080.ai: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/L05/p5.multiplayer/1dead03b1ec317d7336fb13d32566c31f56f779b/data/p5multiplayer_diagrams_1080.ai -------------------------------------------------------------------------------- /docs/NOTES.md: -------------------------------------------------------------------------------- 1 | [Home](../README.md) | [Reference](REFERENCE.md) | [Development Notes]() 2 | 3 | # Development Notes 4 | 5 | ## TODOs 6 | 7 | * ~~Add ability for host to send data to all clients in room~~ 8 | * Add ability for host to send data to specific client 9 | * Encapsulate p5.multiplayer as a class? 10 | * ~~Expand example "game" to include bidirectional communication~~ 11 | * ~~Expand example "game" to include a button action~~ 12 | * Expand example "game" to be an actual game? (LOL) 13 | * Build example Unreal host template 14 | * Build example TouchDesigner host template 15 | * ~~Add documentation about bidirectional communication to reference~~ 16 | * ~~Add documentation section about custom hosts~~ -------------------------------------------------------------------------------- /docs/REFERENCE.md: -------------------------------------------------------------------------------- 1 | [Home](../README.md) | [Reference]() | [Development Notes](NOTES.md) 2 | 3 | # Reference 4 | 5 | [Client](#client) 6 | * [setupClient()](#setupclient) 7 | * [sendData()](#senddata-client) 8 | * [isClientConnected()](#isclientconnected) 9 | * [Callbacks](#client-callbacks) 10 | * [onReceiveData()](#onreceivedata-client) 11 | 12 | [Host](#host) 13 | * [setupHost()](#setuphost) 14 | * [sendData()](#senddata-host) 15 | * [isHostConnected()](#ishostconnected) 16 | * [displayAddress()](#displayaddress) 17 | * [Callbacks](#host-callbacks) 18 | * [onClientConnect()](#onclientconnect) 19 | * [onClientDisconnect()](#onclientdisconnect) 20 | * [onReceiveData()](#onreceivedata-host) 21 | 22 | ----- 23 | 24 | ## Client 25 | 26 | ----- 27 | 28 | #### setupClient() 29 | [[Back to top]](#reference) 30 | 31 | ##### Example 32 | ```javascript 33 | // Network settings 34 | const serverIp = '127.0.0.1'; 35 | const serverPort = '3000'; 36 | const local = true; // true if running locally, false 37 | // if running on remote server 38 | 39 | function setup() { 40 | createCanvas(windowWidth, windowHeight); 41 | 42 | setupClient(); 43 | } 44 | ``` 45 | ##### Description 46 | Sets up client to connect to server and send messages to host. 47 | 48 | ##### Syntax 49 | ```javascript 50 | setupClient() 51 | ``` 52 | 53 | ##### Parameters 54 | `None` 55 | 56 | ##### Returns 57 | `None` 58 | 59 | ----- 60 | 61 | #### sendData() - *Client* 62 | [[Back to top]](#reference) 63 | 64 | ##### Example 65 | ```javascript 66 | sendData('playerColor', { 67 | r: red(playerColor)/255, 68 | g: green(playerColor)/255, 69 | b: blue(playerColor)/255 70 | }); 71 | ``` 72 | ```javascript 73 | let myData = { 74 | val1: 0, 75 | val2: 128, 76 | val3: true 77 | } 78 | 79 | sendData('myDataType', myData); 80 | ``` 81 | ##### Description 82 | Sends JavaScript object message of specified data type from client to host. 83 | 84 | ##### Syntax 85 | ```javascript 86 | sendData(datatype, data) 87 | ``` 88 | 89 | ##### Parameters 90 | `datatype` String: data type of message 91 | `data` Object: a JavaScript object containing user-defined values 92 | 93 | ##### Returns 94 | `None` 95 | 96 | ----- 97 | 98 | #### isClientConnected() 99 | [[Back to top]](#reference) 100 | 101 | ##### Example 102 | ```javascript 103 | function draw() { 104 | background(0); 105 | 106 | if(isClientConnected(display=true)) { 107 | // Client draw here. ----> 108 | 109 | 110 | // <---- 111 | } 112 | } 113 | ``` 114 | ##### Description 115 | Checks to see if the client is successfully connected to the server and returns Boolean result. If `display=true`, connectivity support instructions are displayed on the screen. 116 | 117 | ##### Syntax 118 | ```javascript 119 | isClientConnected(display) 120 | ``` 121 | 122 | ##### Parameters 123 | `display` Boolean: displays connectivity support instructions if `true` (default is `false`) 124 | 125 | ##### Returns 126 | Boolean: `true` if client is connected, `false` otherwise 127 | 128 | ----- 129 | 130 | ### Client Callbacks 131 | User-defined callbacks for handling data received from hosts. These **must** be present in your `index.js` sketch. 132 | 133 | ----- 134 | 135 | #### onReceiveData() - *Client* 136 | [[Back to top]](#reference) 137 | 138 | ##### Example 139 | ```javascript 140 | function onReceiveData (data) { 141 | // Input data processing here. ---> 142 | console.log(data); 143 | 144 | // <--- 145 | 146 | /* Example: 147 | if (data.type === 'myDataType') { 148 | processMyData(data); 149 | } 150 | 151 | Use `data.type` to get the message type sent by host. 152 | */ 153 | 154 | } 155 | ``` 156 | ##### Description 157 | Callback for when data is received from a host. Must be defined in `index.js` sketch. `data` parameter provides all data sent from host and always includes: 158 | * `.id` String: unique ID of host 159 | * `.type` String: data type of message 160 | 161 | ##### Syntax 162 | ```javascript 163 | onReceiveData (data) 164 | ``` 165 | 166 | ##### Parameters 167 | `data` Object: contains all data sent from client 168 | 169 | ##### Returns 170 | `None` 171 | 172 | ----- 173 | 174 | ## Host 175 | 176 | ----- 177 | 178 | #### setupHost() 179 | [[Back to top]](#reference) 180 | 181 | ##### Example 182 | ```javascript 183 | // Network settings 184 | const serverIp = '127.0.0.1'; 185 | const serverPort = '3000'; 186 | const local = true; // true if running locally, false 187 | // if running on remote server 188 | 189 | function setup() { 190 | createCanvas(windowWidth, windowHeight); 191 | 192 | setupHost(); 193 | } 194 | ``` 195 | ##### Description 196 | Sets up host to connect to server and receive messages from clients. 197 | 198 | ##### Syntax 199 | ```javascript 200 | setupHost() 201 | ``` 202 | 203 | ##### Parameters 204 | `None` 205 | 206 | ##### Returns 207 | `None` 208 | 209 | ----- 210 | 211 | #### sendData() - *Host* 212 | [[Back to top]](#reference) 213 | 214 | ##### Example 215 | ```javascript 216 | function mousePressed() { 217 | sendData('timestamp', { timestamp: millis() }); 218 | } 219 | ``` 220 | ```javascript 221 | let myData = { 222 | val1: 0, 223 | val2: 128, 224 | val3: true 225 | } 226 | 227 | sendData('myDataType', myData); 228 | ``` 229 | ##### Description 230 | Sends JavaScript object message of specified data type from host to all connected clients. 231 | 232 | ##### Syntax 233 | ```javascript 234 | sendData(datatype, data) 235 | ``` 236 | 237 | ##### Parameters 238 | `datatype` String: data type of message 239 | `data` Object: a JavaScript object containing user-defined values 240 | 241 | ##### Returns 242 | `None` 243 | 244 | ----- 245 | 246 | #### isHostConnected() 247 | [[Back to top]](#reference) 248 | 249 | ##### Example 250 | ```javascript 251 | function draw () { 252 | background(15); 253 | 254 | if(isHostConnected(display=true)) { 255 | // Host/Game draw here. ---> 256 | 257 | 258 | // <---- 259 | 260 | // Display server address 261 | displayAddress(); 262 | } 263 | } 264 | ``` 265 | ##### Description 266 | Checks to see if the host is successfully connected to the server and returns Boolean result. If `display=true`, connectivity status is displayed on the screen. 267 | 268 | ##### Syntax 269 | ```javascript 270 | isHostConnected(display) 271 | ``` 272 | 273 | ##### Parameters 274 | `display` Boolean: displays connectivity status if `true` (default is `false`) 275 | 276 | ##### Returns 277 | Boolean: `true` if host is connected, `false` otherwise 278 | 279 | ----- 280 | 281 | #### displayAddress() 282 | [[Back to top]](#reference) 283 | 284 | ##### Example 285 | ```javascript 286 | function draw () { 287 | background(15); 288 | 289 | if(isHostConnected(display=true)) { 290 | // Host/Game draw here. ---> 291 | 292 | 293 | // <---- 294 | 295 | // Display server address 296 | displayAddress(); 297 | } 298 | } 299 | ``` 300 | ##### Description 301 | Displays the server address in the lower left of the canvas. 302 | 303 | ##### Syntax 304 | ```javascript 305 | displayAddress() 306 | ``` 307 | 308 | ##### Parameters 309 | `None` 310 | 311 | ##### Returns 312 | `None` 313 | 314 | ----- 315 | 316 | ### Host Callbacks 317 | User-defined callbacks for handling client connections and disconnections and data received from clients. These **must** be present in your `host.js` sketch. 318 | 319 | ----- 320 | 321 | #### onClientConnect() 322 | [[Back to top]](#reference) 323 | 324 | ##### Example 325 | ```javascript 326 | function onClientConnect (data) { 327 | // Client connect logic here. ---> 328 | print(data.id + ' has connected.'); 329 | 330 | // <---- 331 | } 332 | ``` 333 | ##### Description 334 | Callback for when new client connects to server. Must be defined in `host.js` sketch. `data` parameter provides: 335 | * `.id` String: unique ID of client 336 | 337 | ##### Syntax 338 | ```javascript 339 | onClientConnect (data) 340 | ``` 341 | 342 | ##### Parameters 343 | `data` Object: contains client connection data 344 | 345 | ##### Returns 346 | `None` 347 | 348 | ----- 349 | 350 | #### onClientDisconnect() 351 | [[Back to top]](#reference) 352 | 353 | ##### Example 354 | ```javascript 355 | function onClientDisconnect (data) { 356 | // Client connect logic here. ---> 357 | print(data.id + ' has disconnected.'); 358 | 359 | // <---- 360 | } 361 | ``` 362 | ##### Description 363 | Callback for when client disconnects from server. Must be defined in `host.js` sketch. `data` parameter provides: 364 | * `.id` String: unique ID of client 365 | 366 | ##### Syntax 367 | ```javascript 368 | onClientDisconnect (data) 369 | ``` 370 | 371 | ##### Parameters 372 | `data` Object: contains client connection data 373 | 374 | ##### Returns 375 | `None` 376 | 377 | ----- 378 | 379 | #### onReceiveData() - *Host* 380 | [[Back to top]](#reference) 381 | 382 | ##### Example 383 | ```javascript 384 | function onReceiveData (data) { 385 | // Input data processing here. ---> 386 | console.log(data); 387 | 388 | // <--- 389 | 390 | /* Example: 391 | if (data.type === 'myDataType') { 392 | processMyData(data); 393 | } 394 | 395 | Use `data.type` to get the message type sent by client. 396 | */ 397 | 398 | } 399 | ``` 400 | ##### Description 401 | Callback for when data is received from a client. Must be defined in `host.js` sketch. `data` parameter provides all data sent from client and always includes: 402 | * `.id` String: unique ID of client 403 | * `.type` String: data type of message 404 | 405 | ##### Syntax 406 | ```javascript 407 | onReceiveData (data) 408 | ``` 409 | 410 | ##### Parameters 411 | `data` Object: contains all data sent from client 412 | 413 | ##### Returns 414 | `None` 415 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "p5.multiplayer", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "accepts": { 8 | "version": "1.3.7", 9 | "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", 10 | "integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==", 11 | "requires": { 12 | "mime-types": "~2.1.24", 13 | "negotiator": "0.6.2" 14 | } 15 | }, 16 | "after": { 17 | "version": "0.8.2", 18 | "resolved": "https://registry.npmjs.org/after/-/after-0.8.2.tgz", 19 | "integrity": "sha1-/ts5T58OAqqXaOcCvaI7UF+ufh8=" 20 | }, 21 | "array-flatten": { 22 | "version": "1.1.1", 23 | "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", 24 | "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" 25 | }, 26 | "arraybuffer.slice": { 27 | "version": "0.0.7", 28 | "resolved": "https://registry.npmjs.org/arraybuffer.slice/-/arraybuffer.slice-0.0.7.tgz", 29 | "integrity": "sha512-wGUIVQXuehL5TCqQun8OW81jGzAWycqzFF8lFp+GOM5BXLYj3bKNsYC4daB7n6XjCqxQA/qgTJ+8ANR3acjrog==" 30 | }, 31 | "async-limiter": { 32 | "version": "1.0.1", 33 | "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz", 34 | "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==" 35 | }, 36 | "backo2": { 37 | "version": "1.0.2", 38 | "resolved": "https://registry.npmjs.org/backo2/-/backo2-1.0.2.tgz", 39 | "integrity": "sha1-MasayLEpNjRj41s+u2n038+6eUc=" 40 | }, 41 | "base64-arraybuffer": { 42 | "version": "0.1.5", 43 | "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz", 44 | "integrity": "sha1-c5JncZI7Whl0etZmqlzUv5xunOg=" 45 | }, 46 | "base64id": { 47 | "version": "1.0.0", 48 | "resolved": "https://registry.npmjs.org/base64id/-/base64id-1.0.0.tgz", 49 | "integrity": "sha1-R2iMuZu2gE8OBtPnY7HDLlfY5rY=" 50 | }, 51 | "better-assert": { 52 | "version": "1.0.2", 53 | "resolved": "https://registry.npmjs.org/better-assert/-/better-assert-1.0.2.tgz", 54 | "integrity": "sha1-QIZrnhueC1W0gYlDEeaPr/rrxSI=", 55 | "requires": { 56 | "callsite": "1.0.0" 57 | } 58 | }, 59 | "blob": { 60 | "version": "0.0.5", 61 | "resolved": "https://registry.npmjs.org/blob/-/blob-0.0.5.tgz", 62 | "integrity": "sha512-gaqbzQPqOoamawKg0LGVd7SzLgXS+JH61oWprSLH+P+abTczqJbhTR8CmJ2u9/bUYNmHTGJx/UEmn6doAvvuig==" 63 | }, 64 | "body-parser": { 65 | "version": "1.19.0", 66 | "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz", 67 | "integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==", 68 | "requires": { 69 | "bytes": "3.1.0", 70 | "content-type": "~1.0.4", 71 | "debug": "2.6.9", 72 | "depd": "~1.1.2", 73 | "http-errors": "1.7.2", 74 | "iconv-lite": "0.4.24", 75 | "on-finished": "~2.3.0", 76 | "qs": "6.7.0", 77 | "raw-body": "2.4.0", 78 | "type-is": "~1.6.17" 79 | } 80 | }, 81 | "bytes": { 82 | "version": "3.1.0", 83 | "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", 84 | "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==" 85 | }, 86 | "callsite": { 87 | "version": "1.0.0", 88 | "resolved": "https://registry.npmjs.org/callsite/-/callsite-1.0.0.tgz", 89 | "integrity": "sha1-KAOY5dZkvXQDi28JBRU+borxvCA=" 90 | }, 91 | "component-bind": { 92 | "version": "1.0.0", 93 | "resolved": "https://registry.npmjs.org/component-bind/-/component-bind-1.0.0.tgz", 94 | "integrity": "sha1-AMYIq33Nk4l8AAllGx06jh5zu9E=" 95 | }, 96 | "component-emitter": { 97 | "version": "1.2.1", 98 | "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", 99 | "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=" 100 | }, 101 | "component-inherit": { 102 | "version": "0.0.3", 103 | "resolved": "https://registry.npmjs.org/component-inherit/-/component-inherit-0.0.3.tgz", 104 | "integrity": "sha1-ZF/ErfWLcrZJ1crmUTVhnbJv8UM=" 105 | }, 106 | "content-disposition": { 107 | "version": "0.5.3", 108 | "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz", 109 | "integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==", 110 | "requires": { 111 | "safe-buffer": "5.1.2" 112 | } 113 | }, 114 | "content-type": { 115 | "version": "1.0.4", 116 | "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", 117 | "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" 118 | }, 119 | "cookie": { 120 | "version": "0.4.0", 121 | "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", 122 | "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==" 123 | }, 124 | "cookie-signature": { 125 | "version": "1.0.6", 126 | "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", 127 | "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" 128 | }, 129 | "debug": { 130 | "version": "2.6.9", 131 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", 132 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", 133 | "requires": { 134 | "ms": "2.0.0" 135 | } 136 | }, 137 | "depd": { 138 | "version": "1.1.2", 139 | "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", 140 | "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" 141 | }, 142 | "destroy": { 143 | "version": "1.0.4", 144 | "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", 145 | "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" 146 | }, 147 | "ee-first": { 148 | "version": "1.1.1", 149 | "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", 150 | "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" 151 | }, 152 | "encodeurl": { 153 | "version": "1.0.2", 154 | "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", 155 | "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" 156 | }, 157 | "engine.io": { 158 | "version": "3.3.2", 159 | "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-3.3.2.tgz", 160 | "integrity": "sha512-AsaA9KG7cWPXWHp5FvHdDWY3AMWeZ8x+2pUVLcn71qE5AtAzgGbxuclOytygskw8XGmiQafTmnI9Bix3uihu2w==", 161 | "requires": { 162 | "accepts": "~1.3.4", 163 | "base64id": "1.0.0", 164 | "cookie": "0.3.1", 165 | "debug": "~3.1.0", 166 | "engine.io-parser": "~2.1.0", 167 | "ws": "~6.1.0" 168 | }, 169 | "dependencies": { 170 | "cookie": { 171 | "version": "0.3.1", 172 | "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz", 173 | "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=" 174 | }, 175 | "debug": { 176 | "version": "3.1.0", 177 | "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", 178 | "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", 179 | "requires": { 180 | "ms": "2.0.0" 181 | } 182 | } 183 | } 184 | }, 185 | "engine.io-client": { 186 | "version": "3.3.2", 187 | "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-3.3.2.tgz", 188 | "integrity": "sha512-y0CPINnhMvPuwtqXfsGuWE8BB66+B6wTtCofQDRecMQPYX3MYUZXFNKDhdrSe3EVjgOu4V3rxdeqN/Tr91IgbQ==", 189 | "requires": { 190 | "component-emitter": "1.2.1", 191 | "component-inherit": "0.0.3", 192 | "debug": "~3.1.0", 193 | "engine.io-parser": "~2.1.1", 194 | "has-cors": "1.1.0", 195 | "indexof": "0.0.1", 196 | "parseqs": "0.0.5", 197 | "parseuri": "0.0.5", 198 | "ws": "~6.1.0", 199 | "xmlhttprequest-ssl": "~1.5.4", 200 | "yeast": "0.1.2" 201 | }, 202 | "dependencies": { 203 | "debug": { 204 | "version": "3.1.0", 205 | "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", 206 | "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", 207 | "requires": { 208 | "ms": "2.0.0" 209 | } 210 | } 211 | } 212 | }, 213 | "engine.io-parser": { 214 | "version": "2.1.3", 215 | "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-2.1.3.tgz", 216 | "integrity": "sha512-6HXPre2O4Houl7c4g7Ic/XzPnHBvaEmN90vtRO9uLmwtRqQmTOw0QMevL1TOfL2Cpu1VzsaTmMotQgMdkzGkVA==", 217 | "requires": { 218 | "after": "0.8.2", 219 | "arraybuffer.slice": "~0.0.7", 220 | "base64-arraybuffer": "0.1.5", 221 | "blob": "0.0.5", 222 | "has-binary2": "~1.0.2" 223 | } 224 | }, 225 | "escape-html": { 226 | "version": "1.0.3", 227 | "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", 228 | "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" 229 | }, 230 | "etag": { 231 | "version": "1.8.1", 232 | "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", 233 | "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" 234 | }, 235 | "express": { 236 | "version": "4.17.1", 237 | "resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz", 238 | "integrity": "sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==", 239 | "requires": { 240 | "accepts": "~1.3.7", 241 | "array-flatten": "1.1.1", 242 | "body-parser": "1.19.0", 243 | "content-disposition": "0.5.3", 244 | "content-type": "~1.0.4", 245 | "cookie": "0.4.0", 246 | "cookie-signature": "1.0.6", 247 | "debug": "2.6.9", 248 | "depd": "~1.1.2", 249 | "encodeurl": "~1.0.2", 250 | "escape-html": "~1.0.3", 251 | "etag": "~1.8.1", 252 | "finalhandler": "~1.1.2", 253 | "fresh": "0.5.2", 254 | "merge-descriptors": "1.0.1", 255 | "methods": "~1.1.2", 256 | "on-finished": "~2.3.0", 257 | "parseurl": "~1.3.3", 258 | "path-to-regexp": "0.1.7", 259 | "proxy-addr": "~2.0.5", 260 | "qs": "6.7.0", 261 | "range-parser": "~1.2.1", 262 | "safe-buffer": "5.1.2", 263 | "send": "0.17.1", 264 | "serve-static": "1.14.1", 265 | "setprototypeof": "1.1.1", 266 | "statuses": "~1.5.0", 267 | "type-is": "~1.6.18", 268 | "utils-merge": "1.0.1", 269 | "vary": "~1.1.2" 270 | } 271 | }, 272 | "finalhandler": { 273 | "version": "1.1.2", 274 | "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", 275 | "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", 276 | "requires": { 277 | "debug": "2.6.9", 278 | "encodeurl": "~1.0.2", 279 | "escape-html": "~1.0.3", 280 | "on-finished": "~2.3.0", 281 | "parseurl": "~1.3.3", 282 | "statuses": "~1.5.0", 283 | "unpipe": "~1.0.0" 284 | } 285 | }, 286 | "forwarded": { 287 | "version": "0.1.2", 288 | "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", 289 | "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=" 290 | }, 291 | "fresh": { 292 | "version": "0.5.2", 293 | "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", 294 | "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" 295 | }, 296 | "has-binary2": { 297 | "version": "1.0.3", 298 | "resolved": "https://registry.npmjs.org/has-binary2/-/has-binary2-1.0.3.tgz", 299 | "integrity": "sha512-G1LWKhDSvhGeAQ8mPVQlqNcOB2sJdwATtZKl2pDKKHfpf/rYj24lkinxf69blJbnsvtqqNU+L3SL50vzZhXOnw==", 300 | "requires": { 301 | "isarray": "2.0.1" 302 | } 303 | }, 304 | "has-cors": { 305 | "version": "1.1.0", 306 | "resolved": "https://registry.npmjs.org/has-cors/-/has-cors-1.1.0.tgz", 307 | "integrity": "sha1-XkdHk/fqmEPRu5nCPu9J/xJv/zk=" 308 | }, 309 | "http-errors": { 310 | "version": "1.7.2", 311 | "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", 312 | "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==", 313 | "requires": { 314 | "depd": "~1.1.2", 315 | "inherits": "2.0.3", 316 | "setprototypeof": "1.1.1", 317 | "statuses": ">= 1.5.0 < 2", 318 | "toidentifier": "1.0.0" 319 | } 320 | }, 321 | "iconv-lite": { 322 | "version": "0.4.24", 323 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", 324 | "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", 325 | "requires": { 326 | "safer-buffer": ">= 2.1.2 < 3" 327 | } 328 | }, 329 | "indexof": { 330 | "version": "0.0.1", 331 | "resolved": "https://registry.npmjs.org/indexof/-/indexof-0.0.1.tgz", 332 | "integrity": "sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10=" 333 | }, 334 | "inherits": { 335 | "version": "2.0.3", 336 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", 337 | "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" 338 | }, 339 | "ipaddr.js": { 340 | "version": "1.9.0", 341 | "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.0.tgz", 342 | "integrity": "sha512-M4Sjn6N/+O6/IXSJseKqHoFc+5FdGJ22sXqnjTpdZweHK64MzEPAyQZyEU3R/KRv2GLoa7nNtg/C2Ev6m7z+eA==" 343 | }, 344 | "isarray": { 345 | "version": "2.0.1", 346 | "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz", 347 | "integrity": "sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4=" 348 | }, 349 | "media-typer": { 350 | "version": "0.3.0", 351 | "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", 352 | "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" 353 | }, 354 | "merge-descriptors": { 355 | "version": "1.0.1", 356 | "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", 357 | "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" 358 | }, 359 | "methods": { 360 | "version": "1.1.2", 361 | "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", 362 | "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" 363 | }, 364 | "mime": { 365 | "version": "1.6.0", 366 | "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", 367 | "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==" 368 | }, 369 | "mime-db": { 370 | "version": "1.40.0", 371 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.40.0.tgz", 372 | "integrity": "sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA==" 373 | }, 374 | "mime-types": { 375 | "version": "2.1.24", 376 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.24.tgz", 377 | "integrity": "sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ==", 378 | "requires": { 379 | "mime-db": "1.40.0" 380 | } 381 | }, 382 | "ms": { 383 | "version": "2.0.0", 384 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 385 | "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" 386 | }, 387 | "negotiator": { 388 | "version": "0.6.2", 389 | "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", 390 | "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==" 391 | }, 392 | "object-component": { 393 | "version": "0.0.3", 394 | "resolved": "https://registry.npmjs.org/object-component/-/object-component-0.0.3.tgz", 395 | "integrity": "sha1-8MaapQ78lbhmwYb0AKM3acsvEpE=" 396 | }, 397 | "on-finished": { 398 | "version": "2.3.0", 399 | "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", 400 | "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", 401 | "requires": { 402 | "ee-first": "1.1.1" 403 | } 404 | }, 405 | "parseqs": { 406 | "version": "0.0.5", 407 | "resolved": "https://registry.npmjs.org/parseqs/-/parseqs-0.0.5.tgz", 408 | "integrity": "sha1-1SCKNzjkZ2bikbouoXNoSSGouJ0=", 409 | "requires": { 410 | "better-assert": "~1.0.0" 411 | } 412 | }, 413 | "parseuri": { 414 | "version": "0.0.5", 415 | "resolved": "https://registry.npmjs.org/parseuri/-/parseuri-0.0.5.tgz", 416 | "integrity": "sha1-gCBKUNTbt3m/3G6+J3jZDkvOMgo=", 417 | "requires": { 418 | "better-assert": "~1.0.0" 419 | } 420 | }, 421 | "parseurl": { 422 | "version": "1.3.3", 423 | "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", 424 | "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" 425 | }, 426 | "path-to-regexp": { 427 | "version": "0.1.7", 428 | "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", 429 | "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" 430 | }, 431 | "proxy-addr": { 432 | "version": "2.0.5", 433 | "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.5.tgz", 434 | "integrity": "sha512-t/7RxHXPH6cJtP0pRG6smSr9QJidhB+3kXu0KgXnbGYMgzEnUxRQ4/LDdfOwZEMyIh3/xHb8PX3t+lfL9z+YVQ==", 435 | "requires": { 436 | "forwarded": "~0.1.2", 437 | "ipaddr.js": "1.9.0" 438 | } 439 | }, 440 | "qs": { 441 | "version": "6.7.0", 442 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", 443 | "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==" 444 | }, 445 | "range-parser": { 446 | "version": "1.2.1", 447 | "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", 448 | "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==" 449 | }, 450 | "raw-body": { 451 | "version": "2.4.0", 452 | "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz", 453 | "integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==", 454 | "requires": { 455 | "bytes": "3.1.0", 456 | "http-errors": "1.7.2", 457 | "iconv-lite": "0.4.24", 458 | "unpipe": "1.0.0" 459 | } 460 | }, 461 | "safe-buffer": { 462 | "version": "5.1.2", 463 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", 464 | "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" 465 | }, 466 | "safer-buffer": { 467 | "version": "2.1.2", 468 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", 469 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" 470 | }, 471 | "send": { 472 | "version": "0.17.1", 473 | "resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz", 474 | "integrity": "sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==", 475 | "requires": { 476 | "debug": "2.6.9", 477 | "depd": "~1.1.2", 478 | "destroy": "~1.0.4", 479 | "encodeurl": "~1.0.2", 480 | "escape-html": "~1.0.3", 481 | "etag": "~1.8.1", 482 | "fresh": "0.5.2", 483 | "http-errors": "~1.7.2", 484 | "mime": "1.6.0", 485 | "ms": "2.1.1", 486 | "on-finished": "~2.3.0", 487 | "range-parser": "~1.2.1", 488 | "statuses": "~1.5.0" 489 | }, 490 | "dependencies": { 491 | "ms": { 492 | "version": "2.1.1", 493 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", 494 | "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" 495 | } 496 | } 497 | }, 498 | "serve-static": { 499 | "version": "1.14.1", 500 | "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz", 501 | "integrity": "sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==", 502 | "requires": { 503 | "encodeurl": "~1.0.2", 504 | "escape-html": "~1.0.3", 505 | "parseurl": "~1.3.3", 506 | "send": "0.17.1" 507 | } 508 | }, 509 | "setprototypeof": { 510 | "version": "1.1.1", 511 | "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", 512 | "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==" 513 | }, 514 | "socket.io": { 515 | "version": "2.2.0", 516 | "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-2.2.0.tgz", 517 | "integrity": "sha512-wxXrIuZ8AILcn+f1B4ez4hJTPG24iNgxBBDaJfT6MsyOhVYiTXWexGoPkd87ktJG8kQEcL/NBvRi64+9k4Kc0w==", 518 | "requires": { 519 | "debug": "~4.1.0", 520 | "engine.io": "~3.3.1", 521 | "has-binary2": "~1.0.2", 522 | "socket.io-adapter": "~1.1.0", 523 | "socket.io-client": "2.2.0", 524 | "socket.io-parser": "~3.3.0" 525 | }, 526 | "dependencies": { 527 | "debug": { 528 | "version": "4.1.1", 529 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", 530 | "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", 531 | "requires": { 532 | "ms": "^2.1.1" 533 | } 534 | }, 535 | "ms": { 536 | "version": "2.1.2", 537 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", 538 | "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" 539 | } 540 | } 541 | }, 542 | "socket.io-adapter": { 543 | "version": "1.1.1", 544 | "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-1.1.1.tgz", 545 | "integrity": "sha1-KoBeihTWNyEk3ZFZrUUC+MsH8Gs=" 546 | }, 547 | "socket.io-client": { 548 | "version": "2.2.0", 549 | "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-2.2.0.tgz", 550 | "integrity": "sha512-56ZrkTDbdTLmBIyfFYesgOxsjcLnwAKoN4CiPyTVkMQj3zTUh0QAx3GbvIvLpFEOvQWu92yyWICxB0u7wkVbYA==", 551 | "requires": { 552 | "backo2": "1.0.2", 553 | "base64-arraybuffer": "0.1.5", 554 | "component-bind": "1.0.0", 555 | "component-emitter": "1.2.1", 556 | "debug": "~3.1.0", 557 | "engine.io-client": "~3.3.1", 558 | "has-binary2": "~1.0.2", 559 | "has-cors": "1.1.0", 560 | "indexof": "0.0.1", 561 | "object-component": "0.0.3", 562 | "parseqs": "0.0.5", 563 | "parseuri": "0.0.5", 564 | "socket.io-parser": "~3.3.0", 565 | "to-array": "0.1.4" 566 | }, 567 | "dependencies": { 568 | "debug": { 569 | "version": "3.1.0", 570 | "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", 571 | "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", 572 | "requires": { 573 | "ms": "2.0.0" 574 | } 575 | } 576 | } 577 | }, 578 | "socket.io-parser": { 579 | "version": "3.3.0", 580 | "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.3.0.tgz", 581 | "integrity": "sha512-hczmV6bDgdaEbVqhAeVMM/jfUfzuEZHsQg6eOmLgJht6G3mPKMxYm75w2+qhAQZ+4X+1+ATZ+QFKeOZD5riHng==", 582 | "requires": { 583 | "component-emitter": "1.2.1", 584 | "debug": "~3.1.0", 585 | "isarray": "2.0.1" 586 | }, 587 | "dependencies": { 588 | "debug": { 589 | "version": "3.1.0", 590 | "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", 591 | "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", 592 | "requires": { 593 | "ms": "2.0.0" 594 | } 595 | } 596 | } 597 | }, 598 | "statuses": { 599 | "version": "1.5.0", 600 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", 601 | "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" 602 | }, 603 | "to-array": { 604 | "version": "0.1.4", 605 | "resolved": "https://registry.npmjs.org/to-array/-/to-array-0.1.4.tgz", 606 | "integrity": "sha1-F+bBH3PdTz10zaek/zI46a2b+JA=" 607 | }, 608 | "toidentifier": { 609 | "version": "1.0.0", 610 | "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", 611 | "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==" 612 | }, 613 | "type-is": { 614 | "version": "1.6.18", 615 | "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", 616 | "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", 617 | "requires": { 618 | "media-typer": "0.3.0", 619 | "mime-types": "~2.1.24" 620 | } 621 | }, 622 | "unpipe": { 623 | "version": "1.0.0", 624 | "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", 625 | "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" 626 | }, 627 | "utils-merge": { 628 | "version": "1.0.1", 629 | "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", 630 | "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" 631 | }, 632 | "vary": { 633 | "version": "1.1.2", 634 | "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", 635 | "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" 636 | }, 637 | "ws": { 638 | "version": "6.1.4", 639 | "resolved": "https://registry.npmjs.org/ws/-/ws-6.1.4.tgz", 640 | "integrity": "sha512-eqZfL+NE/YQc1/ZynhojeV8q+H050oR8AZ2uIev7RU10svA9ZnJUddHcOUZTJLinZ9yEfdA2kSATS2qZK5fhJA==", 641 | "requires": { 642 | "async-limiter": "~1.0.0" 643 | } 644 | }, 645 | "xmlhttprequest-ssl": { 646 | "version": "1.5.5", 647 | "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.5.5.tgz", 648 | "integrity": "sha1-wodrBhaKrcQOV9l+gRkayPQ5iz4=" 649 | }, 650 | "yeast": { 651 | "version": "0.1.2", 652 | "resolved": "https://registry.npmjs.org/yeast/-/yeast-0.1.2.tgz", 653 | "integrity": "sha1-AI4G2AlDIMNy28L47XagymyKxBk=" 654 | } 655 | } 656 | } 657 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "p5.multiplayer", 3 | "version": "1.0.0", 4 | "description": "p5.multiplayer example and template", 5 | "main": "server.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "start": "node server.js" 9 | }, 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "express": "^4.16.4", 14 | "socket.io": "^2.2.0" 15 | }, 16 | "engines": { 17 | "node": "10.x" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /public/host.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /public/host.js: -------------------------------------------------------------------------------- 1 | /* 2 | p5.multiplayer - HOST 3 | 4 | This 'host' sketch is intended to be run in desktop browsers. 5 | It connects to a node server via socket.io, from which it receives 6 | rerouted input data from all connected 'clients'. 7 | 8 | Navigate to the project's 'public' directory. 9 | Run http-server -c-1 to start server. This will default to port 8080. 10 | Run http-server -c-1 -p80 to start server on open port 80. 11 | 12 | */ 13 | 14 | //////////// 15 | // Network Settings 16 | // const serverIp = 'https://yourservername.herokuapp.com'; 17 | // const serverIp = 'https://yourprojectname.glitch.me'; 18 | const serverIp = '127.0.0.1'; 19 | const serverPort = '3000'; 20 | const local = true; // true if running locally, false 21 | // if running on remote server 22 | 23 | // Global variables here. ----> 24 | 25 | const velScale = 10; 26 | const debug = true; 27 | let game; 28 | 29 | // <---- 30 | 31 | function preload() { 32 | setupHost(); 33 | } 34 | 35 | function setup () { 36 | createCanvas(windowWidth, windowHeight); 37 | 38 | // Host/Game setup here. ----> 39 | 40 | game = new Game(width, height); 41 | 42 | // <---- 43 | } 44 | 45 | function windowResized() { 46 | resizeCanvas(windowWidth, windowHeight); 47 | } 48 | 49 | function draw () { 50 | background(15); 51 | 52 | if(isHostConnected(display=true)) { 53 | // Host/Game draw here. ---> 54 | 55 | // Display player IDs in top left corner 56 | game.printPlayerIds(5, 20); 57 | 58 | // Update and draw game objects 59 | game.draw(); 60 | 61 | // <---- 62 | 63 | // Display server address 64 | displayAddress(); 65 | } 66 | } 67 | 68 | function onClientConnect (data) { 69 | // Client connect logic here. ---> 70 | console.log(data.id + ' has connected.'); 71 | 72 | if (!game.checkId(data.id)) { 73 | game.add(data.id, 74 | random(0.25*width, 0.75*width), 75 | random(0.25*height, 0.75*height), 76 | 60, 60 77 | ); 78 | } 79 | 80 | // <---- 81 | } 82 | 83 | function onClientDisconnect (data) { 84 | // Client disconnect logic here. ---> 85 | 86 | if (game.checkId(data.id)) { 87 | game.remove(data.id); 88 | } 89 | 90 | // <---- 91 | } 92 | 93 | function onReceiveData (data) { 94 | // Input data processing here. ---> 95 | console.log(data); 96 | 97 | if (data.type === 'joystick') { 98 | processJoystick(data); 99 | } 100 | else if (data.type === 'button') { 101 | processButton(data); 102 | } 103 | else if (data.type === 'playerColor') { 104 | game.setColor(data.id, data.r*255, data.g*255, data.b*255); 105 | } 106 | 107 | // <---- 108 | 109 | /* Example: 110 | if (data.type === 'myDataType') { 111 | processMyData(data); 112 | } 113 | 114 | Use `data.type` to get the message type sent by client. 115 | */ 116 | } 117 | 118 | // This is included for testing purposes to demonstrate that 119 | // messages can be sent from a host back to all connected clients 120 | function mousePressed() { 121 | sendData('timestamp', { timestamp: millis() }); 122 | } 123 | 124 | //////////// 125 | // Input processing 126 | function processJoystick (data) { 127 | 128 | game.setVelocity(data.id, data.joystickX*velScale, -data.joystickY*velScale); 129 | 130 | if (debug) { 131 | console.log(data.id + ': {' + 132 | data.joystickX + ',' + 133 | data.joystickY + '}'); 134 | } 135 | } 136 | 137 | function processButton (data) { 138 | game.players[data.id].val = data.button; 139 | 140 | game.createRipple(data.id, 300, 1000); 141 | 142 | if (debug) { 143 | console.log(data.id + ': ' + 144 | data.button); 145 | } 146 | } 147 | 148 | //////////// 149 | // Game 150 | // This simple placeholder game makes use of p5.play 151 | class Game { 152 | constructor (w, h) { 153 | this.w = w; 154 | this.h = h; 155 | this.players = {}; 156 | this.numPlayers = 0; 157 | this.id = 0; 158 | this.colliders = new Group(); 159 | this.ripples = new Ripples(); 160 | } 161 | 162 | add (id, x, y, w, h) { 163 | this.players[id] = createSprite(x, y, w, h); 164 | this.players[id].id = "p"+this.id; 165 | this.players[id].setCollider("rectangle", 0, 0, w, h); 166 | this.players[id].color = color(255, 255, 255); 167 | this.players[id].shapeColor = color(255, 255, 255); 168 | this.players[id].scale = 1; 169 | this.players[id].mass = 1; 170 | this.colliders.add(this.players[id]); 171 | print(this.players[id].id + " added."); 172 | this.id++; 173 | this.numPlayers++; 174 | } 175 | 176 | draw() { 177 | this.checkBounds(); 178 | this.ripples.draw(); 179 | drawSprites(); 180 | } 181 | 182 | createRipple(id, r, duration) { 183 | this.ripples.add( 184 | this.players[id].position.x, 185 | this.players[id].position.y, 186 | r, 187 | duration, 188 | this.players[id].color); 189 | } 190 | 191 | setColor (id, r, g, b) { 192 | this.players[id].color = color(r, g, b); 193 | this.players[id].shapeColor = color(r, g, b); 194 | 195 | print(this.players[id].id + " color added."); 196 | } 197 | 198 | remove (id) { 199 | this.colliders.remove(this.players[id]); 200 | this.players[id].remove(); 201 | delete this.players[id]; 202 | this.numPlayers--; 203 | } 204 | 205 | checkId (id) { 206 | if (id in this.players) { return true; } 207 | else { return false; } 208 | } 209 | 210 | printPlayerIds (x, y) { 211 | push(); 212 | noStroke(); 213 | fill(255); 214 | textSize(16); 215 | text("# players: " + this.numPlayers, x, y); 216 | 217 | y = y + 16; 218 | fill(200); 219 | for (let id in this.players) { 220 | text(this.players[id].id, x, y); 221 | y += 16; 222 | } 223 | 224 | pop(); 225 | } 226 | 227 | setVelocity(id, velx, vely) { 228 | this.players[id].velocity.x = velx; 229 | this.players[id].velocity.y = vely; 230 | } 231 | 232 | checkBounds() { 233 | for (let id in this.players) { 234 | 235 | if (this.players[id].position.x < 0) { 236 | this.players[id].position.x = this.w - 1; 237 | } 238 | 239 | if (this.players[id].position.x > this.w) { 240 | this.players[id].position.x = 1; 241 | } 242 | 243 | if (this.players[id].position.y < 0) { 244 | this.players[id].position.y = this.h - 1; 245 | } 246 | 247 | if (this.players[id].position.y > this.h) { 248 | this.players[id].position.y = 1; 249 | } 250 | } 251 | } 252 | } 253 | 254 | // A simple pair of classes for generating ripples 255 | class Ripples { 256 | constructor() { 257 | this.ripples = []; 258 | } 259 | 260 | add(x, y, r, duration, rcolor) { 261 | this.ripples.push(new Ripple(x, y, r, duration, rcolor)); 262 | } 263 | 264 | draw() { 265 | for (let i = 0; i < this.ripples.length; i++) { 266 | // Draw each ripple in the array 267 | if(this.ripples[i].draw()) { 268 | // If the ripple is finished (returns true), remove it 269 | this.ripples.splice(i, 1); 270 | } 271 | } 272 | } 273 | } 274 | 275 | class Ripple { 276 | constructor(x, y, r, duration, rcolor) { 277 | this.x = x; 278 | this.y = y; 279 | this.r = r; 280 | 281 | // If rcolor is not defined, default to white 282 | if (rcolor == null) { 283 | rcolor = color(255); 284 | } 285 | 286 | this.stroke = rcolor; 287 | this.strokeWeight = 3; 288 | 289 | this.duration = duration; // in milliseconds 290 | this.startTime = millis(); 291 | this.endTime = this.startTime + this.duration; 292 | } 293 | 294 | draw() { 295 | let progress = (this.endTime - millis())/this.duration; 296 | let r = this.r*(1 - progress); 297 | 298 | push(); 299 | stroke(red(this.stroke), 300 | green(this.stroke), 301 | blue(this.stroke), 302 | 255*progress); 303 | strokeWeight(this.strokeWeight); 304 | fill(0, 0); 305 | ellipse(this.x, this.y, r); 306 | pop(); 307 | 308 | if (millis() > this.endTime) { 309 | return true; 310 | } 311 | 312 | return false; 313 | } 314 | } -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /public/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | p5.multiplayer - CLIENT 3 | 4 | This 'client' sketch is intended to be run in either mobile or 5 | desktop browsers. It sends a basic joystick and button input data 6 | to a node server via socket.io. This data is then rerouted to a 7 | 'host' sketch, which displays all connected 'clients'. 8 | 9 | Navigate to the project's 'public' directory. 10 | Run http-server -c-1 to start server. This will default to port 8080. 11 | Run http-server -c-1 -p80 to start server on open port 80. 12 | 13 | */ 14 | 15 | //////////// 16 | // Network Settings 17 | // const serverIp = 'https://yourservername.herokuapp.com'; 18 | // const serverIp = 'https://yourprojectname.glitch.me'; 19 | const serverIp = '127.0.0.1'; 20 | const serverPort = '3000'; 21 | const local = true; // true if running locally, false 22 | // if running on remote server 23 | 24 | // Global variables here. ----> 25 | 26 | // Initialize GUI related variables 27 | let gui = null; 28 | let button = null; 29 | let joystick = null; 30 | let joystickRes = 4; 31 | let thisJ = {x: 0, y: 0}; 32 | let prevJ = {x: 0, y: 0}; 33 | 34 | // Initialize Game related variables 35 | let playerColor; 36 | let playerColorDim; 37 | 38 | // <---- 39 | 40 | function preload() { 41 | setupClient(); 42 | } 43 | 44 | function setup() { 45 | createCanvas(windowWidth, windowHeight); 46 | 47 | // Client setup here. ----> 48 | 49 | gui = createGui(); 50 | 51 | setPlayerColors(); 52 | setupUI(); 53 | 54 | // <---- 55 | 56 | // Send any initial setup data to your host here. 57 | /* 58 | Example: 59 | sendData('myDataType', { 60 | val1: 0, 61 | val2: 128, 62 | val3: true 63 | }); 64 | 65 | Use `type` to classify message types for host. 66 | */ 67 | sendData('playerColor', { 68 | r: red(playerColor)/255, 69 | g: green(playerColor)/255, 70 | b: blue(playerColor)/255 71 | }); 72 | } 73 | 74 | function windowResized() { 75 | resizeCanvas(windowWidth, windowHeight); 76 | } 77 | 78 | function draw() { 79 | background(0); 80 | 81 | if(isClientConnected(display=true)) { 82 | // Client draw here. ----> 83 | 84 | drawGui(); 85 | 86 | // <--- 87 | } 88 | } 89 | 90 | // Messages can be sent from a host to all connected clients 91 | function onReceiveData (data) { 92 | // Input data processing here. ---> 93 | 94 | if (data.type === 'timestamp') { 95 | print(data.timestamp); 96 | } 97 | 98 | // <---- 99 | 100 | /* Example: 101 | if (data.type === 'myDataType') { 102 | processMyData(data); 103 | } 104 | 105 | Use `data.type` to get the message type sent by host. 106 | */ 107 | } 108 | 109 | //////////// 110 | // GUI setup 111 | function setPlayerColors() { 112 | let hue = random(0, 360); 113 | colorMode(HSB); 114 | playerColor = color(hue, 100, 100); 115 | playerColorDim = color(hue, 100, 75); 116 | colorMode(RGB); 117 | } 118 | 119 | function setupUI() { 120 | // Temp variables for calculating GUI object positions 121 | let jX, jY, jW, jH, bX, bY, bW, bH; 122 | 123 | // Rudimentary calculation based on portrait or landscape 124 | if (width < height) { 125 | jX = 0.05*width; 126 | jY = 0.05*height; 127 | jW = 0.9*width; 128 | jH = 0.9*width; 129 | 130 | bX = 0.05*windowWidth; 131 | bY = 0.75*windowHeight; 132 | bW = 0.9*windowWidth; 133 | bH = 0.2*windowHeight; 134 | } 135 | else { 136 | jX = 0.05*width; 137 | jY = 0.05*height; 138 | jW = 0.9*height; 139 | jH = 0.9*height; 140 | 141 | bX = 0.75*windowWidth; 142 | bY = 0.05*windowHeight; 143 | bW = 0.2*windowWidth; 144 | bH = 0.9*windowHeight; 145 | } 146 | 147 | // Create joystick and button, stylize with player colors 148 | joystick = createJoystick("Joystick", jX, jY, jW, jH); 149 | joystick.setStyle({ 150 | handleRadius: joystick.w*0.2, 151 | fillBg: color(0), 152 | fillBgHover: color(0), 153 | fillBgActive: color(0), 154 | strokeBg: playerColor, 155 | strokeBgHover: playerColor, 156 | strokeBgActive: playerColor, 157 | fillHandle: playerColorDim, 158 | fillHandleHover: playerColorDim, 159 | fillHandleActive: playerColor, 160 | strokeHandleHover: color(255), 161 | strokeHandleActive: color(255) 162 | }); 163 | joystick.onChange = onJoystickChange; 164 | 165 | button = createButton("Interact", bX, bY, bW, bH); 166 | button.setStyle({ 167 | textSize: 40, 168 | fillBg: playerColorDim, 169 | fillBgHover: playerColorDim, 170 | fillBgActive: playerColor 171 | }); 172 | button.onPress = onButtonPress; 173 | } 174 | 175 | //////////// 176 | // Input processing 177 | function onJoystickChange() { 178 | thisJ.x = floor(joystick.val.x*joystickRes)/joystickRes; 179 | thisJ.y = floor(joystick.val.y*joystickRes)/joystickRes; 180 | 181 | if (thisJ.x != prevJ.x || thisJ.y != prevJ.y) { 182 | let data = { 183 | joystickX: thisJ.x, 184 | joystickY: thisJ.y 185 | } 186 | sendData('joystick', data); 187 | } 188 | 189 | prevJ.x = thisJ.x; 190 | prevJ.y = thisJ.y; 191 | } 192 | 193 | function onButtonPress() { 194 | let data = { 195 | button: button.val 196 | } 197 | 198 | sendData('button', data); 199 | } 200 | 201 | /// Add these lines below sketch to prevent scrolling on mobile 202 | function touchMoved() { 203 | // do some stuff 204 | return false; 205 | } -------------------------------------------------------------------------------- /public/index.php: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /public/lib/p5.multiplayer.js: -------------------------------------------------------------------------------- 1 | //////////// 2 | // COMMON 3 | 4 | // Initialize Network related variables 5 | let socket; 6 | let roomId = null; 7 | let id = null; 8 | 9 | // Process URL 10 | // Used to process the room ID. In order to specify a room ID, 11 | // include ?=uniqueName, where uniqueName is replaced with the 12 | // desired unique room ID. 13 | function _processUrl() { 14 | const parameters = location.search.substring(1).split("&"); 15 | 16 | const temp = parameters[0].split("="); 17 | roomId = unescape(temp[1]); 18 | 19 | console.log("id: " + roomId); 20 | } 21 | 22 | // Send data from client to host via server 23 | function sendData(datatype, data) { 24 | data.type = datatype; 25 | data.roomId = roomId; 26 | 27 | socket.emit('sendData', data); 28 | } 29 | 30 | // Displays a message while attempting connection 31 | function _displayWaiting() { 32 | push(); 33 | fill(200); 34 | textAlign(CENTER, CENTER); 35 | textSize(20); 36 | text("Attempting connection...", width/2, height/2-10); 37 | pop(); 38 | } 39 | 40 | //////////// 41 | // HOST 42 | 43 | // Initialize Network related variables 44 | let hostConnected = false; 45 | 46 | function setupHost() { 47 | _processUrl(); 48 | 49 | let addr = serverIp; 50 | if (local) { addr = serverIp + ':' + serverPort; } 51 | socket = io.connect(addr); 52 | 53 | socket.emit('join', {name: 'host', roomId: roomId}); 54 | 55 | socket.on('id', function(data) { 56 | id = data; 57 | console.log("id: " + id); 58 | }); 59 | 60 | socket.on('hostConnect', onHostConnect); 61 | socket.on('clientConnect', onClientConnect); 62 | socket.on('clientDisconnect', onClientDisconnect); 63 | socket.on('receiveData', onReceiveData); 64 | } 65 | 66 | function isHostConnected(display=false) { 67 | if (!hostConnected) { 68 | if (display) { _displayWaiting(); } 69 | return false; 70 | } 71 | return true; 72 | } 73 | 74 | function onHostConnect (data) { 75 | console.log("Host connected to server."); 76 | hostConnected = true; 77 | 78 | if (roomId === null || roomId === 'undefined') { 79 | roomId = data.roomId; 80 | } 81 | } 82 | 83 | // Displays server address in lower left of screen 84 | function displayAddress() { 85 | push(); 86 | fill(255); 87 | textSize(50); 88 | text(serverIp+"/?="+roomId, 10, height-50); 89 | pop(); 90 | } 91 | 92 | //////////// 93 | // CLIENT 94 | 95 | // Initialize Network related variables 96 | let waiting = true; 97 | let connected = false; 98 | 99 | function setupClient() { 100 | _processUrl(); 101 | 102 | // Socket.io - open a connection to the web server on specified port 103 | let addr = serverIp; 104 | if (local) { addr = serverIp + ':' + serverPort; } 105 | socket = io.connect(addr); 106 | 107 | socket.emit('join', {name: 'client', roomId: roomId}); 108 | 109 | socket.on('id', function(data) { 110 | id = data; 111 | console.log("id: " + id); 112 | }); 113 | 114 | socket.on('found', function(data) { 115 | connected = data.status; 116 | waiting = false; 117 | console.log("connected: " + connected); 118 | }) 119 | 120 | socket.emit('clientConnect', { 121 | roomId: roomId 122 | }); 123 | 124 | socket.on('receiveData', onReceiveData); 125 | } 126 | 127 | function isClientConnected(display=false) { 128 | if (waiting) { 129 | if (display) { _displayWaiting(); } 130 | return false; 131 | } 132 | else if (!connected) { 133 | if (display) { _displayInstructions(); } 134 | return false; 135 | } 136 | 137 | return true; 138 | } 139 | 140 | // Displays a message instructing player to look at host screen 141 | // for correct link. 142 | function _displayInstructions() { 143 | push(); 144 | fill(200); 145 | textAlign(CENTER, CENTER); 146 | textSize(20); 147 | text("Please enter the link at the", width/2, height/2-10); 148 | text("bottom of the host screen.", width/2, height/2+10); 149 | pop(); 150 | } 151 | -------------------------------------------------------------------------------- /public/lib/rotation.js: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////// 2 | //////////////////////////////////////// 3 | 4 | // Requires utilities.js. 5 | 6 | class Rotation { 7 | constructor() { 8 | this.rot = createVector(0, 0, 0); 9 | this.rotX = 0; 10 | this.rotY = 0; 11 | this.rotZ = 0; 12 | this.offsetZ = 0; 13 | this.rotZRef = 0; 14 | this.rotationThreshold = 0; 15 | this.debug = false; 16 | } 17 | 18 | setDebug(debug) { 19 | this.debug = debug; 20 | } 21 | 22 | // Initialize rotation variables 23 | initRotation() { 24 | this.rot.x = rotationX; 25 | this.rot.y = rotationY; 26 | this.rot.z = cycle(rotationZ - this.offsetZ, 0, 360); 27 | } 28 | 29 | update() { 30 | this.rot.x = rotationX; 31 | this.rot.y = rotationY; 32 | this.rot.z = cycle(rotationZ - this.offsetZ, 0, 360); 33 | } 34 | 35 | // Getters for retrieving rotation 36 | get() { 37 | return this.rot; 38 | } 39 | 40 | // Check if change in rotation is above threshold in all three axes. 41 | isChanging() { 42 | if (this.checkX() || this.checkY() || this.checkZ()) { 43 | return true; 44 | } else { 45 | return false; 46 | } 47 | } 48 | 49 | isChangingX() { 50 | if (abs(rotationX-pRotationX)>this.rotationThreshold) { 51 | return true; 52 | } else { 53 | return false; 54 | } 55 | } 56 | 57 | isChangingY() { 58 | if (abs(rotationY-pRotationY)>this.rotationThreshold) { 59 | return true; 60 | } else { 61 | return false; 62 | } 63 | } 64 | 65 | isChangingZ() { 66 | if (abs(rotationZ-pRotationZ)>this.rotationThreshold) { 67 | return true; 68 | } else { 69 | return false; 70 | } 71 | } 72 | 73 | // Check to see if device is laying flat 74 | isFlat() { 75 | if (rotationX < 1 && rotationX > -1 && rotationY < 1 && rotationY > -1) { 76 | return true; 77 | } else { 78 | return false; 79 | } 80 | } 81 | 82 | // Set the rotation threshold 83 | setRotationThreshold(threshold) { 84 | this.rotationThreshold = threshold; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /public/lib/utilities.js: -------------------------------------------------------------------------------- 1 | // Function that is similar to clamp but loops between a min and max value 2 | function cycle(value, minimum, maximum) { 3 | const difference = maximum - minimum; 4 | 5 | if (value > maximum) { 6 | return (value - maximum) % difference + minimum; 7 | } else if (value < minimum) { 8 | return maximum - (minimum - value) % difference; 9 | } else { 10 | return value; 11 | } 12 | } 13 | 14 | // A utility function to calculate area of triangle 15 | // formed by (x1, y1) (x2, y2) and (x3, y3) 16 | function triangleArea(x1, y1, x2, y2, x3, y3) { 17 | return abs((x1*(y2-y3) + x2*(y3-y1) + x3*(y1-y2))/2.0); 18 | } 19 | 20 | // A function to check whether point P(x, y) inside 21 | // the triangle formed by A(x1, y1), B(x2, y2) and C(x3, y3) 22 | function isInsideTriangle(x, y, x1, y1, x2, y2, x3, y3) { 23 | /* Calculate area of triangle ABC */ 24 | const A = triangleArea (x1, y1, x2, y2, x3, y3); 25 | 26 | /* Calculate area of triangle PBC */ 27 | const A1 = triangleArea (x, y, x2, y2, x3, y3); 28 | 29 | /* Calculate area of triangle PAC */ 30 | const A2 = triangleArea (x1, y1, x, y, x3, y3); 31 | 32 | /* Calculate area of triangle PAB */ 33 | const A3 = triangleArea (x1, y1, x2, y2, x, y); 34 | 35 | /* Check if sum of A1, A2 and A3 is same as A */ 36 | return (A == A1 + A2 + A3); 37 | } 38 | -------------------------------------------------------------------------------- /secureServer.js: -------------------------------------------------------------------------------- 1 | // Create dictionaries for tracking hosts, clients, and rooms 2 | let hosts = {}; 3 | let clients = {}; 4 | let rooms = {}; 5 | 6 | //////////// 7 | // Required for secure server 8 | const fs = require('fs'); 9 | 10 | const options = { 11 | key: fs.readFileSync('key.pem'), 12 | cert: fs.readFileSync('cert.pem') 13 | }; 14 | 15 | // Setup express web server and listen on port 3000 16 | let express = require('express'); 17 | let app = express(); 18 | let port = Number(process.env.PORT || 3000); 19 | let server = require('https').createServer(options, app); 20 | server.listen(port); 21 | 22 | app.use(express.static('public')); 23 | console.log("My socket server is running on port " + port); 24 | 25 | //////////// 26 | // Start socket.io 27 | let socket = require('socket.io'); 28 | 29 | // Connect it to the web server 30 | let io = socket(server); 31 | 32 | //////////// 33 | // Setup a connection 34 | io.sockets.on('connection', newConnection); 35 | function newConnection(socket) { 36 | 37 | // Inform incoming connection of its ID 38 | console.log('\n' + socket.id + ' is attempting connection...'); 39 | socket.emit('id', socket.id); 40 | 41 | socket.on('test', function (data) { 42 | console.log('TEST'); 43 | }); 44 | 45 | // Process a request to join. 46 | socket.on('join', function (data) { 47 | 48 | // If request is from a client... 49 | if (data.name == 'client') { 50 | 51 | console.log("Verifying client..."); 52 | 53 | // If the roomId field is not null 54 | if (data.roomId != null) { 55 | 56 | // Search existing roomIds for a match 57 | console.log("Searching for existing room ID..."); 58 | if (rooms[data.roomId] != null) { 59 | 60 | // Add client to room with all connected clients 61 | socket.join(data.name); 62 | 63 | // Add client and corresponding data to clients dictionary 64 | // by socket ID 65 | clients[socket.id] = { 66 | type: data.name, 67 | roomId: data.roomId 68 | } 69 | 70 | // Add client to its own room and to host room by room ID 71 | socket.join([socket.id, data.roomId]); 72 | console.log('Client added to room '+data.roomId+'.\tNumber of clients: ' + Object.keys(clients).length); 73 | 74 | // Send match confirmation back to client 75 | socket.emit("found", {status: true}); 76 | } 77 | else { 78 | // Notify client of failure to match 79 | socket.emit("found", {status: false}); 80 | } 81 | } 82 | } 83 | else if (data.name == 'host') { 84 | // If the attempted connection is from a host... 85 | 86 | // Store a transmitted room ID if it exists, otherwise 87 | // generate a random gemstone name as room ID. 88 | let roomId = null; 89 | if (data.roomId === null || data.roomId === 'undefined') { 90 | roomId = makeIdFromList(); 91 | } 92 | else { 93 | roomId = data.roomId; 94 | } 95 | 96 | // Add client and corresponding data to devices dictionary 97 | // by socket ID 98 | let hostData = { 99 | type: data.name, 100 | roomId: roomId 101 | }; 102 | 103 | hosts[socket.id] = hostData; 104 | rooms[roomId] = socket.id; 105 | 106 | // Add host to "host" room, its own room by room ID, and to a room 107 | // with its clients by room ID. 108 | socket.join([data.name, 'host:'+hostData.roomId, hostData.roomId]); 109 | 110 | // Send clients room ID back to host 111 | socket.emit("hostConnect", hostData); 112 | 113 | console.log('Host added with room ID of ' + hostData.roomId + '.\tNumber of hosts: ' + Object.keys(hosts).length); 114 | } 115 | else { 116 | console.log('warning: data type not recognized.') 117 | } 118 | }) 119 | 120 | //// Process device disconnects. 121 | socket.on('disconnect', function () { 122 | console.log('\n' + socket.id + ' has been disconnected!'); 123 | 124 | if (clients[socket.id] != null) { 125 | // If the device is a client, delete it 126 | delete clients[socket.id]; 127 | console.log('Client removed.\tNumber of clients: ' + Object.keys(clients).length); 128 | 129 | // Notify hosts that client has disconnected. 130 | socket.in('host').emit('clientDisconnect', {id: socket.id}); 131 | } 132 | else if (hosts[socket.id] != null) { 133 | // If the device is a host, delete it 134 | let roomId = hosts[socket.id].roomId; 135 | delete hosts[socket.id]; 136 | console.log('Host with ID ' + roomId + ' removed.\tHumber of hosts: ' + Object.keys(hosts).length); 137 | 138 | // Remove corresponding room 139 | let key = getKeyByValue(rooms, socket.id); 140 | if (key != null) { 141 | delete rooms[key]; 142 | } 143 | 144 | // TODO: add handling for all clients connected to host when host 145 | // is disconnected. 146 | } 147 | }) 148 | 149 | //// Process client connects. 150 | socket.on('clientConnect', onClientConnect); 151 | 152 | function onClientConnect(data) { 153 | if (rooms[data.roomId] != null) { 154 | console.log('clientConnect message received from ' + socket.id + ' for room ' + data.roomId + "."); 155 | socket.in('host:'+data.roomId).emit('clientConnect', {id: socket.id, roomId: data.roomId}); 156 | } 157 | } 158 | 159 | //// Reroute data sent between clients and hosts 160 | socket.on('sendData', sendData); 161 | 162 | function sendData(data) { 163 | let packet = {...data}; 164 | packet.id = socket.id; 165 | 166 | // If room ID is valid... 167 | if (rooms[data.roomId] != null) { 168 | if (clients[socket.id] != null) { 169 | // And if device is a client, send to corresponding host 170 | socket.in('host:'+data.roomId).emit('receiveData', packet); 171 | } 172 | else if (hosts[socket.id] != null) { 173 | // And if device is a host, send to corresponding clients 174 | socket.broadcast.in(data.roomId).emit('receiveData', packet); 175 | } 176 | } 177 | } 178 | } 179 | 180 | //////////// 181 | // Utility Functions 182 | function searchRoomId(roomId_, array_) { 183 | for (let i = 0; i < array_.length; i++) { 184 | if (array_[i].roomId == roomId_) { 185 | return { 186 | item: array_[i], 187 | index: i 188 | }; 189 | } 190 | } 191 | } 192 | 193 | function getKeyByValue(object, value) { 194 | return Object.keys(object).find(key => object[key] === value); 195 | } 196 | 197 | //////////// 198 | // Gemstone room ID generator 199 | const roomNames = 200 | ["agate", 201 | "amber", 202 | "amethyst", 203 | "barite", 204 | "beryl", 205 | "bloodstone", 206 | "coral", 207 | "crystal", 208 | "diamond", 209 | "emerald", 210 | "fluorite", 211 | "garnet", 212 | "goldstone", 213 | "jade", 214 | "jasper", 215 | "moonstone", 216 | "onyx", 217 | "opal", 218 | "pearl", 219 | "peridot", 220 | "quahog", 221 | "quartz", 222 | "ruby", 223 | "sapphire", 224 | "sardonyx", 225 | "sunstone", 226 | "tigereye", 227 | "topaz", 228 | "turquoise", 229 | "zircon"] 230 | 231 | const roomIds = randomNoRepeats(roomNames); 232 | 233 | function randomNoRepeats(array) { 234 | let copy = array.slice(0); 235 | return function() { 236 | if (copy.length < 1) { copy = array.slice(0); } 237 | let index = Math.floor(Math.random() * copy.length); 238 | let item = copy[index]; 239 | copy.splice(index, 1); 240 | return {id: item, length: copy.length}; 241 | }; 242 | } 243 | 244 | function makeIdFromList() { 245 | for (let i = 0; i < roomNames.length; i++) { 246 | let text = roomIds().id; 247 | let room = searchRoomId(text, hosts); 248 | if (room == null) { 249 | return text; 250 | } 251 | } 252 | console.log(hosts.length + " hosts detected. No names available."); 253 | return null; 254 | } 255 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | // Create dictionaries for tracking hosts, clients, and rooms 2 | let hosts = {}; 3 | let clients = {}; 4 | let rooms = {}; 5 | 6 | //////////// 7 | // Setup express web server and listen on port 3000 8 | let express = require('express'); 9 | let app = express(); 10 | let port=Number(process.env.PORT || 3000); 11 | let server = app.listen(port); 12 | 13 | app.use(express.static('public')); 14 | console.log("My socket server is running on port " + port); 15 | 16 | //////////// 17 | // Start socket.io 18 | let socket = require('socket.io'); 19 | 20 | // Connect it to the web server 21 | let io = socket(server); 22 | 23 | //////////// 24 | // Setup a connection 25 | io.sockets.on('connection', newConnection); 26 | function newConnection(socket) { 27 | 28 | // Inform incoming connection of its ID 29 | console.log('\n' + socket.id + ' is attempting connection...'); 30 | socket.emit('id', socket.id); 31 | 32 | // Process a request to join. 33 | socket.on('join', function (data) { 34 | 35 | // If request is from a client... 36 | if (data.name == 'client') { 37 | 38 | console.log("Verifying client..."); 39 | 40 | // If the roomId field is not null 41 | if (data.roomId != null) { 42 | 43 | // Search existing roomIds for a match 44 | console.log("Searching for existing room ID..."); 45 | if (rooms[data.roomId] != null) { 46 | 47 | // Add client to room with all connected clients 48 | socket.join(data.name); 49 | 50 | // Add client and corresponding data to clients dictionary 51 | // by socket ID 52 | clients[socket.id] = { 53 | type: data.name, 54 | roomId: data.roomId 55 | } 56 | 57 | // Add client to its own room and to host room by room ID 58 | socket.join([socket.id, data.roomId]); 59 | console.log('Client added to room '+data.roomId+'.\tNumber of clients: ' + Object.keys(clients).length); 60 | 61 | // Send match confirmation back to client 62 | socket.emit("found", {status: true}); 63 | } 64 | else { 65 | // Notify client of failure to match 66 | socket.emit("found", {status: false}); 67 | } 68 | } 69 | } 70 | else if (data.name == 'host') { 71 | // If the attempted connection is from a host... 72 | 73 | // Store a transmitted room ID if it exists, otherwise 74 | // generate a random gemstone name as room ID. 75 | let roomId = null; 76 | if (data.roomId === null || data.roomId === 'undefined') { 77 | roomId = makeIdFromList(); 78 | } 79 | else { 80 | roomId = data.roomId; 81 | } 82 | 83 | // Add client and corresponding data to devices dictionary 84 | // by socket ID 85 | let hostData = { 86 | type: data.name, 87 | roomId: roomId 88 | }; 89 | 90 | hosts[socket.id] = hostData; 91 | rooms[roomId] = socket.id; 92 | 93 | // Add host to "host" room, its own room by room ID, and to a room 94 | // with its clients by room ID. 95 | socket.join([data.name, 'host:'+hostData.roomId, hostData.roomId]); 96 | 97 | // Send clients room ID back to host 98 | socket.emit("hostConnect", hostData); 99 | 100 | console.log('Host added with room ID of ' + hostData.roomId + '.\tNumber of hosts: ' + Object.keys(hosts).length); 101 | } 102 | else { 103 | console.log('warning: data type not recognized.') 104 | } 105 | }) 106 | 107 | //// Process device disconnects. 108 | socket.on('disconnect', function () { 109 | console.log('\n' + socket.id + ' has been disconnected!'); 110 | 111 | if (clients[socket.id] != null) { 112 | // If the device is a client, delete it 113 | delete clients[socket.id]; 114 | console.log('Client removed.\tNumber of clients: ' + Object.keys(clients).length); 115 | 116 | // Notify hosts that client has disconnected. 117 | socket.in('host').emit('clientDisconnect', {id: socket.id}); 118 | } 119 | else if (hosts[socket.id] != null) { 120 | // If the device is a host, delete it 121 | let roomId = hosts[socket.id].roomId; 122 | delete hosts[socket.id]; 123 | console.log('Host with ID ' + roomId + ' removed.\tHumber of hosts: ' + Object.keys(hosts).length); 124 | 125 | // Remove corresponding room 126 | let key = getKeyByValue(rooms, socket.id); 127 | if (key != null) { 128 | delete rooms[key]; 129 | } 130 | 131 | // TODO: add handling for all clients connected to host when host 132 | // is disconnected. 133 | } 134 | }) 135 | 136 | //// Process client connects. 137 | socket.on('clientConnect', onClientConnect); 138 | 139 | function onClientConnect(data) { 140 | if (rooms[data.roomId] != null) { 141 | console.log('clientConnect message received from ' + socket.id + ' for room ' + data.roomId + "."); 142 | socket.in('host:'+data.roomId).emit('clientConnect', {id: socket.id, roomId: data.roomId}); 143 | } 144 | } 145 | 146 | //// Reroute data sent between clients and hosts 147 | socket.on('sendData', sendData); 148 | 149 | function sendData(data) { 150 | let packet = {...data}; 151 | packet.id = socket.id; 152 | 153 | // If room ID is valid... 154 | if (rooms[data.roomId] != null) { 155 | if (clients[socket.id] != null) { 156 | // And if device is a client, send to corresponding host 157 | socket.in('host:'+data.roomId).emit('receiveData', packet); 158 | } 159 | else if (hosts[socket.id] != null) { 160 | // And if device is a host, send to corresponding clients 161 | socket.broadcast.in(data.roomId).emit('receiveData', packet); 162 | } 163 | } 164 | } 165 | } 166 | 167 | //////////// 168 | // Utility Functions 169 | function searchRoomId(roomId_, array_) { 170 | for (let i = 0; i < array_.length; i++) { 171 | if (array_[i].roomId == roomId_) { 172 | return { 173 | item: array_[i], 174 | index: i 175 | }; 176 | } 177 | } 178 | } 179 | 180 | function getKeyByValue(object, value) { 181 | return Object.keys(object).find(key => object[key] === value); 182 | } 183 | 184 | //////////// 185 | // Gemstone room ID generator 186 | const roomNames = 187 | ["agate", 188 | "amber", 189 | "amethyst", 190 | "barite", 191 | "beryl", 192 | "bloodstone", 193 | "coral", 194 | "crystal", 195 | "diamond", 196 | "emerald", 197 | "fluorite", 198 | "garnet", 199 | "goldstone", 200 | "jade", 201 | "jasper", 202 | "moonstone", 203 | "onyx", 204 | "opal", 205 | "pearl", 206 | "peridot", 207 | "quahog", 208 | "quartz", 209 | "ruby", 210 | "sapphire", 211 | "sardonyx", 212 | "sunstone", 213 | "tigereye", 214 | "topaz", 215 | "turquoise", 216 | "zircon"] 217 | 218 | const roomIds = randomNoRepeats(roomNames); 219 | 220 | function randomNoRepeats(array) { 221 | let copy = array.slice(0); 222 | return function() { 223 | if (copy.length < 1) { copy = array.slice(0); } 224 | let index = Math.floor(Math.random() * copy.length); 225 | let item = copy[index]; 226 | copy.splice(index, 1); 227 | return {id: item, length: copy.length}; 228 | }; 229 | } 230 | 231 | function makeIdFromList() { 232 | for (let i = 0; i < roomNames.length; i++) { 233 | let text = roomIds().id; 234 | let room = searchRoomId(text, hosts); 235 | if (room == null) { 236 | return text; 237 | } 238 | } 239 | console.log(hosts.length + " hosts detected. No names available."); 240 | return null; 241 | } 242 | -------------------------------------------------------------------------------- /template/host.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /template/host.js: -------------------------------------------------------------------------------- 1 | /* 2 | p5.multiplayer - HOST 3 | 4 | This 'host' sketch is intended to be run in desktop browsers. 5 | It connects to a node server via socket.io, from which it receives 6 | rerouted input data from all connected 'clients'. 7 | 8 | Navigate to the project's 'public' directory. 9 | Run http-server -c-1 to start server. This will default to port 8080. 10 | Run http-server -c-1 -p80 to start server on open port 80. 11 | 12 | */ 13 | 14 | //////////// 15 | // Network Settings 16 | // const serverIp = 'https://yourservername.herokuapp.com'; 17 | // const serverIp = 'https://yourprojectname.glitch.me'; 18 | const serverIp = '127.0.0.1'; 19 | const serverPort = '3000'; 20 | const local = true; // true if running locally, false 21 | // if running on remote server 22 | 23 | // Global variables here. ----> 24 | 25 | // <---- 26 | 27 | function preload() { 28 | setupHost(); 29 | } 30 | 31 | function setup () { 32 | createCanvas(windowWidth, windowHeight); 33 | 34 | // Host/Game setup here. ----> 35 | 36 | 37 | // <---- 38 | } 39 | 40 | function windowResized() { 41 | resizeCanvas(windowWidth, windowHeight); 42 | } 43 | 44 | function draw () { 45 | background(15); 46 | 47 | if(isHostConnected(display=true)) { 48 | // Host/Game draw here. ---> 49 | 50 | 51 | // <---- 52 | 53 | // Display server address 54 | displayAddress(); 55 | } 56 | } 57 | 58 | function onClientConnect (data) { 59 | // Client connect logic here. ---> 60 | console.log(data.id + ' has connected.'); 61 | 62 | // <---- 63 | } 64 | 65 | function onClientDisconnect (data) { 66 | // Client disconnect logic here. ---> 67 | console.log(data.id + ' has disconnected.'); 68 | 69 | // <---- 70 | } 71 | 72 | function onReceiveData (data) { 73 | // Input data processing here. ---> 74 | console.log(data); 75 | 76 | // <--- 77 | 78 | /* Example: 79 | if (data.type === 'myDataType') { 80 | processMyData(data); 81 | } 82 | 83 | Use `data.type` to get the message type sent by client. 84 | */ 85 | 86 | } 87 | -------------------------------------------------------------------------------- /template/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /template/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | p5.multiplayer - CLIENT 3 | 4 | This 'client' sketch is intended to be run in either mobile or 5 | desktop browsers. It sends a basic joystick and button input data 6 | to a node server via socket.io. This data is then rerouted to a 7 | 'host' sketch, which displays all connected 'clients'. 8 | 9 | Navigate to the project's 'public' directory. 10 | Run http-server -c-1 to start server. This will default to port 8080. 11 | Run http-server -c-1 -p80 to start server on open port 80. 12 | 13 | */ 14 | 15 | //////////// 16 | // Network Settings 17 | // const serverIp = 'https://yourservername.herokuapp.com'; 18 | // const serverIp = 'https://yourprojectname.glitch.me'; 19 | const serverIp = '127.0.0.1'; 20 | const serverPort = '3000'; 21 | const local = true; // true if running locally, false 22 | // if running on remote server 23 | 24 | // Global variables here. ----> 25 | 26 | // <---- 27 | 28 | function preload() { 29 | setupClient(); 30 | } 31 | 32 | function setup() { 33 | createCanvas(windowWidth, windowHeight); 34 | 35 | // Client setup here. ----> 36 | 37 | // <---- 38 | 39 | // Send any initial setup data to your host here. 40 | /* 41 | Example: 42 | sendData('myDataType', { 43 | val1: 0, 44 | val2: 128, 45 | val3: true 46 | }); 47 | 48 | Use `type` to classify message types for host. 49 | */ 50 | } 51 | 52 | function windowResized() { 53 | resizeCanvas(windowWidth, windowHeight); 54 | } 55 | 56 | function draw() { 57 | background(0); 58 | 59 | if(isClientConnected(display=true)) { 60 | // Client draw here. ----> 61 | 62 | 63 | // <---- 64 | } 65 | } 66 | 67 | // Messages can be sent from a host to all connected clients 68 | function onReceiveData (data) { 69 | // Input data processing here. ---> 70 | console.log(data); 71 | 72 | // <---- 73 | 74 | /* Example: 75 | if (data.type === 'myDataType') { 76 | processMyData(data); 77 | } 78 | 79 | Use `data.type` to get the message type sent by host. 80 | */ 81 | } 82 | 83 | /// Add these lines below sketch to prevent scrolling on mobile 84 | function touchMoved() { 85 | // do some stuff 86 | return false; 87 | } -------------------------------------------------------------------------------- /template/index.php: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /template/lib/p5.multiplayer.js: -------------------------------------------------------------------------------- 1 | //////////// 2 | // COMMON 3 | 4 | // Initialize Network related variables 5 | let socket; 6 | let roomId = null; 7 | let id = null; 8 | 9 | // Process URL 10 | // Used to process the room ID. In order to specify a room ID, 11 | // include ?=uniqueName, where uniqueName is replaced with the 12 | // desired unique room ID. 13 | function _processUrl() { 14 | const parameters = location.search.substring(1).split("&"); 15 | 16 | const temp = parameters[0].split("="); 17 | roomId = unescape(temp[1]); 18 | 19 | console.log("id: " + roomId); 20 | } 21 | 22 | // Send data from client to host via server 23 | function sendData(datatype, data) { 24 | data.type = datatype; 25 | data.roomId = roomId; 26 | 27 | socket.emit('sendData', data); 28 | } 29 | 30 | // Displays a message while attempting connection 31 | function _displayWaiting() { 32 | push(); 33 | fill(200); 34 | textAlign(CENTER, CENTER); 35 | textSize(20); 36 | text("Attempting connection...", width/2, height/2-10); 37 | pop(); 38 | } 39 | 40 | //////////// 41 | // HOST 42 | 43 | // Initialize Network related variables 44 | let hostConnected = false; 45 | 46 | function setupHost() { 47 | _processUrl(); 48 | 49 | let addr = serverIp; 50 | if (local) { addr = serverIp + ':' + serverPort; } 51 | socket = io.connect(addr); 52 | 53 | socket.emit('join', {name: 'host', roomId: roomId}); 54 | 55 | socket.on('id', function(data) { 56 | id = data; 57 | console.log("id: " + id); 58 | }); 59 | 60 | socket.on('hostConnect', onHostConnect); 61 | socket.on('clientConnect', onClientConnect); 62 | socket.on('clientDisconnect', onClientDisconnect); 63 | socket.on('receiveData', onReceiveData); 64 | } 65 | 66 | function isHostConnected(display=false) { 67 | if (!hostConnected) { 68 | if (display) { _displayWaiting(); } 69 | return false; 70 | } 71 | return true; 72 | } 73 | 74 | function onHostConnect (data) { 75 | console.log("Host connected to server."); 76 | hostConnected = true; 77 | 78 | if (roomId === null || roomId === 'undefined') { 79 | roomId = data.roomId; 80 | } 81 | } 82 | 83 | // Displays server address in lower left of screen 84 | function displayAddress() { 85 | push(); 86 | fill(255); 87 | textSize(50); 88 | text(serverIp+"/?="+roomId, 10, height-50); 89 | pop(); 90 | } 91 | 92 | //////////// 93 | // CLIENT 94 | 95 | // Initialize Network related variables 96 | let waiting = true; 97 | let connected = false; 98 | 99 | function setupClient() { 100 | _processUrl(); 101 | 102 | // Socket.io - open a connection to the web server on specified port 103 | let addr = serverIp; 104 | if (local) { addr = serverIp + ':' + serverPort; } 105 | socket = io.connect(addr); 106 | 107 | socket.emit('join', {name: 'client', roomId: roomId}); 108 | 109 | socket.on('id', function(data) { 110 | id = data; 111 | console.log("id: " + id); 112 | }); 113 | 114 | socket.on('found', function(data) { 115 | connected = data.status; 116 | waiting = false; 117 | console.log("connected: " + connected); 118 | }) 119 | 120 | socket.emit('clientConnect', { 121 | roomId: roomId 122 | }); 123 | 124 | socket.on('receiveData', onReceiveData); 125 | } 126 | 127 | function isClientConnected(display=false) { 128 | if (waiting) { 129 | if (display) { _displayWaiting(); } 130 | return false; 131 | } 132 | else if (!connected) { 133 | if (display) { _displayInstructions(); } 134 | return false; 135 | } 136 | 137 | return true; 138 | } 139 | 140 | // Displays a message instructing player to look at host screen 141 | // for correct link. 142 | function _displayInstructions() { 143 | push(); 144 | fill(200); 145 | textAlign(CENTER, CENTER); 146 | textSize(20); 147 | text("Please enter the link at the", width/2, height/2-10); 148 | text("bottom of the host screen.", width/2, height/2+10); 149 | pop(); 150 | } 151 | --------------------------------------------------------------------------------