├── .gitignore ├── public └── js │ ├── client.js │ └── game │ ├── Drawing.js │ ├── Game.js │ └── Input.js ├── views └── index.pug ├── package.json ├── LICENSE ├── README.md ├── lib ├── Player.js ├── Game.js ├── Entity2D.js └── Entity3D.js ├── server.js └── shared └── Util.js /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /public/dist 3 | -------------------------------------------------------------------------------- /public/js/client.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Client side script 3 | * @author alvin@omgimanerd.tech (Alvin Lin) 4 | */ 5 | 6 | $(document).ready(function() { 7 | var socket = io(); 8 | var canvas = document.getElementById('canvas'); 9 | 10 | Input.applyEventHandlers(); 11 | Input.addMouseTracker(canvas); 12 | 13 | var game = Game.create(socket, canvas); 14 | game.init(); 15 | game.animate(); 16 | }); 17 | -------------------------------------------------------------------------------- /views/index.pug: -------------------------------------------------------------------------------- 1 | doctype html 2 | 3 | html 4 | head 5 | title Game! 6 | body 7 | canvas(id='canvas' width='800px' height='600px') 8 | script(src='/socket.io/socket.io.js') 9 | script(src='/node_modules/jquery/dist/jquery.min.js') 10 | script(src='/shared/Util.js') 11 | script(src='/public/js/game/Drawing.js') 12 | script(src='/public/js/game/Game.js') 13 | script(src='/public/js/game/Input.js') 14 | script(src='/public/js/client.js') 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "game-framework", 3 | "version": "1.0.0", 4 | "description": "A basic multiplayer game framework", 5 | "main": "server.js", 6 | "repository": { 7 | "type": "git", 8 | "url": "git+ssh://git@github.com/penumbragames/game-framework.git" 9 | }, 10 | "author": { 11 | "name": "Alvin Lin", 12 | "email": "alvin@omgimanerd.tech", 13 | "url": "http://omgimanerd.tech" 14 | }, 15 | "license": "MIT", 16 | "bugs": { 17 | "url": "https://github.com/penumbragames/game-framework/issues" 18 | }, 19 | "dependencies": { 20 | "express": "^4.14.1", 21 | "hashmap": "^2.0.6", 22 | "jquery": "^3.5.0", 23 | "morgan": "^1.8.1", 24 | "pug": "^2.0.0-beta11", 25 | "socket.io": "^2.4.0" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2017 Penumbra Games 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # game-framework 2 | This is a generic multiplayer game framework used by me. The master branch 3 | contains a minimal implementation of multiplayer interaction on top of which 4 | more functionality can be added. Read about it How To Build A Multiplayer Game 5 | [part one](https://medium.com/@omgimanerd/how-to-build-a-multiplayer-browser-game-4a793818c29b#.cgcrdee49) 6 | and [part two](https://medium.com/@omgimanerd/how-to-build-a-multiplayer-browser-game-part-2-2edd112aabdf#.nze6w0pgs). 7 | 8 | ## Games Built With This Framework 9 | - [tankanarchy](https://www.github.com/penumbragames/tankanarchy) 10 | - [blockwars](https://www.github.com/penumbragames/blockwars) 11 | - [obsidio](https://www.github.com/penumbragames/obsidio) 12 | - [git-to-the-hub](https://www.github.com/penumbragames/git-to-the-hub) 13 | - [fightmeirl](https://www.github.com/omgimanerd/fightmeirl) 14 | - [explosive-firerrhea](https://www.github.com/karlcoehlo/brickhack3) 15 | 16 | ## Setting Up: 17 | This project requires node version 0.12 or greater. 18 | ``` 19 | npm install 20 | ``` 21 | 22 | ## Creators: 23 | - Alvin Lin (omgimanerd) 24 | - Kenneth Li (noobbyte) 25 | 26 | ## License 27 | [MIT](https://opensource.org/licenses/MIT) 28 | 29 | © 2017 Penumbra Games 30 | -------------------------------------------------------------------------------- /lib/Player.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview This is a class encapsulating a Player. 3 | * @author alvin@omgimanerd.tech (Alvin Lin) 4 | */ 5 | 6 | const Entity2D = require('./Entity2D'); 7 | 8 | const Util = require('../shared/Util'); 9 | 10 | /** 11 | * Constructor for a Player 12 | * @constructor 13 | * @param {string} id The socket ID of the Player 14 | */ 15 | function Player(id) { 16 | Entity2D.call(this, [10, 10], null, null, null, null, Player.HITBOX); 17 | this.id = id; 18 | } 19 | Util.extend(Player, Entity2D); 20 | 21 | Player.HITBOX = 10; 22 | 23 | /** 24 | * Factory method for creating a Player 25 | * @param {string} id The socket ID of the Player 26 | * @return {Player} 27 | */ 28 | Player.create = function(id) { 29 | return new Player(id); 30 | }; 31 | 32 | /** 33 | * Updates the Player based on received input. 34 | * @param {Object} keyboardState The keyboard input received. 35 | */ 36 | Player.prototype.updateOnInput = function(keyboardState) { 37 | this.vy = 100 * (Number(keyboardState.down) - Number(keyboardState.up)); 38 | this.vx = 100 * (Number(keyboardState.right) - Number(keyboardState.left)); 39 | }; 40 | 41 | /** 42 | * Steps the Player forward in time and updates the internal position, velocity, 43 | * etc. 44 | */ 45 | Player.prototype.update = function() { 46 | this.parent.update.call(this); 47 | }; 48 | 49 | module.exports = Player; 50 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview This is the server app script. 3 | * @author alvin@omgimanerd.tech (Alvin Lin) 4 | */ 5 | 6 | const DEV_MODE = process.argv.indexOf('--dev') != -1; 7 | const FPS = 60; 8 | const PORT = process.env.PORT || 5000; 9 | 10 | // Dependencies. 11 | const express = require('express'); 12 | const http = require('http'); 13 | const morgan = require('morgan'); 14 | const socketIO = require('socket.io'); 15 | 16 | const Game = require('./lib/Game'); 17 | 18 | // Initialization. 19 | var app = express(); 20 | var server = http.Server(app); 21 | var io = socketIO(server); 22 | var game = Game.create(); 23 | 24 | app.set('port', PORT); 25 | app.set('view engine', 'pug'); 26 | 27 | app.use(morgan('dev')); 28 | app.use('/node_modules', express.static(__dirname + '/node_modules')) 29 | app.use('/public', express.static(__dirname + '/public')); 30 | app.use('/shared', express.static(__dirname + '/shared')); 31 | 32 | app.use('/', (request, response) => { 33 | response.render('index'); 34 | }); 35 | 36 | /** 37 | * Server side input handler, modifies the state of the players and the 38 | * game based on the input it receives. Everything here runs asynchronously. 39 | */ 40 | io.on('connection', (socket) => { 41 | socket.on('player-join', () => { 42 | game.addNewPlayer(socket); 43 | }); 44 | 45 | socket.on('player-action', (data) => { 46 | game.updatePlayerOnInput(socket.id, data); 47 | }); 48 | 49 | socket.on('disconnect', () => { 50 | game.removePlayer(socket.id); 51 | }) 52 | }); 53 | 54 | /** 55 | * Server side game loop. This runs at 60 frames per second. 56 | */ 57 | setInterval(() => { 58 | game.update(); 59 | game.sendState(); 60 | }, 1000 / FPS); 61 | 62 | /** 63 | * Starts the server. 64 | */ 65 | server.listen(PORT, function() { 66 | console.log(`STARTING SERVER ON PORT ${PORT}`); 67 | if (DEV_MODE) { 68 | console.log('DEVELOPMENT MODE ENABLED') 69 | } 70 | }); 71 | -------------------------------------------------------------------------------- /public/js/game/Drawing.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Class handling the drawing of objects in the game. 3 | * @author kennethli.3470@gmail.com (Kenneth Li) 4 | */ 5 | 6 | /** 7 | * Creates a Drawing object. 8 | * @param {CanvasRenderingContext2D} context The context this object will 9 | * draw to. 10 | * @constructor 11 | */ 12 | function Drawing(context) { 13 | this.context = context; 14 | } 15 | 16 | /** 17 | * This is a factory method for creating a Drawing object. 18 | * @param {CanvasRenderingContext2D} context The context this object will 19 | * draw to. 20 | * @return {Drawing} 21 | */ 22 | Drawing.create = function(context) { 23 | return new Drawing(context); 24 | }; 25 | 26 | /** 27 | * This method creates and returns an Image object. 28 | * @param {string} src The path to the image 29 | * @param {number} width The width of the image in pixels 30 | * @param {number} height The height of the image in pixels 31 | * @return {Image} 32 | */ 33 | Drawing.createImage = function(src, width, height) { 34 | var image = new Image(width, height); 35 | image.src = src; 36 | return image; 37 | }; 38 | 39 | /** 40 | * Clears the canvas context. 41 | */ 42 | Drawing.prototype.clear = function() { 43 | var canvas = this.context.canvas; 44 | this.context.clearRect(0, 0, canvas.width, canvas.height); 45 | }; 46 | 47 | /** 48 | * Draws the player's sprite as a red circle. 49 | * @param {number} x The x coordinate of the player 50 | * @param {number} y The y coordinate of the player 51 | * @param {number} size The radial size of the player 52 | */ 53 | Drawing.prototype.drawSelf = function(x, y, size) { 54 | this.context.save(); 55 | this.context.beginPath(); 56 | this.context.fillStyle = 'green'; 57 | this.context.arc(x, y, size, 0, Math.PI * 2); 58 | this.context.fill(); 59 | this.context.restore(); 60 | }; 61 | 62 | /** 63 | * Draws other players' sprite as a red circle. 64 | * @param {number} x The x coordinate of the player 65 | * @param {number} y The y coordinate of the player 66 | * @param {number} size The radial size of the player 67 | */ 68 | Drawing.prototype.drawOther = function(x, y, size) { 69 | this.context.save(); 70 | this.context.beginPath(); 71 | this.context.fillStyle = 'red'; 72 | this.context.arc(x, y, size, 0, Math.PI * 2); 73 | this.context.fill(); 74 | this.context.restore(); 75 | }; 76 | -------------------------------------------------------------------------------- /lib/Game.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview This class encapsulates an active game on the server and 3 | * handles game updates. 4 | * @author alvin@omgimanerd.tech (Alvin Lin) 5 | */ 6 | 7 | const HashMap = require('hashmap'); 8 | const Player = require('./Player'); 9 | const Util = require('../shared/Util'); 10 | 11 | /** 12 | * Constructor for a Game object. 13 | * @constructor 14 | */ 15 | function Game() { 16 | this.clients = new HashMap(); 17 | this.players = new HashMap(); 18 | } 19 | 20 | /** 21 | * Factory method for a Game object. 22 | * @return {Game} 23 | */ 24 | Game.create = function() { 25 | return new Game(); 26 | }; 27 | 28 | /** 29 | * Returns a list containing the connected Player objects. 30 | * @return {Array} 31 | */ 32 | Game.prototype.getPlayers = function() { 33 | return this.players.values(); 34 | }; 35 | 36 | /** 37 | * Returns callbacks that can be passed into an update() 38 | * method for an object so that it can access other elements and 39 | * entities in the game. 40 | * @return {Object} 41 | */ 42 | Game.prototype._callbacks = function() { 43 | return { 44 | players: Util.bind(this, this.players) 45 | }; 46 | }; 47 | 48 | Game.prototype.addNewPlayer = function(socket, data) { 49 | this.clients.set(socket.id, socket); 50 | this.players.set(socket.id, Player.create(socket.id, [10, 10])); 51 | }; 52 | 53 | Game.prototype.removePlayer = function(id) { 54 | this.clients.remove(id); 55 | this.players.remove(id); 56 | } 57 | 58 | /** 59 | * Updates a player based on input received from their client. 60 | * @param {string} id The socket ID of the client 61 | * @param {Object} data The input received from the client 62 | */ 63 | Game.prototype.updatePlayerOnInput = function(id, data) { 64 | var player = this.players.get(id); 65 | if (player) { 66 | player.updateOnInput(data.keyboardState); 67 | } 68 | } 69 | 70 | /** 71 | * Steps the server forward in time. Updates every entity in the game. 72 | */ 73 | Game.prototype.update = function() { 74 | var players = this.getPlayers(); 75 | for (var i = 0; i < players.length; ++i) { 76 | players[i].update(); 77 | } 78 | }; 79 | 80 | /** 81 | * Sends the state of the game to every client. 82 | */ 83 | Game.prototype.sendState = function() { 84 | var ids = this.clients.keys(); 85 | for (var i = 0; i < ids.length; ++i) { 86 | this.clients.get(ids[i]).emit('update', { 87 | self: this.players.get(ids[i]), 88 | players: this.players.values().filter((player) => player.id != ids[i]) 89 | }); 90 | } 91 | }; 92 | 93 | module.exports = Game; 94 | -------------------------------------------------------------------------------- /lib/Entity2D.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview This is a wrapper class for 3D entities. 3 | * @author alvin@omgimanerd.tech (Alvin Lin) 4 | */ 5 | 6 | const Util = require('../shared/Util'); 7 | 8 | const DIMENSIONS = 2; 9 | 10 | /** 11 | * Constructor for an Entity2D. 12 | * @constructor 13 | * @param {Array} position The position of the Entity2D 14 | * @param {Array} velocity The velocity of the Entity2D 15 | * @param {Array} acceleration The acceleration of the Entity2D 16 | * @param {number} orientation The orientation of the Entity2D (radians) 17 | * @param {number} mass The mass of the Entity2D 18 | * @param {number} hitbox The radius of the Entity2D's circular hitbox 19 | */ 20 | function Entity2D(position, velocity, acceleration, orientation, mass, hitbox) { 21 | this.position = position || [0, 0]; 22 | this.velocity = velocity || [0, 0]; 23 | this.acceleration = acceleration || [0, 0]; 24 | this.orientation = orientation || 0; 25 | this.mass = mass || 1; 26 | this.hitbox = hitbox || 0; 27 | 28 | this.lastUpdateTime = 0; 29 | this.deltaTime = 0; 30 | 31 | Util.splitProperties(this, ['x', 'y'], 'position'); 32 | Util.splitProperties(this, ['vx', 'vy'], 'velocity'); 33 | Util.splitProperties(this, ['ax', 'ay'], 'acceleration'); 34 | } 35 | 36 | /** 37 | * Applies a force to the Entity2D. 38 | * @param {Array} force The force to apply 39 | */ 40 | Entity2D.prototype.applyForce = function(force) { 41 | for (var i = 0; i < DIMENSIONS; ++i) { 42 | this.acceleration[i] += force[i] / this.mass; 43 | } 44 | }; 45 | 46 | /** 47 | * Returns true if this Entity2D is contact with or intersecting another 48 | * Entity2D. 49 | * @param {Entity2D} other The other Entity2D to check against 50 | * @return {boolean} 51 | */ 52 | Entity2D.prototype.isCollidedWith = function(other) { 53 | var minDistance = (this.hitbox + other.hitbox); 54 | return Util.getEuclideanDistance2(this._position, other.position) <= 55 | (minDistance * minDistance); 56 | }; 57 | 58 | /** 59 | * Steps this Entity2D forward in time and updates the position, velocity, 60 | * and acceleration. 61 | * @param {?number=} deltaTime An optional deltaTime to update with 62 | */ 63 | Entity2D.prototype.update = function(deltaTime) { 64 | var currentTime = (new Date()).getTime(); 65 | if (deltaTime) { 66 | this.deltaTime = deltaTime; 67 | } else if (this.lastUpdateTime === 0) { 68 | this.deltaTime = 0; 69 | } else { 70 | this.deltaTime = (currentTime - this.lastUpdateTime) / 1000; 71 | } 72 | for (var i = 0; i < DIMENSIONS; ++i) { 73 | this.position[i] += this.velocity[i] * this.deltaTime; 74 | this.velocity[i] += this.acceleration[i] * this.deltaTime; 75 | this.acceleration[i] = 0; 76 | } 77 | this.lastUpdateTime = currentTime; 78 | }; 79 | 80 | module.exports = Entity2D; 81 | -------------------------------------------------------------------------------- /lib/Entity3D.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview This is a wrapper class for 3D entities. 3 | * @author alvin@omgimanerd.tech (Alvin Lin) 4 | */ 5 | 6 | const Util = require('../shared/Util'); 7 | 8 | const DIMENSIONS = 3; 9 | 10 | /** 11 | * Constructor for an Entity3D. 12 | * @constructor 13 | * @param {Array} position The position of the Entity3D 14 | * @param {Array} velocity The velocity of the Entity3D 15 | * @param {Array} acceleration The acceleration of the Entity3D 16 | * @param {number} orientation The orientation of the Entity3D (radians) 17 | * @param {number} mass The mass of the Entity3D 18 | * @param {number} hitbox The radius of the Entity3D's spherical hitbox 19 | */ 20 | function Entity3D(position, velocity, acceleration, orientation, mass, hitbox) { 21 | this.position = position || [0, 0, 0]; 22 | this.velocity = velocity || [0, 0, 0]; 23 | this.acceleration = acceleration || [0, 0, 0]; 24 | this.orientation = orientation || 0; 25 | this.mass = mass || 1; 26 | this.hitbox = hitbox || 0; 27 | 28 | this.lastUpdateTime = 0; 29 | this.deltaTime = 0; 30 | 31 | Util.splitProperties(this, ['x', 'y', 'z'], 'position'); 32 | Util.splitProperties(this, ['vx', 'vy', 'vz'], 'velocity'); 33 | Util.splitProperties(this, ['ax', 'ay', 'az'], 'acceleration'); 34 | } 35 | 36 | /** 37 | * Applies a force to the Entity3D 38 | * @param {Array} force The force to apply 39 | */ 40 | Entity3D.prototype.applyForce = function(force) { 41 | for (var i = 0; i < DIMENSIONS; ++i) { 42 | this.acceleration[i] += force[i] / this.mass; 43 | } 44 | }; 45 | 46 | /** 47 | * Returns true if this Entity3D is in contact with or intersecting another 48 | * Entity3D. 49 | * @param {Entity3D} other The other Entity3D to check against 50 | * @return {boolean} 51 | */ 52 | Entity3D.prototype.isCollidedWith = function(other) { 53 | var minDistance = (this.hitbox + other.hitbox); 54 | return Util.getEuclideanDistance2(this._position, other.position) < 55 | (minDistance * minDistance); 56 | }; 57 | 58 | /** 59 | * Steps this Entity3D forward in time and updates the position, velocity, 60 | * and acceleration. 61 | * @param {?number=} deltaTime An optional deltaTime to update with 62 | */ 63 | Entity3D.prototype.update = function(deltaTime) { 64 | var currentTime = (new Date()).getTime(); 65 | if (deltaTime) { 66 | this.deltaTime = deltaTime; 67 | } else if (this.lastUpdateTime === 0) { 68 | this.deltaTime = 0; 69 | } else { 70 | this.deltaTime = currentTime - this.lastUpdateTime; 71 | } 72 | for (var i = 0; i < DIMENSIONS; ++i) { 73 | this.position[i] += this.velocity[i] * this.deltaTime; 74 | this.velocity[i] += this.acceleration[i] * this.deltaTime; 75 | this.acceleration[i] = 0; 76 | } 77 | this.lastUpdateTime = currentTime; 78 | }; 79 | 80 | module.exports = Entity3D; 81 | -------------------------------------------------------------------------------- /public/js/game/Game.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview This is a class encapsulating the client side of the game, 3 | * which handles the rendering of the lobby and game and the sending of 4 | * user input to the server. 5 | * @author alvin.lin.dev@gmail.com (Alvin Lin) 6 | */ 7 | /** 8 | * Creates a Game on the client side to render the players and entities. 9 | * @constructor 10 | * @param {Object} socket The socket connected to the server. 11 | * @param {Drawing} drawing The Drawing object that will render the game. 12 | */ 13 | function Game(socket, drawing) { 14 | this.socket = socket; 15 | this.drawing = drawing; 16 | 17 | this.selfPlayer = null; 18 | this.otherPlayers = []; 19 | this.animationFrameId = 0; 20 | } 21 | 22 | /** 23 | * Factory method to create a Game object. 24 | * @param {Object} socket The Socket connected to the server. 25 | * @param {Element} canvasElement The canvas element that the game will use to 26 | * draw to. 27 | * @return {Game} 28 | */ 29 | Game.create = function(socket, canvasElement) { 30 | /** 31 | * Set the aspect ratio of the canvas. 32 | */ 33 | canvasElement.width = 800; 34 | canvasElement.height = 600; 35 | canvasElement.style.border = '1px solid black'; 36 | var canvasContext = canvasElement.getContext('2d'); 37 | 38 | var drawing = Drawing.create(canvasContext); 39 | return new Game(socket, drawing); 40 | }; 41 | 42 | /** 43 | * Initializes the Game object and its child objects as well as setting the 44 | * event handlers. 45 | */ 46 | Game.prototype.init = function() { 47 | var context = this; 48 | this.socket.on('update', function(data) { 49 | context.receiveGameState(data); 50 | }); 51 | this.socket.emit('player-join'); 52 | }; 53 | 54 | /** 55 | * This method begins the animation loop for the game. 56 | */ 57 | Game.prototype.animate = function() { 58 | this.animationFrameId = window.requestAnimationFrame( 59 | Util.bind(this, this.update)); 60 | }; 61 | 62 | /** 63 | * This method stops the animation loop for the game. 64 | */ 65 | Game.prototype.stopAnimation = function() { 66 | window.cancelAnimationFrame(this.animationFrameId); 67 | }; 68 | 69 | /** 70 | * Updates the game's internal storage of all the powerups, called each time 71 | * the server sends packets. 72 | * @param {Object} state The game state received from the server. 73 | */ 74 | Game.prototype.receiveGameState = function(state) { 75 | this.selfPlayer = state['self']; 76 | this.otherPlayers = state['players']; 77 | }; 78 | 79 | /** 80 | * Updates the state of the game client side and relays intents to the 81 | * server. 82 | */ 83 | Game.prototype.update = function() { 84 | if (this.selfPlayer) { 85 | // Emits an event for the containing the player's input. 86 | this.socket.emit('player-action', { 87 | keyboardState: { 88 | left: Input.LEFT, 89 | right: Input.RIGHT, 90 | up: Input.UP, 91 | down: Input.DOWN 92 | } 93 | }); 94 | this.draw(); 95 | } 96 | this.animate(); 97 | }; 98 | 99 | /** 100 | * Draws the state of the game using the internal Drawing object. 101 | */ 102 | Game.prototype.draw = function() { 103 | // Clear the canvas. 104 | this.drawing.clear(); 105 | 106 | // Draw yourself 107 | this.drawing.drawSelf( 108 | this.selfPlayer.x, 109 | this.selfPlayer.y, 110 | this.selfPlayer.hitbox 111 | ); 112 | 113 | // Draw the other players 114 | for (var player of this.otherPlayers) { 115 | this.drawing.drawOther( 116 | player.x, 117 | player.y, 118 | player.hitbox 119 | ); 120 | } 121 | }; 122 | -------------------------------------------------------------------------------- /public/js/game/Input.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This class facilitates the tracking of user input, such as mouse clicks 3 | * and button presses. 4 | * @author alvin.lin.dev@gmail.com (Alvin Lin) 5 | */ 6 | 7 | /** 8 | * Empty constructor for the Input object. 9 | */ 10 | function Input() { 11 | throw new Error('Input should not be instantiated!'); 12 | } 13 | 14 | /** @type {boolean} */ 15 | Input.LEFT_CLICK = false; 16 | /** @type {boolean} */ 17 | Input.RIGHT_CLICK = false; 18 | /** @type {Array} */ 19 | Input.MOUSE = [0, 0]; 20 | 21 | /** @type {boolean} */ 22 | Input.LEFT = false; 23 | /** @type {boolean} */ 24 | Input.UP = false; 25 | /** @type {boolean} */ 26 | Input.RIGHT = false; 27 | /** @type {boolean} */ 28 | Input.DOWN = false; 29 | /** @type {Object} */ 30 | Input.MISC_KEYS = {}; 31 | 32 | /** 33 | * This method is a callback bound to the onmousedown event 34 | * and updates the state of the mouse click stored in the Input class. 35 | * @param {Event} event The event passed to this function 36 | */ 37 | Input.onMouseDown = function(event) { 38 | if (event.which == 1) { 39 | Input.LEFT_CLICK = true; 40 | } else if (event.which == 3) { 41 | // This may fail depending on the browser as right click handling is 42 | // not universally supported. 43 | Input.RIGHT_CLICK = true; 44 | } 45 | }; 46 | 47 | /** 48 | * This method is a callback bound to the onmouseup event on and 49 | * updates the state of the mouse click stored in the Input class. 50 | * @param {Event} event The event passed to this function. 51 | */ 52 | Input.onMouseUp = function(event) { 53 | if (event.which == 1) { 54 | Input.LEFT_CLICK = false; 55 | } else if (event.which == 3) { 56 | // This may fail depending on the browser as right click handling is 57 | // not universally supported. 58 | Input.RIGHT_CLICK = false; 59 | } 60 | }; 61 | 62 | /** 63 | * This method is a callback bound to the onkeydown event on the document and 64 | * @param {Event} event The event passed to this function. 65 | * updates the state of the keys stored in the Input class. 66 | */ 67 | Input.onKeyDown = function(event) { 68 | switch (event.keyCode) { 69 | case 37: 70 | case 65: 71 | Input.LEFT = true; 72 | break; 73 | case 38: 74 | case 87: 75 | Input.UP = true; 76 | break; 77 | case 39: 78 | case 68: 79 | Input.RIGHT = true; 80 | break; 81 | case 40: 82 | case 83: 83 | Input.DOWN = true; 84 | break; 85 | default: 86 | Input.MISC_KEYS[event.keyCode] = true; 87 | break; 88 | } 89 | }; 90 | 91 | /** 92 | * This method is a callback bound to the onkeyup event on the document and 93 | * updates the state of the keys stored in the Input class. 94 | * @param {Event} event The event passed to this function. 95 | */ 96 | Input.onKeyUp = function(event) { 97 | switch (event.keyCode) { 98 | case 37: 99 | case 65: 100 | Input.LEFT = false; 101 | break; 102 | case 38: 103 | case 87: 104 | Input.UP = false; 105 | break; 106 | case 39: 107 | case 68: 108 | Input.RIGHT = false; 109 | break; 110 | case 40: 111 | case 83: 112 | Input.DOWN = false; 113 | break; 114 | default: 115 | Input.MISC_KEYS[event.keyCode] = false; 116 | } 117 | }; 118 | 119 | /** 120 | * This should be called during initialization to allow the Input 121 | * class to track user input. 122 | * @param {Element} element The element to apply the event listener to. 123 | */ 124 | Input.applyEventHandlers = function() { 125 | document.addEventListener('mousedown', Input.onMouseDown); 126 | document.addEventListener('mouseup', Input.onMouseUp); 127 | document.addEventListener('keyup', Input.onKeyUp); 128 | document.addEventListener('keydown', Input.onKeyDown); 129 | }; 130 | 131 | /** 132 | * This should be called any time an element needs to track mouse coordinates 133 | * over it. The event listener will be applied to the entire document, but the 134 | * the coordinates will be taken relative to the given element (using the given 135 | * element's top left as [0, 0]). 136 | * @param {Element} element The element to take the coordinates relative to. 137 | */ 138 | Input.addMouseTracker = function(element) { 139 | document.addEventListener('mousemove', (event) => { 140 | Input.MOUSE = [event.pageX - element.offsetLeft, 141 | event.pageY - element.offsetTop]; 142 | }); 143 | }; 144 | -------------------------------------------------------------------------------- /shared/Util.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview This is a utility class containing utility methods used on the 3 | * server and client. 4 | * @author alvin.lin.dev@gmail.com (Alvin Lin) 5 | */ 6 | 7 | /** 8 | * Empty constructor for the Util class, all functions will be static. 9 | */ 10 | function Util() { 11 | throw new Error('Util should not be instantiated!'); 12 | } 13 | 14 | /** 15 | * This function takes an object and defines getter and setter methods for 16 | * it. This is best demonstrated with an example: 17 | * 18 | * Util.splitProperties(object, ['x', 'y'], 'position') 19 | * will assign object.x as the getter/setter for object['position'][0] 20 | * and object.y as the getter/setter for object['position'][1] 21 | * 22 | * Util.splitProperties(object, ['vx', 'vy', 'vz'], 'velocity') 23 | * will assign object.vx as the getter/setter for object['velocity'][0], 24 | * object.vy as the getter/setter for object['velocity'][1], 25 | * and object.vz as the getter/setter for object['velocity'][2]. 26 | * @param {Object} object The object to apply the properties to 27 | * @param {Array} propertyNames The property names to apply 28 | * @param {string} propertyFrom The property to split 29 | */ 30 | Util.splitProperties = function(object, propertyNames, propertyFrom) { 31 | for (var i = 0; i < propertyNames.length; ++i) { 32 | (function(j) { 33 | Object.defineProperty(object, propertyNames[j], { 34 | enumerable: true, 35 | configurable: true, 36 | get: function() { 37 | return this[propertyFrom][j]; 38 | }, 39 | set: function(property) { 40 | this[propertyFrom][j] = property; 41 | } 42 | }); 43 | })(i); 44 | } 45 | }; 46 | 47 | /** 48 | * Allows for ES5 class inheritance by implementing functionality for a 49 | * child class to inherit from a parent class. 50 | * @param {Object} child The child object that inherits the parent 51 | * @param {Object} parent The parent object to inherit from 52 | */ 53 | Util.extend = function (child, parent) { 54 | child.prototype = Object.create(parent); 55 | child.prototype.parent = parent.prototype; 56 | }; 57 | 58 | /** 59 | * Binds a function to a context, useful for assigning event handlers and 60 | * function callbacks. 61 | * @param {Object} context The context to assign the method to. 62 | * @param {function(?)} method The method to bind the context to. 63 | * @return {function(?)} 64 | */ 65 | Util.bind = function(context, method) { 66 | return function() { 67 | return method.apply(context, arguments); 68 | } 69 | }; 70 | 71 | /** 72 | * This method returns the sign of a number. 73 | * @param {number} x The number to check. 74 | * @return {number} 75 | */ 76 | Util.getSign = function(x) { 77 | if (x > 0) { 78 | return 1; 79 | } else if (x < 0) { 80 | return -1; 81 | } 82 | return 0; 83 | }; 84 | 85 | /** 86 | * This method linearly scales a number from one range to another. 87 | * @param {number} x The number to scale. 88 | * @param {number} a1 The lower bound of the range to scale from. 89 | * @param {number} a2 The upper bound of the range to scale from. 90 | * @param {number} b1 The lower bound of the range to scale to. 91 | * @param {number} b2 The upper bound of the range to scale to. 92 | * @return {number} 93 | */ 94 | Util.linearScale = function(x, a1, a2, b1, b2) { 95 | return ((x - a1) * (b2 - b1) / (a2 - a1)) + b1; 96 | }; 97 | 98 | /** 99 | * Returns the sum of all the elements in an array. 100 | * @param {Array} array An array to sum. 101 | * @return {number} 102 | */ 103 | Util.sum = function(array) { 104 | return array.reduce((total, value) => total + value); 105 | } 106 | 107 | /** 108 | * Returns the Manhattan Distance between two points. 109 | * @param {Array} p1 The first point. 110 | * @param {Array} p2 The second point. 111 | * @return {number} 112 | */ 113 | Util.getManhattanDistance = function(p1, p2) { 114 | if (p1.length != p2.length) { 115 | throw new Error(`Cannot compute distance between ${p1} and ${p2}`); 116 | } 117 | return Util.sum(p1.map((value, index) => { 118 | return Math.abs(value - p2[index]); 119 | })); 120 | }; 121 | 122 | /** 123 | * Returns the squared Euclidean distance between two points. 124 | * @param {Array} p1 The first point. 125 | * @param {Array} p2 The second point. 126 | * @return {number} 127 | */ 128 | Util.getEuclideanDistance2 = function(p1, p2) { 129 | if (p1.length != p2.length) { 130 | throw new Error(`Cannot compute distance between ${p1} and ${p2}`); 131 | } 132 | return Util.sum(p1.map((value, index) => { 133 | return (value - p2[index]) * (value - p2[index]); 134 | })); 135 | }; 136 | 137 | /** 138 | * Returns the true Euclidean distance between two points. 139 | * @param {Array} p1 The first point. 140 | * @param {Array} p2 The second point. 141 | * @return {number} 142 | */ 143 | Util.getEuclideanDistance = function(p1, p2) { 144 | return Math.sqrt(Util.getEuclideanDistance2(p1, p2)); 145 | }; 146 | 147 | /** 148 | * Given a value, a minimum, and a maximum, returns true if value is 149 | * between the minimum and maximum, inclusive of both bounds. This 150 | * functio will still work if min and max are switched. 151 | * @param {number} val The value to check. 152 | * @param {number} min The minimum bound. 153 | * @param {number} max The maximum bound. 154 | * @return {boolean} 155 | */ 156 | Util.inBound = function(val, min, max) { 157 | if (min > max) { 158 | return val >= max && val <= min; 159 | } 160 | return val >= min && val <= max; 161 | }; 162 | 163 | /** 164 | * Bounds a number to the given minimum and maximum, inclusive of both 165 | * bounds. This function will still work if min and max are switched. 166 | * @param {number} val The value to check. 167 | * @param {number} min The minimum number to bound to. 168 | * @param {number} max The maximum number to bound to. 169 | * @return {number} 170 | */ 171 | Util.bound = function(val, min, max) { 172 | if (min > max) { 173 | return Math.min(Math.max(val, max), min); 174 | } 175 | return Math.min(Math.max(val, min), max); 176 | }; 177 | 178 | /** 179 | * Returns a random floating-point number between the given min and max 180 | * values, exclusive of the max value. 181 | * @param {number} min The minimum number to generate. 182 | * @param {number} max The maximum number to generate. 183 | * @return {number} 184 | */ 185 | Util.randRange = function(min, max) { 186 | if (min >= max) { 187 | var swap = min; 188 | min = max; 189 | max = swap; 190 | } 191 | return (Math.random() * (max - min)) + min; 192 | }; 193 | 194 | /** 195 | * Returns a random integer between the given min and max values, exclusive 196 | * of the max value. 197 | * @param {number} min The minimum number to generate. 198 | * @param {number} max The maximum number to generate. 199 | * @return {number} 200 | */ 201 | Util.randRangeInt = function(min, max) { 202 | if (min >= max) { 203 | var swap = min; 204 | min = max; 205 | max = swap; 206 | } 207 | return Math.floor(Math.random() * (max - min)) + min; 208 | }; 209 | 210 | /** 211 | * Returns a random element in a given array. 212 | * @param {Array<*>} array The array from which to select a random 213 | * element from. 214 | * @return {*} 215 | */ 216 | Util.choiceArray = function(array) { 217 | return array[Util.randRangeInt(0, array.length)]; 218 | }; 219 | 220 | if (typeof module === 'object') { 221 | module.exports = Util; 222 | } else { 223 | window.Util = Util; 224 | } 225 | --------------------------------------------------------------------------------