├── .eslintrc.json ├── .gitignore ├── .travis.yml ├── README.md ├── config ├── env.js ├── jest │ ├── cssTransform.js │ └── fileTransform.js ├── paths.js ├── polyfills.js ├── webpack.config.dev.js ├── webpack.config.prod.js └── webpackDevServer.config.js ├── package.json ├── public ├── favicon.ico ├── index.html └── manifest.json ├── scripts ├── build.js ├── start.js └── test.js └── src ├── App.css ├── App.js ├── game ├── Game.js ├── Input.js ├── State.js └── assets │ ├── dude.png │ ├── platform.png │ └── sky.png ├── guest └── Guest.js ├── host ├── Host.js ├── HostGame.js └── HostName.js ├── index.js ├── lobby └── LobbyList.js ├── player ├── DisplayGame.js └── Player.js ├── registerServiceWorker.js └── testgame └── TestGame.js /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "react-app", 3 | "rules": { 4 | "no-loop-func": "off" 5 | } 6 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | 12 | # misc 13 | .DS_Store 14 | .env.local 15 | .env.development.local 16 | .env.test.local 17 | .env.production.local 18 | 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | package-lock.json 23 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "6" 4 | script: 5 | - npm run test 6 | - npm run build 7 | deploy: 8 | provider: pages 9 | skip_cleanup: true 10 | github_token: $GITHUB_TOKEN 11 | local_dir: build 12 | repo: rynobax/jump-game 13 | on: 14 | branch: master -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # jump-game 2 | A mulitplayer game that uses WebRTC. 3 | 4 | ## Playing 5 | One player should press the host button. Other players should press join, then use the host's code to join his game. Once everyone presses the ready button, the game will begin. 6 | 7 | The goal is to stay on the platforms. The last player to fall wins. 8 | 9 | ## About 10 | I wrote a blog post about this project [here](https://rynobax.github.io/Creating-a-Multiplayer-Game-with-WebRTC/)! 11 | -------------------------------------------------------------------------------- /config/env.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | const paths = require('./paths'); 6 | 7 | // Make sure that including paths.js after env.js will read .env variables. 8 | delete require.cache[require.resolve('./paths')]; 9 | 10 | const NODE_ENV = process.env.NODE_ENV; 11 | if (!NODE_ENV) { 12 | throw new Error( 13 | 'The NODE_ENV environment variable is required but was not specified.' 14 | ); 15 | } 16 | 17 | // https://github.com/bkeepers/dotenv#what-other-env-files-can-i-use 18 | var dotenvFiles = [ 19 | `${paths.dotenv}.${NODE_ENV}.local`, 20 | `${paths.dotenv}.${NODE_ENV}`, 21 | // Don't include `.env.local` for `test` environment 22 | // since normally you expect tests to produce the same 23 | // results for everyone 24 | NODE_ENV !== 'test' && `${paths.dotenv}.local`, 25 | paths.dotenv, 26 | ].filter(Boolean); 27 | 28 | // Load environment variables from .env* files. Suppress warnings using silent 29 | // if this file is missing. dotenv will never modify any environment variables 30 | // that have already been set. 31 | // https://github.com/motdotla/dotenv 32 | dotenvFiles.forEach(dotenvFile => { 33 | if (fs.existsSync(dotenvFile)) { 34 | require('dotenv').config({ 35 | path: dotenvFile, 36 | }); 37 | } 38 | }); 39 | 40 | // We support resolving modules according to `NODE_PATH`. 41 | // This lets you use absolute paths in imports inside large monorepos: 42 | // https://github.com/facebookincubator/create-react-app/issues/253. 43 | // It works similar to `NODE_PATH` in Node itself: 44 | // https://nodejs.org/api/modules.html#modules_loading_from_the_global_folders 45 | // Note that unlike in Node, only *relative* paths from `NODE_PATH` are honored. 46 | // Otherwise, we risk importing Node.js core modules into an app instead of Webpack shims. 47 | // https://github.com/facebookincubator/create-react-app/issues/1023#issuecomment-265344421 48 | // We also resolve them to make sure all tools using them work consistently. 49 | const appDirectory = fs.realpathSync(process.cwd()); 50 | process.env.NODE_PATH = (process.env.NODE_PATH || '') 51 | .split(path.delimiter) 52 | .filter(folder => folder && !path.isAbsolute(folder)) 53 | .map(folder => path.resolve(appDirectory, folder)) 54 | .join(path.delimiter); 55 | 56 | // Grab NODE_ENV and REACT_APP_* environment variables and prepare them to be 57 | // injected into the application via DefinePlugin in Webpack configuration. 58 | const REACT_APP = /^REACT_APP_/i; 59 | 60 | function getClientEnvironment(publicUrl) { 61 | const raw = Object.keys(process.env) 62 | .filter(key => REACT_APP.test(key)) 63 | .reduce( 64 | (env, key) => { 65 | env[key] = process.env[key]; 66 | return env; 67 | }, 68 | { 69 | // Useful for determining whether we’re running in production mode. 70 | // Most importantly, it switches React into the correct mode. 71 | NODE_ENV: process.env.NODE_ENV || 'development', 72 | // Useful for resolving the correct path to static assets in `public`. 73 | // For example, . 74 | // This should only be used as an escape hatch. Normally you would put 75 | // images into the `src` and `import` them in code to get their paths. 76 | PUBLIC_URL: publicUrl, 77 | } 78 | ); 79 | // Stringify all values so we can feed into Webpack DefinePlugin 80 | const stringified = { 81 | 'process.env': Object.keys(raw).reduce( 82 | (env, key) => { 83 | env[key] = JSON.stringify(raw[key]); 84 | return env; 85 | }, 86 | {} 87 | ), 88 | }; 89 | 90 | return { raw, stringified }; 91 | } 92 | 93 | module.exports = getClientEnvironment; 94 | -------------------------------------------------------------------------------- /config/jest/cssTransform.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // This is a custom Jest transformer turning style imports into empty objects. 4 | // http://facebook.github.io/jest/docs/tutorial-webpack.html 5 | 6 | module.exports = { 7 | process() { 8 | return 'module.exports = {};'; 9 | }, 10 | getCacheKey() { 11 | // The output is always the same. 12 | return 'cssTransform'; 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /config/jest/fileTransform.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | 5 | // This is a custom Jest transformer turning file imports into filenames. 6 | // http://facebook.github.io/jest/docs/tutorial-webpack.html 7 | 8 | module.exports = { 9 | process(src, filename) { 10 | return `module.exports = ${JSON.stringify(path.basename(filename))};`; 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /config/paths.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const fs = require('fs'); 5 | const url = require('url'); 6 | 7 | // Make sure any symlinks in the project folder are resolved: 8 | // https://github.com/facebookincubator/create-react-app/issues/637 9 | const appDirectory = fs.realpathSync(process.cwd()); 10 | const resolveApp = relativePath => path.resolve(appDirectory, relativePath); 11 | 12 | const envPublicUrl = process.env.PUBLIC_URL; 13 | 14 | function ensureSlash(path, needsSlash) { 15 | const hasSlash = path.endsWith('/'); 16 | if (hasSlash && !needsSlash) { 17 | return path.substr(path, path.length - 1); 18 | } else if (!hasSlash && needsSlash) { 19 | return `${path}/`; 20 | } else { 21 | return path; 22 | } 23 | } 24 | 25 | const getPublicUrl = appPackageJson => 26 | envPublicUrl || require(appPackageJson).homepage; 27 | 28 | // We use `PUBLIC_URL` environment variable or "homepage" field to infer 29 | // "public path" at which the app is served. 30 | // Webpack needs to know it to put the right 14 | 23 | Jump Game 24 | 25 | 26 | 29 |
30 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "24x22", 8 | "type": "image/png" 9 | } 10 | ], 11 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /scripts/build.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Do this as the first thing so that any code reading it knows the right env. 4 | process.env.BABEL_ENV = 'production'; 5 | process.env.NODE_ENV = 'production'; 6 | 7 | // Makes the script crash on unhandled rejections instead of silently 8 | // ignoring them. In the future, promise rejections that are not handled will 9 | // terminate the Node.js process with a non-zero exit code. 10 | process.on('unhandledRejection', err => { 11 | throw err; 12 | }); 13 | 14 | // Ensure environment variables are read. 15 | require('../config/env'); 16 | 17 | const path = require('path'); 18 | const chalk = require('chalk'); 19 | const fs = require('fs-extra'); 20 | const webpack = require('webpack'); 21 | const config = require('../config/webpack.config.prod'); 22 | const paths = require('../config/paths'); 23 | const checkRequiredFiles = require('react-dev-utils/checkRequiredFiles'); 24 | const formatWebpackMessages = require('react-dev-utils/formatWebpackMessages'); 25 | const printHostingInstructions = require('react-dev-utils/printHostingInstructions'); 26 | const FileSizeReporter = require('react-dev-utils/FileSizeReporter'); 27 | 28 | const measureFileSizesBeforeBuild = FileSizeReporter.measureFileSizesBeforeBuild; 29 | const printFileSizesAfterBuild = FileSizeReporter.printFileSizesAfterBuild; 30 | const useYarn = fs.existsSync(paths.yarnLockFile); 31 | 32 | // Warn and crash if required files are missing 33 | if (!checkRequiredFiles([paths.appHtml, paths.appIndexJs])) { 34 | process.exit(1); 35 | } 36 | 37 | // First, read the current file sizes in build directory. 38 | // This lets us display how much they changed later. 39 | measureFileSizesBeforeBuild(paths.appBuild) 40 | .then(previousFileSizes => { 41 | // Remove all content but keep the directory so that 42 | // if you're in it, you don't end up in Trash 43 | fs.emptyDirSync(paths.appBuild); 44 | // Merge with the public folder 45 | copyPublicFolder(); 46 | // Start the webpack build 47 | return build(previousFileSizes); 48 | }) 49 | .then( 50 | ({ stats, previousFileSizes, warnings }) => { 51 | if (warnings.length) { 52 | console.log(chalk.yellow('Compiled with warnings.\n')); 53 | console.log(warnings.join('\n\n')); 54 | console.log( 55 | '\nSearch for the ' + 56 | chalk.underline(chalk.yellow('keywords')) + 57 | ' to learn more about each warning.' 58 | ); 59 | console.log( 60 | 'To ignore, add ' + 61 | chalk.cyan('// eslint-disable-next-line') + 62 | ' to the line before.\n' 63 | ); 64 | } else { 65 | console.log(chalk.green('Compiled successfully.\n')); 66 | } 67 | 68 | console.log('File sizes after gzip:\n'); 69 | printFileSizesAfterBuild(stats, previousFileSizes, paths.appBuild); 70 | console.log(); 71 | 72 | const appPackage = require(paths.appPackageJson); 73 | const publicUrl = paths.publicUrl; 74 | const publicPath = config.output.publicPath; 75 | const buildFolder = path.relative(process.cwd(), paths.appBuild); 76 | printHostingInstructions( 77 | appPackage, 78 | publicUrl, 79 | publicPath, 80 | buildFolder, 81 | useYarn 82 | ); 83 | }, 84 | err => { 85 | console.log(chalk.red('Failed to compile.\n')); 86 | console.log((err.message || err) + '\n'); 87 | process.exit(1); 88 | } 89 | ); 90 | 91 | // Create the production build and print the deployment instructions. 92 | function build(previousFileSizes) { 93 | console.log('Creating an optimized production build...'); 94 | 95 | let compiler = webpack(config); 96 | return new Promise((resolve, reject) => { 97 | compiler.run((err, stats) => { 98 | if (err) { 99 | return reject(err); 100 | } 101 | const messages = formatWebpackMessages(stats.toJson({}, true)); 102 | if (messages.errors.length) { 103 | return reject(new Error(messages.errors.join('\n\n'))); 104 | } 105 | if (process.env.CI && messages.warnings.length) { 106 | console.log( 107 | chalk.yellow( 108 | '\nTreating warnings as errors because process.env.CI = true.\n' + 109 | 'Most CI servers set it automatically.\n' 110 | ) 111 | ); 112 | return reject(new Error(messages.warnings.join('\n\n'))); 113 | } 114 | return resolve({ 115 | stats, 116 | previousFileSizes, 117 | warnings: messages.warnings, 118 | }); 119 | }); 120 | }); 121 | } 122 | 123 | function copyPublicFolder() { 124 | fs.copySync(paths.appPublic, paths.appBuild, { 125 | dereference: true, 126 | filter: file => file !== paths.appHtml, 127 | }); 128 | } 129 | -------------------------------------------------------------------------------- /scripts/start.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Do this as the first thing so that any code reading it knows the right env. 4 | process.env.BABEL_ENV = 'development'; 5 | process.env.NODE_ENV = 'development'; 6 | 7 | // Makes the script crash on unhandled rejections instead of silently 8 | // ignoring them. In the future, promise rejections that are not handled will 9 | // terminate the Node.js process with a non-zero exit code. 10 | process.on('unhandledRejection', err => { 11 | throw err; 12 | }); 13 | 14 | // Ensure environment variables are read. 15 | require('../config/env'); 16 | 17 | const fs = require('fs'); 18 | const chalk = require('chalk'); 19 | const webpack = require('webpack'); 20 | const WebpackDevServer = require('webpack-dev-server'); 21 | const clearConsole = require('react-dev-utils/clearConsole'); 22 | const checkRequiredFiles = require('react-dev-utils/checkRequiredFiles'); 23 | const { 24 | choosePort, 25 | createCompiler, 26 | prepareProxy, 27 | prepareUrls, 28 | } = require('react-dev-utils/WebpackDevServerUtils'); 29 | const openBrowser = require('react-dev-utils/openBrowser'); 30 | const paths = require('../config/paths'); 31 | const config = require('../config/webpack.config.dev'); 32 | const createDevServerConfig = require('../config/webpackDevServer.config'); 33 | 34 | const useYarn = fs.existsSync(paths.yarnLockFile); 35 | const isInteractive = process.stdout.isTTY; 36 | 37 | // Warn and crash if required files are missing 38 | if (!checkRequiredFiles([paths.appHtml, paths.appIndexJs])) { 39 | process.exit(1); 40 | } 41 | 42 | // Tools like Cloud9 rely on this. 43 | const DEFAULT_PORT = parseInt(process.env.PORT, 10) || 3000; 44 | const HOST = process.env.HOST || '0.0.0.0'; 45 | 46 | // We attempt to use the default port but if it is busy, we offer the user to 47 | // run on a different port. `detect()` Promise resolves to the next free port. 48 | choosePort(HOST, DEFAULT_PORT) 49 | .then(port => { 50 | if (port == null) { 51 | // We have not found a port. 52 | return; 53 | } 54 | const protocol = process.env.HTTPS === 'true' ? 'https' : 'http'; 55 | const appName = require(paths.appPackageJson).name; 56 | const urls = prepareUrls(protocol, HOST, port); 57 | // Create a webpack compiler that is configured with custom messages. 58 | const compiler = createCompiler(webpack, config, appName, urls, useYarn); 59 | // Load proxy config 60 | const proxySetting = require(paths.appPackageJson).proxy; 61 | const proxyConfig = prepareProxy(proxySetting, paths.appPublic); 62 | // Serve webpack assets generated by the compiler over a web sever. 63 | const serverConfig = createDevServerConfig( 64 | proxyConfig, 65 | urls.lanUrlForConfig 66 | ); 67 | const devServer = new WebpackDevServer(compiler, serverConfig); 68 | // Launch WebpackDevServer. 69 | devServer.listen(port, HOST, err => { 70 | if (err) { 71 | return console.log(err); 72 | } 73 | if (isInteractive) { 74 | clearConsole(); 75 | } 76 | console.log(chalk.cyan('Starting the development server...\n')); 77 | openBrowser(urls.localUrlForBrowser); 78 | }); 79 | 80 | ['SIGINT', 'SIGTERM'].forEach(function(sig) { 81 | process.on(sig, function() { 82 | devServer.close(); 83 | process.exit(); 84 | }); 85 | }); 86 | }) 87 | .catch(err => { 88 | if (err && err.message) { 89 | console.log(err.message); 90 | } 91 | process.exit(1); 92 | }); 93 | -------------------------------------------------------------------------------- /scripts/test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Do this as the first thing so that any code reading it knows the right env. 4 | process.env.BABEL_ENV = 'test'; 5 | process.env.NODE_ENV = 'test'; 6 | process.env.PUBLIC_URL = ''; 7 | 8 | // Makes the script crash on unhandled rejections instead of silently 9 | // ignoring them. In the future, promise rejections that are not handled will 10 | // terminate the Node.js process with a non-zero exit code. 11 | process.on('unhandledRejection', err => { 12 | throw err; 13 | }); 14 | 15 | // Ensure environment variables are read. 16 | require('../config/env'); 17 | 18 | const jest = require('jest'); 19 | const argv = process.argv.slice(2); 20 | 21 | // Watch unless on CI or in coverage mode 22 | if (!process.env.CI && argv.indexOf('--coverage') < 0) { 23 | argv.push('--watch'); 24 | } 25 | 26 | 27 | jest.run(argv); 28 | -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App-header { 6 | background-color: #222; 7 | height: 150px; 8 | padding: 20px; 9 | color: white; 10 | } 11 | 12 | .App-intro { 13 | font-size: large; 14 | } 15 | 16 | .button { 17 | flex: 1 18 | } 19 | 20 | #buttonsDiv { 21 | display: flex 22 | } -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import './App.css'; 3 | import HostName from './host/HostName'; 4 | import Player from './player/Player'; 5 | import Guest from './guest/Guest'; 6 | import AppBar from 'material-ui/AppBar'; 7 | import Peer from 'simple-peer'; 8 | import Paper from 'material-ui/Paper'; 9 | //import TestGame from './testgame/TestGame'; 10 | 11 | class App extends Component { 12 | constructor(){ 13 | super(); 14 | if(Peer.WEBRTC_SUPPORT){ // test for webrtc support 15 | this.state = { 16 | role: 'visitor' 17 | }; 18 | } else { 19 | this.state = { 20 | role: 'unsupported', 21 | playingGame: false 22 | }; 23 | } 24 | } 25 | 26 | getRoleContent = (role) => { 27 | if(this.state.role === 'host') { 28 | return ; 29 | } else if (this.state.role === 'player') { 30 | return ; 31 | } else if (this.state.role === 'unsupported') { 32 | return ( 33 |
34 | Your browser does not support WebRTC Data Channel 35 |
36 | ); 37 | } else { 38 | return this.setState({role: 'host'})} 40 | becomePlayer={() => this.setState({role: 'player'})} 41 | />; 42 | //return 43 | } 44 | } 45 | 46 | render() { 47 | return ( 48 |
49 | 54 | 63 | {this.getRoleContent(this.state.role)} 64 | 65 |
66 | ) 67 | } 68 | } 69 | 70 | export default App; 71 | -------------------------------------------------------------------------------- /src/game/Game.js: -------------------------------------------------------------------------------- 1 | import 'pixi.js'; 2 | import 'p2'; 3 | import Phaser from 'phaser'; 4 | import { random } from 'lodash'; 5 | import React from 'react'; 6 | 7 | // Image imports 8 | import sky from './assets/sky.png'; 9 | import ground from './assets/platform.png'; 10 | import dude from './assets/dude.png'; 11 | 12 | const gameWidth = 800; 13 | const gameHeight = 600; 14 | 15 | function createGame(inputPlayers, onUpdateCb, ignore, getPlayerInput) { 16 | const state = { preload: preload, create: create, update: update} 17 | let renderMode = Phaser.AUTO; 18 | if (ignore){ 19 | ignore.forEach((key) => { 20 | delete state[key]; 21 | }); 22 | } 23 | 24 | var game = new Phaser.Game(gameWidth, gameHeight, renderMode, 'gameDiv', state); 25 | 26 | function preload() { 27 | game.load.image('sky', sky); 28 | game.load.image('ground', ground); 29 | game.load.spritesheet('dude', dude, 32, 48); 30 | game.stage.disableVisibilityChange = true; 31 | } 32 | 33 | let players; 34 | let platforms; 35 | 36 | function create() { 37 | // We're going to be using physics, so enable the Arcade Physics system 38 | game.physics.startSystem(Phaser.Physics.ARCADE); 39 | 40 | // Prevent pausing when tabbed out 41 | game.stage.disableVisibilityChange = false; 42 | //Phaser.RequestAnimationFrame(game, true); 43 | 44 | // A simple background for our game 45 | game.add.sprite(0, 0, 'sky'); 46 | 47 | // The platforms group contains the ground and the 2 ledges we can jump on 48 | platforms = game.add.group(); 49 | 50 | // We will enable physics for any object that is created in this group 51 | platforms.enableBody = true; 52 | 53 | // Here we create the ground. 54 | var ground = platforms.create(0, game.world.height - 64, 'ground'); 55 | 56 | // Scale it to fit the width of the game (the original sprite is 400x32 in size) 57 | ground.scale.setTo(2, 2); 58 | 59 | // This stops it from falling away when you jump on it 60 | ground.body.immovable = true; 61 | 62 | // Destroy it when it leaves the screen 63 | ground.checkWorldBounds = true; 64 | ground.events.onOutOfBounds.add((o) => { 65 | o.destroy(); 66 | }); 67 | 68 | players = Object.assign({}, inputPlayers); 69 | for(const playerName in inputPlayers){ 70 | // The player and its settings 71 | const playerSprite = game.add.sprite(500, game.world.height - 150, 'dude'); 72 | 73 | // We need to enable physics on the player 74 | game.physics.arcade.enable(playerSprite); 75 | 76 | // Player physics properties. Give the little guy a slight bounce. 77 | playerSprite.body.bounce.y = 0; 78 | playerSprite.body.gravity.y = 1000; 79 | 80 | playerSprite.checkWorldBounds = true; 81 | playerSprite.events.onOutOfBounds.add((o) => { 82 | if(o.x < 0 || o.y > 0){ 83 | o.kill(); 84 | players[playerName].text.destroy(); 85 | delete players[playerName]; 86 | if(Object.keys(players).length === 0) { 87 | game.paused = true; 88 | const style = {font: '32px Calibri', boundsAlignH: 'center', boundsAlignV: 'middle', align: 'center'}; 89 | const text = game.add.text(0, 0, playerName + ' is the winner!\nRestarting in 5 seconds.', style); 90 | text.y = (game.height/2) - (text.height/2); 91 | text.x = (game.width/2) - (text.width/2); 92 | setTimeout(() => { 93 | game.paused = false; 94 | game.state.restart(); 95 | }, 5000) 96 | } 97 | } 98 | }); 99 | 100 | // Our two animations, walking left and right. 101 | playerSprite.animations.add('left', [0, 1, 2, 3], 10, true); 102 | playerSprite.animations.add('right', [5, 6, 7, 8], 10, true); 103 | players[playerName].sprite = playerSprite; 104 | 105 | const style = {font: '16px Arial', align: 'center'}; 106 | const text = game.add.text(0, 0, playerName, style); 107 | players[playerName].text = text; 108 | 109 | //debugText = game.add.text(0, 0, ''); 110 | }; 111 | } 112 | 113 | function addPlatform() { 114 | function getHeight(lastY) { 115 | // Get the range of valid heights for platforms 116 | const heightMax = game.height - 64; 117 | const heightMin = 200; 118 | 119 | const maxDiff = 100; 120 | 121 | // Get the range for the platform we are about to get 122 | // This is based off the last platform created 123 | // so that the player will be able to jump to it 124 | let rangeMax = lastY + maxDiff; 125 | if(rangeMax > heightMax) rangeMax = heightMax; 126 | let rangeMin = lastY - maxDiff; 127 | if(rangeMin < heightMin) rangeMin = heightMin; 128 | 129 | const num = random(rangeMin, rangeMax); 130 | return num; 131 | } 132 | 133 | const latestPlatform = platforms.getTop(); 134 | var ledge = platforms.create(game.width, getHeight(latestPlatform.y), 'ground'); 135 | ledge.body.immovable = true; 136 | ledge.checkWorldBounds = true; 137 | ledge.events.onOutOfBounds.add((o) => { 138 | o.destroy(); 139 | }); 140 | } 141 | 142 | function shouldAddPlatform(){ 143 | const pc = platforms.length; 144 | let chance; 145 | if(platforms.getTop().x > game.width - 200){ 146 | chance = 0; 147 | } else { 148 | switch(pc) { 149 | case 0: 150 | chance = 1/1; 151 | break; 152 | case 1: 153 | chance = 1/30; 154 | break; 155 | case 2: 156 | chance = 1/200; 157 | break; 158 | case 3: 159 | chance = 1/400; 160 | break; 161 | default: 162 | chance = 0; 163 | } 164 | } 165 | //debugText.text = chance; 166 | if(Math.random() < chance) { 167 | return true; 168 | } else { 169 | return false; 170 | } 171 | } 172 | 173 | function update() { 174 | const speed = 3; 175 | // Update players 176 | for(const playerName in players) { 177 | const input = getPlayerInput(playerName); 178 | const playerSprite = players[playerName].sprite; 179 | 180 | playerSprite.x -= speed; 181 | 182 | // Collide the player and the stars with the platforms 183 | game.physics.arcade.collide(playerSprite, platforms); 184 | 185 | // Reset the players velocity (movement) 186 | playerSprite.body.velocity.x = 0; 187 | 188 | if (input.left) 189 | { 190 | // Move to the left 191 | playerSprite.body.velocity.x = -200; 192 | 193 | playerSprite.animations.play('left'); 194 | } 195 | else if (input.right) 196 | { 197 | // Move to the right 198 | playerSprite.body.velocity.x = 400; 199 | 200 | playerSprite.animations.play('right'); 201 | } 202 | else 203 | { 204 | // Stand still 205 | playerSprite.animations.stop(); 206 | 207 | playerSprite.frame = 4; 208 | } 209 | 210 | // Allow the player to jump if they are touching the ground. 211 | if (input.up && playerSprite.body.touching.down) 212 | { 213 | playerSprite.body.velocity.y = -700; 214 | } 215 | 216 | const playerText = players[playerName].text; 217 | playerText.x = playerSprite.x+(playerSprite.width/2)-(playerText.width/2); 218 | playerText.y = playerSprite.y-22; 219 | } 220 | 221 | // Update Platforms 222 | platforms.forEach((platform) => { 223 | platform.x -= speed; 224 | }); 225 | 226 | // Possibly add another platform 227 | if(shouldAddPlatform()) { 228 | addPlatform(); 229 | } 230 | 231 | if(onUpdateCb != null) onUpdateCb(game); 232 | } 233 | 234 | 235 | return game; 236 | } 237 | 238 | function gameDiv() { 239 | return ( 240 |
241 |
242 |
243 | ); 244 | } 245 | 246 | export { createGame, gameDiv } -------------------------------------------------------------------------------- /src/game/Input.js: -------------------------------------------------------------------------------- 1 | function OnInputChange(_cb){ 2 | document.onkeydown = function(e) { 3 | onKey(e, true); 4 | }; 5 | 6 | document.onkeyup = function(e) { 7 | onKey(e, false); 8 | }; 9 | 10 | function doCB(e, input){ 11 | e.preventDefault(); 12 | _cb(input) 13 | } 14 | 15 | function onKey(e, state){ 16 | const cb = doCB.bind(this, e); 17 | switch (e.keyCode) { 18 | case 37: 19 | cb({left: state}); 20 | break; 21 | case 38: 22 | cb({up: state}); 23 | break; 24 | case 39: 25 | cb({right: state}); 26 | break; 27 | case 40: 28 | cb({down: state}); 29 | break; 30 | default: 31 | break; 32 | } 33 | } 34 | } 35 | 36 | const DefaultInput = () => { 37 | return { 38 | up: false, 39 | down: false, 40 | left: false, 41 | right: false 42 | }; 43 | } 44 | 45 | export { OnInputChange, DefaultInput }; -------------------------------------------------------------------------------- /src/game/State.js: -------------------------------------------------------------------------------- 1 | import 'pixi.js'; 2 | import 'p2'; 3 | import Phaser from 'phaser'; 4 | import { pick } from 'lodash'; 5 | 6 | function serializeState(world) { 7 | const {sprites, texts} = recursivelyGetData(world.children); 8 | const spriteData = getSpriteData(sprites); 9 | const textData = getTextData(texts); 10 | return {sprites: spriteData, texts: textData}; 11 | } 12 | 13 | function recursivelyGetData(children){ 14 | return children.reduce((data, child) => { 15 | // The order matters because Text is a Child of Sprite, 16 | // so it would evaluate true to both 17 | if(child instanceof Phaser.Text){ 18 | data.texts.push(child); 19 | } else if (child instanceof Phaser.Sprite) { 20 | data.sprites.push(child); 21 | } 22 | 23 | if(child.children.length > 0) { 24 | const {sprites, texts} = recursivelyGetData(child.children); 25 | data.sprites = data.sprites.concat(sprites); 26 | data.texts = data.texts.concat(texts); 27 | } 28 | 29 | return data; 30 | }, { 31 | sprites: [], 32 | texts: [] 33 | }); 34 | } 35 | 36 | function getSpriteData(sprites) { 37 | return sprites.map((sprite) => { 38 | return pick(sprite, ['x', 'y', 'key', 'frame', 'scale']) 39 | }); 40 | } 41 | 42 | function getTextData(texts) { 43 | return texts.map((text) => { 44 | return pick(text, ['x', 'y', 'text', 'style']) 45 | }); 46 | } 47 | 48 | export { serializeState }; -------------------------------------------------------------------------------- /src/game/assets/dude.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rynobax/jump-game/17e7341b5d54ceba659fba3f3e485b27a55ad3dc/src/game/assets/dude.png -------------------------------------------------------------------------------- /src/game/assets/platform.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rynobax/jump-game/17e7341b5d54ceba659fba3f3e485b27a55ad3dc/src/game/assets/platform.png -------------------------------------------------------------------------------- /src/game/assets/sky.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rynobax/jump-game/17e7341b5d54ceba659fba3f3e485b27a55ad3dc/src/game/assets/sky.png -------------------------------------------------------------------------------- /src/guest/Guest.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import RaisedButton from 'material-ui/RaisedButton'; 3 | 4 | const buttonDivStyle = { 5 | display: "flex" 6 | } 7 | 8 | const buttonStyle = { 9 | margin: 25, 10 | flex: 1 11 | }; 12 | 13 | const Guest = (props) => { 14 | return ( 15 |
16 | 22 | 28 |
29 | ); 30 | } 31 | 32 | export default Guest; -------------------------------------------------------------------------------- /src/host/Host.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import CircularProgress from 'material-ui/CircularProgress'; 3 | import LobbyList from '../lobby/LobbyList'; 4 | import * as firebase from 'firebase'; 5 | import SimplePeer from 'simple-peer'; 6 | import HostGame from './HostGame'; 7 | import { OnInputChange, DefaultInput } from '../game/Input'; 8 | 9 | const roomCodeOptions = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; 10 | 11 | function generateRoomCode() { 12 | let code = ''; 13 | for(let i=0; i<4; i++){ 14 | const ndx = Math.floor(Math.random() * roomCodeOptions.length); 15 | code += roomCodeOptions[ndx]; 16 | } 17 | return code; 18 | } 19 | 20 | function createRoom(room,){ 21 | return room.set({ 22 | 'createdAt': firebase.database.ServerValue.TIMESTAMP 23 | }); 24 | } 25 | 26 | function getOpenRoom(database){ 27 | return new Promise((resolve, reject) => { 28 | const code = generateRoomCode(); 29 | const room = database.ref('rooms/'+code); 30 | room.once('value').then((snapshot) => { 31 | const roomData = snapshot.val(); 32 | if (roomData == null) { 33 | // Room does not exist 34 | createRoom(room).then(resolve(code)); 35 | } else { 36 | const roomTimeout = 1800000; // 30 min 37 | const now = Date.now(); 38 | const msSinceCreated = now - roomData.createdAt; 39 | if (msSinceCreated > roomTimeout) { 40 | // It is an old room so wipe it and create a new one 41 | room.remove().then(() => createRoom(room)).then(resolve(code)); 42 | } else { 43 | // The room is in use so try a different code 44 | resolve(getOpenRoom(database)); 45 | } 46 | } 47 | }) 48 | }); 49 | } 50 | 51 | class Host extends Component { 52 | constructor(props){ 53 | super(props); 54 | const players = {}; 55 | players[props.name] = { 56 | host: true, 57 | ready: false, 58 | input: DefaultInput(), 59 | // Peer object with blank methods so I don't have to 60 | // filter when I iterate over players 61 | peer: { 62 | send: () => {} 63 | } 64 | } 65 | this.state = { 66 | players: players, 67 | code: null, 68 | gameStarted: false 69 | } 70 | 71 | this.database = null; 72 | this.hostName = props.name; 73 | 74 | this.copyPlayers = () => Object.assign({}, this.state.players); 75 | 76 | this.playersToArray = () => { 77 | const playersArr = []; 78 | for (const playerName in this.state.players){ 79 | playersArr.push({ 80 | name: playerName, 81 | input: this.state.players[playerName].input, 82 | peer: this.state.players[playerName].peer, 83 | ready: this.state.players[playerName].ready 84 | }); 85 | } 86 | return playersArr; 87 | } 88 | 89 | this.getPlayersForGame = () => { 90 | // Don't send peer info 91 | const players = {}; 92 | for(const playerName in this.state.players) { 93 | players[playerName] = { 94 | input: this.state.players[playerName].input 95 | } 96 | } 97 | return players; 98 | } 99 | 100 | // Send message to all players 101 | this.broadcast = (obj) => { 102 | for(const playerName in this.state.players){ 103 | const peer = this.state.players[playerName].peer; 104 | if(peer.connected) peer.send(JSON.stringify(obj)); 105 | } 106 | } 107 | 108 | this.broadcastPlayers = () => { 109 | this.broadcast({ 110 | type: 'players', 111 | players: this.playersToArray().map((e) => { 112 | return { 113 | name: e.name, 114 | ready: e.ready 115 | } 116 | }) 117 | }) 118 | } 119 | 120 | this.handleData = (playerName, data) => { 121 | switch(data.type){ 122 | case 'ready': 123 | this.handleReady(playerName, data.ready); 124 | break; 125 | case 'input': 126 | this.handleInput(playerName, data.input); 127 | break; 128 | case 'connected': 129 | this.handleConnected(playerName); 130 | break; 131 | default: 132 | throw Error('Unkown input ', data.type); 133 | } 134 | return; 135 | } 136 | 137 | // Input from players 138 | this.handleInput = (playerName, input) => { 139 | const playersCopy = this.copyPlayers(); 140 | for (const key in input) { 141 | playersCopy[playerName].input[key] = input[key]; 142 | } 143 | this.setState({players: playersCopy}); 144 | } 145 | 146 | // Input from host 147 | OnInputChange((input) => { 148 | this.handleInput(this.hostName, input); 149 | }); 150 | 151 | this.handleConnected = (playerName) => { 152 | // Workaround for https://github.com/feross/simple-peer/issues/178 153 | this.broadcastPlayers(); 154 | } 155 | 156 | this.handleReady = (name, ready) => { 157 | const p = this.copyPlayers(); 158 | p[name].ready = ready; 159 | this.setState({ 160 | players: p 161 | }, () => { 162 | // Update players of everyone's status 163 | this.broadcastPlayers(); 164 | 165 | // After updating the players ready status, check if the game should start 166 | let playerCount = 0; 167 | const playersReady = []; 168 | for(const playerName in this.state.players){ 169 | playerCount++; 170 | playersReady.push(this.state.players[playerName].ready); 171 | } 172 | if(playerCount > 0 && playersReady.every(e => e === true)){ 173 | // We have enough players and they are all ready 174 | this.setState({gameStarted: true}); 175 | 176 | // Send start game to all peers 177 | this.broadcast({type: 'startGame'}); 178 | 179 | // Delete the room 180 | this.database.ref('/rooms/'+this.state.code).remove(); 181 | } 182 | }); 183 | } 184 | } 185 | 186 | componentDidMount(){ 187 | const database = firebase.database(); 188 | this.database = database; 189 | getOpenRoom(database).then((code) => { 190 | // Display room code 191 | this.setState({code: code}); 192 | 193 | // Players signaling 194 | database.ref('/rooms/'+code+'/players').on('child_added', ({key: playerName}) => { 195 | // Create Peer channel 196 | const peer = new SimplePeer(); 197 | 198 | // Upload Host signals 199 | const signalDataRef = database.ref('/rooms/'+code+'/host/'+playerName); 200 | peer.on('signal', (signalData) => { 201 | const newSignalDataRef = signalDataRef.push(); 202 | newSignalDataRef.set({ 203 | data: JSON.stringify(signalData) 204 | }); 205 | }); 206 | 207 | // Add player to player list 208 | // Use fake peer so broadcasts don't fail 209 | const playersCopy = this.copyPlayers(); 210 | playersCopy[playerName] = { 211 | peer: peer, 212 | //_peer: peer, 213 | ready: false, 214 | input: DefaultInput() 215 | } 216 | this.setState({players: playersCopy}, () => { 217 | // And notify other players 218 | this.broadcastPlayers(); 219 | }); 220 | 221 | // Listen for player singnaling data 222 | const playerRef = database.ref('/rooms/'+code+'/players/'+playerName); 223 | playerRef.on('child_added', (res) => peer.signal(JSON.parse(res.val().data))); 224 | 225 | // Listen to messages from player 226 | peer.on('data', (data) => { 227 | this.handleData(playerName, JSON.parse(data)); 228 | }); 229 | 230 | // Player disconnect 231 | peer.on('close', () => { 232 | // Delete local ref to player 233 | const playersCopy = Object.assign({}, this.state.players); 234 | delete playersCopy[playerName]; 235 | this.setState({players: playersCopy}); 236 | 237 | // Delete remote ref to player 238 | playerRef.remove(); 239 | 240 | // Remove callbacks 241 | playerRef.off('child_added'); 242 | 243 | // Delete remote signaling to player 244 | signalDataRef.remove(); 245 | 246 | // Delete peer reference 247 | peer.destroy(); 248 | }); 249 | }); 250 | }); 251 | } 252 | 253 | render() { 254 | const codeNode = () => { 255 | if(this.state.code){ 256 | return this.state.code; 257 | } else { 258 | return ; 259 | } 260 | } 261 | 262 | // Push players into array so it's easier to work with in the game 263 | const playersArr = this.playersToArray(); 264 | 265 | if(this.state.gameStarted){ 266 | // Display the game once it starts 267 | return 268 | } else { 269 | // Not enough players or not all players are ready 270 | return ( 271 |
272 |

Room Code: {codeNode()}

273 | 274 |
275 | ) 276 | } 277 | } 278 | } 279 | 280 | export default Host; -------------------------------------------------------------------------------- /src/host/HostGame.js: -------------------------------------------------------------------------------- 1 | import { Component } from 'react'; 2 | import { serializeState } from '../game/State'; 3 | import { createGame, gameDiv } from '../game/Game'; 4 | 5 | 6 | class HostGame extends Component { 7 | componentDidMount(){ 8 | const {players, broadcast} = this.props; 9 | 10 | const onUpdateCb = (game) => { 11 | broadcast({ 12 | type: 'gameUpdate', 13 | gameState: serializeState(game.world) 14 | }); 15 | } 16 | 17 | const getPlayerInput = (playerName) => { 18 | return this.props.players[playerName].input; 19 | } 20 | 21 | createGame(players, onUpdateCb, null, getPlayerInput); 22 | } 23 | 24 | render() { 25 | return gameDiv(); 26 | } 27 | } 28 | 29 | export default HostGame; -------------------------------------------------------------------------------- /src/host/HostName.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import TextField from 'material-ui/TextField'; 3 | import RaisedButton from 'material-ui/RaisedButton'; 4 | import Host from './Host'; 5 | 6 | class LobbyList extends Component { 7 | constructor() { 8 | super(); 9 | this.state = { 10 | name: '', 11 | submitted: false 12 | } 13 | } 14 | render() { 15 | if(!this.state.submitted){ 16 | return ( 17 |
18 | this.setState({name: v})} 22 | /> 23 |
24 | this.setState({submitted: true})} 27 | disabled={this.state.name.length === 0}/> 28 |
29 | ) 30 | } else { 31 | return 32 | } 33 | } 34 | } 35 | 36 | export default LobbyList; -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | import registerServiceWorker from './registerServiceWorker'; 5 | import injectTapEventPlugin from 'react-tap-event-plugin'; 6 | import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider'; 7 | import lightBaseTheme from 'material-ui/styles/baseThemes/lightBaseTheme'; 8 | import getMuiTheme from 'material-ui/styles/getMuiTheme'; 9 | import * as firebase from "firebase"; 10 | 11 | firebase.initializeApp({ 12 | apiKey: "AIzaSyC--x3JTzuP0ezakeUJKLtsiu--iyW1xrA", 13 | authDomain: "jump-game.firebaseapp.com", 14 | databaseURL: "https://jump-game.firebaseio.com", 15 | projectId: "jump-game", 16 | storageBucket: "jump-game.appspot.com", 17 | messagingSenderId: "1088406848683" 18 | }); 19 | 20 | injectTapEventPlugin(); 21 | 22 | const ThemedApp = () => { 23 | return ( 24 | 25 | 26 | 27 | ); 28 | }; 29 | 30 | ReactDOM.render(, document.getElementById('root')); 31 | registerServiceWorker(); 32 | -------------------------------------------------------------------------------- /src/lobby/LobbyList.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import {List, ListItem} from 'material-ui/List'; 3 | import CheckBox from 'material-ui/svg-icons/toggle/check-box'; 4 | import CheckBoxOutline from 'material-ui/svg-icons/toggle/check-box-outline-blank'; 5 | import RaisedButton from 'material-ui/RaisedButton'; 6 | 7 | class LobbyList extends Component { 8 | constructor() { 9 | super(); 10 | this.state = { 11 | ready: false 12 | } 13 | } 14 | 15 | render() { 16 | const {players} = this.props; 17 | let button = ''; 18 | if(this.state.ready) { 19 | button = { 20 | this.props.checkFunction(false); 21 | this.setState({ready: false}); 22 | }} /> 23 | } else { 24 | button = { 25 | this.props.checkFunction(true); 26 | this.setState({ready: true}); 27 | }} /> 28 | } 29 | return ( 30 |
31 |

To join, have your friends navigate to this page, and use the code to join!

32 | Game will start when all players are ready 33 |
34 | 35 | {players.map(({name, ready}, i) => { 36 | return : } key={i}/> 37 | })} 38 | 39 | {button} 40 |
41 | ) 42 | } 43 | } 44 | 45 | export default LobbyList; -------------------------------------------------------------------------------- /src/player/DisplayGame.js: -------------------------------------------------------------------------------- 1 | import { Component } from 'react'; 2 | import { createGame, gameDiv } from '../game/Game'; 3 | 4 | class DisplayGame extends Component { 5 | componentDidMount(){ 6 | this.game = createGame(null, null, [ 7 | 'create', 8 | 'update' 9 | ]); 10 | 11 | this.objects = []; 12 | } 13 | 14 | componentWillReceiveProps(nextProps){ 15 | const {gameState} = nextProps; 16 | this.objects.forEach((obj) => { 17 | obj.destroy(); 18 | }); 19 | const spritesData = gameState.sprites; 20 | spritesData.forEach(({x, y, key, frame, scale}) => { 21 | const sprite = this.game.add.sprite(x, y, key, frame); 22 | sprite.scale.setTo(scale.x, scale.y); 23 | this.objects.push(sprite); 24 | }); 25 | const textData = gameState.texts; 26 | textData.forEach(({x, y, text, style}) => { 27 | const textObj = this.game.add.text(x, y, text, style); 28 | this.objects.push(textObj); 29 | }); 30 | return true; 31 | } 32 | 33 | render() { 34 | return gameDiv(); 35 | } 36 | } 37 | 38 | export default DisplayGame; -------------------------------------------------------------------------------- /src/player/Player.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import TextField from 'material-ui/TextField'; 3 | import RaisedButton from 'material-ui/RaisedButton'; 4 | import * as firebase from 'firebase'; 5 | import SimplePeer from 'simple-peer'; 6 | import DisplayGame from './DisplayGame'; 7 | import LobbyList from '../lobby/LobbyList'; 8 | import { OnInputChange } from '../game/Input'; 9 | import CircularProgress from 'material-ui/CircularProgress'; 10 | 11 | class Player extends Component { 12 | constructor() { 13 | super(); 14 | this.state = { 15 | code: '', 16 | name: '', 17 | connected: false, 18 | connecting: false, 19 | gameStarted: false, 20 | error: '', 21 | database: firebase.database(), 22 | host: null, 23 | players: [], 24 | gameState: { 25 | sprites: [] 26 | } 27 | } 28 | 29 | this.peer = null; 30 | 31 | this.broadcast = (obj) => { 32 | this.peer.send(JSON.stringify(obj)); 33 | } 34 | 35 | this.sendReady = (ready) => { 36 | this.broadcast({ 37 | type: 'ready', 38 | ready: ready 39 | }); 40 | } 41 | 42 | this.handleData = (data) => { 43 | switch(data.type){ 44 | case 'startGame': 45 | this.setState({gameStarted: true}); 46 | break; 47 | case 'players': 48 | this.setState({players: data.players}); 49 | break; 50 | case 'gameUpdate': 51 | this.trackInputs(); 52 | this.setState({gameState: data.gameState}); 53 | break; 54 | default: 55 | throw Error('Unknown input ', data.type); 56 | } 57 | return; 58 | } 59 | 60 | this.trackInputs = () => { 61 | OnInputChange((input) => { 62 | this.broadcast({ 63 | type: 'input', 64 | input: input 65 | }) 66 | }); 67 | } 68 | } 69 | 70 | joinGame = () => { 71 | this.setState({error: '', connecting: true}); 72 | const {code, database, name} = this.state; 73 | const nameRef = database.ref('/rooms/'+code+'/players/'+name); 74 | nameRef.once('value').then((data) => { 75 | const val = data.val(); 76 | if (val) { 77 | // Name is taken 78 | return this.setState({error: 'Name is taken', connecting: false}); 79 | } else { 80 | // Store reference to peer 81 | const peer = new SimplePeer({initiator: true}); 82 | this.peer = peer; 83 | 84 | // Sending signal 85 | peer.on('signal', (signalData) => { 86 | const newSignalDataRef = nameRef.push(); 87 | newSignalDataRef.set({ 88 | data: JSON.stringify(signalData) 89 | }); 90 | }); 91 | 92 | // Recieving signal 93 | const hostSignalRef = database.ref('/rooms/'+code+'/host/'+name); 94 | hostSignalRef.on('child_added', (res) => { 95 | peer.signal(JSON.parse(res.val().data)); 96 | }); 97 | 98 | // Connecting 99 | peer.on('connect', () => { 100 | 101 | // The connection is established, so disconnect from firebase 102 | database.goOffline(); 103 | 104 | // connect event is broken in chrome tabs or something, so this works around it for host 105 | // https://github.com/feross/simple-peer/issues/178 106 | setTimeout(() => { 107 | this.broadcast({ 108 | type: 'connected' 109 | }); 110 | this.setState({connected: true, connecting: false}) 111 | }, 1000); 112 | }); 113 | 114 | // Data 115 | peer.on('data', (data) => { 116 | // got a data channel message 117 | this.handleData(JSON.parse(data)); 118 | }); 119 | 120 | // Host disconnect 121 | peer.on('close', () => { 122 | // Update UI 123 | this.setState({ 124 | gameStarted: false, 125 | connected: false, 126 | error: 'Disconnected from host', 127 | code: '' 128 | }); 129 | 130 | // Reconnect to firebase 131 | database.goOnline(); 132 | 133 | // Remove room 134 | database.ref('/rooms/'+code).remove(); 135 | // TODO: Allow another host to join and continue game? 136 | }); 137 | } 138 | }) 139 | } 140 | 141 | render() { 142 | if(this.state.connected){ 143 | if(this.state.gameStarted){ 144 | return 145 | } else { 146 | return 147 | } 148 | } else { 149 | if(this.state.connecting){ 150 | return 151 | } else { 152 | return ( 153 |
154 |

Join a Game

155 | this.setState({code: v.toUpperCase()})} 161 | errorText={this.state.error} 162 | /> 163 |
164 | this.setState({name: v})} 170 | /> 171 |
172 | 177 |
178 | ); 179 | } 180 | } 181 | } 182 | } 183 | 184 | export default Player; 185 | -------------------------------------------------------------------------------- /src/registerServiceWorker.js: -------------------------------------------------------------------------------- 1 | // In production, we register a service worker to serve assets from local cache. 2 | 3 | // This lets the app load faster on subsequent visits in production, and gives 4 | // it offline capabilities. However, it also means that developers (and users) 5 | // will only see deployed updates on the "N+1" visit to a page, since previously 6 | // cached resources are updated in the background. 7 | 8 | // To learn more about the benefits of this model, read https://goo.gl/KwvDNy. 9 | // This link also includes instructions on opting out of this behavior. 10 | 11 | export default function register() { 12 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 13 | window.addEventListener('load', () => { 14 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 15 | navigator.serviceWorker 16 | .register(swUrl) 17 | .then(registration => { 18 | registration.onupdatefound = () => { 19 | const installingWorker = registration.installing; 20 | installingWorker.onstatechange = () => { 21 | if (installingWorker.state === 'installed') { 22 | if (navigator.serviceWorker.controller) { 23 | // At this point, the old content will have been purged and 24 | // the fresh content will have been added to the cache. 25 | // It's the perfect time to display a "New content is 26 | // available; please refresh." message in your web app. 27 | console.log('New content is available; please refresh.'); 28 | } else { 29 | // At this point, everything has been precached. 30 | // It's the perfect time to display a 31 | // "Content is cached for offline use." message. 32 | console.log('Content is cached for offline use.'); 33 | } 34 | } 35 | }; 36 | }; 37 | }) 38 | .catch(error => { 39 | console.error('Error during service worker registration:', error); 40 | }); 41 | }); 42 | } 43 | } 44 | 45 | export function unregister() { 46 | if ('serviceWorker' in navigator) { 47 | navigator.serviceWorker.ready.then(registration => { 48 | registration.unregister(); 49 | }); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/testgame/TestGame.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import HostGame from '../host/HostGame'; 3 | import { OnInputChange, DefaultInput } from '../game/Input'; 4 | 5 | /* 6 | This is used so I can test the game without going through the menus 7 | */ 8 | 9 | class TestGame extends Component { 10 | constructor(props){ 11 | super(props); 12 | const players = {}; 13 | this.hostName = 'Ryan'; 14 | players[this.hostName] = { 15 | host: true, 16 | ready: false, 17 | input: DefaultInput(), 18 | // Peer object with blank methods so I don't have to 19 | // filter when I iterate over players 20 | peer: { 21 | send: () => {} 22 | } 23 | } 24 | 25 | this.state = { 26 | players: players 27 | } 28 | 29 | this.copyPlayers = () => Object.assign({}, this.state.players); 30 | 31 | this.getPlayersForGame = () => { 32 | // Don't send peer info 33 | const players = {}; 34 | for(const playerName in this.state.players) { 35 | players[playerName] = { 36 | input: this.state.players[playerName].input 37 | } 38 | } 39 | return players; 40 | } 41 | 42 | // Input from players 43 | this.handleInput = (playerName, input) => { 44 | const playersCopy = this.copyPlayers(); 45 | for (const key in input) { 46 | playersCopy[playerName].input[key] = input[key]; 47 | } 48 | this.setState({players: playersCopy}); 49 | } 50 | 51 | // Input from host 52 | OnInputChange((input) => { 53 | this.handleInput(this.hostName, input); 54 | }); 55 | } 56 | 57 | render() { 58 | return {}}/> 59 | } 60 | } 61 | 62 | export default TestGame; --------------------------------------------------------------------------------