├── .gitattributes ├── .gitignore ├── Project ├── .firebaserc ├── database.rules.json ├── firebase.json └── public │ ├── css │ └── main.css │ ├── index.html │ └── js │ ├── game.js │ ├── lib │ ├── PlayerControls.js │ └── three.js │ ├── main.js │ └── player.js └── README.md /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | 7 | # Standard to msysgit 8 | *.doc diff=astextplain 9 | *.DOC diff=astextplain 10 | *.docx diff=astextplain 11 | *.DOCX diff=astextplain 12 | *.dot diff=astextplain 13 | *.DOT diff=astextplain 14 | *.pdf diff=astextplain 15 | *.PDF diff=astextplain 16 | *.rtf diff=astextplain 17 | *.RTF diff=astextplain 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Windows image file caches 2 | Thumbs.db 3 | ehthumbs.db 4 | 5 | # Folder config file 6 | Desktop.ini 7 | 8 | # Recycle Bin used on file shares 9 | $RECYCLE.BIN/ 10 | 11 | # Windows Installer files 12 | *.cab 13 | *.msi 14 | *.msm 15 | *.msp 16 | 17 | # Windows shortcuts 18 | *.lnk 19 | 20 | # ========================= 21 | # Operating System Files 22 | # ========================= 23 | 24 | # OSX 25 | # ========================= 26 | 27 | .DS_Store 28 | .AppleDouble 29 | .LSOverride 30 | 31 | # Thumbnails 32 | ._* 33 | 34 | # Files that might appear in the root of a volume 35 | .DocumentRevisions-V100 36 | .fseventsd 37 | .Spotlight-V100 38 | .TemporaryItems 39 | .Trashes 40 | .VolumeIcon.icns 41 | 42 | # Directories potentially created on remote AFP share 43 | .AppleDB 44 | .AppleDesktop 45 | Network Trash Folder 46 | Temporary Items 47 | .apdisk 48 | -------------------------------------------------------------------------------- /Project/.firebaserc: -------------------------------------------------------------------------------- 1 | { 2 | "projects": { 3 | "default": "mptemplate1" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /Project/database.rules.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": { 3 | "Players": { 4 | ".read": "auth != null", 5 | "$user_id": { 6 | ".write": "$user_id === auth.uid", 7 | "orientation": { 8 | "$orientation_type": { 9 | "$orientation_axis": { 10 | ".validate": "(!data.exists() || !newData.exists()) || (newData.isNumber() && (newData.val() - data.val() < 1) && (newData.val() - data.val() > -1))" 11 | } 12 | } 13 | } 14 | } 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /Project/firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "database": { 3 | "rules": "database.rules.json" 4 | }, 5 | "hosting": { 6 | "public": "public", 7 | "rewrites": [ 8 | { 9 | "source": "**", 10 | "destination": "/index.html" 11 | } 12 | ] 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Project/public/css/main.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0px; 3 | overflow: hidden; 4 | } -------------------------------------------------------------------------------- /Project/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 3D Multiplayer Game 5 | 6 | 7 | 8 | 9 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /Project/public/js/game.js: -------------------------------------------------------------------------------- 1 | var otherPlayers = {}; 2 | 3 | var playerID; 4 | var player; 5 | 6 | function loadGame() { 7 | // load the environment 8 | loadEnvironment(); 9 | // load the player 10 | initMainPlayer(); 11 | 12 | listenToOtherPlayers(); 13 | 14 | window.onunload = function() { 15 | fbRef.child( "Players/" + playerID ).remove(); 16 | }; 17 | 18 | window.onbeforeunload = function() { 19 | fbRef.child( "Players/" + playerID ).remove(); 20 | }; 21 | } 22 | 23 | function listenToPlayer( playerData ) { 24 | if ( playerData.val() ) { 25 | otherPlayers[playerData.key].setOrientation( playerData.val().orientation.position, playerData.val().orientation.rotation ); 26 | } 27 | } 28 | 29 | function listenToOtherPlayers() { 30 | // when a player is added, do something 31 | fbRef.child( "Players" ).on( "child_added", function( playerData ) { 32 | if ( playerData.val() ) { 33 | if ( playerID != playerData.key && !otherPlayers[playerData.key] ) { 34 | otherPlayers[playerData.key] = new Player( playerData.key ); 35 | otherPlayers[playerData.key].init(); 36 | fbRef.child( "Players/" + playerData.key ).on( "value", listenToPlayer ); 37 | } 38 | } 39 | }); 40 | 41 | // when a player is removed, do something 42 | 43 | fbRef.child( "Players" ).on( "child_removed", function( playerData ) { 44 | if ( playerData.val() ) { 45 | fbRef.child( "Players/" + playerData.key ).off( "value", listenToPlayer ); 46 | scene.remove( otherPlayers[playerData.key].mesh ); 47 | delete otherPlayers[playerData.key]; 48 | } 49 | }); 50 | } 51 | 52 | function initMainPlayer() { 53 | 54 | fbRef.child( "Players/" + playerID ).set({ 55 | isOnline: true, 56 | orientation: { 57 | position: {x: 0, y:0, z:0}, 58 | rotation: {x: 0, y:0, z:0} 59 | } 60 | }); 61 | 62 | player = new Player( playerID ); 63 | player.isMainPlayer = true; 64 | player.init(); 65 | } 66 | 67 | function loadEnvironment() { 68 | var sphere_geometry = new THREE.SphereGeometry( 1 ); 69 | var sphere_material = new THREE.MeshNormalMaterial(); 70 | var sphere = new THREE.Mesh( sphere_geometry, sphere_material ); 71 | 72 | scene.add( sphere ); 73 | } -------------------------------------------------------------------------------- /Project/public/js/lib/PlayerControls.js: -------------------------------------------------------------------------------- 1 | 2 | THREE.PlayerControls = function ( camera, player, domElement ) { 3 | 4 | this.camera = camera; 5 | this.player = player; 6 | this.domElement = ( domElement !== undefined ) ? domElement : document; 7 | 8 | // API 9 | 10 | this.enabled = true; 11 | 12 | this.center = new THREE.Vector3( player.position.x, player.position.y, player.position.z ); 13 | 14 | this.moveSpeed = 0.2; 15 | this.turnSpeed = 0.1; 16 | 17 | this.userZoom = true; 18 | this.userZoomSpeed = 1.0; 19 | 20 | this.userRotate = true; 21 | this.userRotateSpeed = 1.5; 22 | 23 | this.autoRotate = false; 24 | this.autoRotateSpeed = 0.1; 25 | this.YAutoRotation = false; 26 | 27 | this.minPolarAngle = 0; 28 | this.maxPolarAngle = Math.PI; 29 | 30 | this.minDistance = 0; 31 | this.maxDistance = Infinity; 32 | 33 | // internals 34 | 35 | var scope = this; 36 | 37 | var EPS = 0.000001; 38 | var PIXELS_PER_ROUND = 1800; 39 | 40 | var rotateStart = new THREE.Vector2(); 41 | var rotateEnd = new THREE.Vector2(); 42 | var rotateDelta = new THREE.Vector2(); 43 | 44 | var zoomStart = new THREE.Vector2(); 45 | var zoomEnd = new THREE.Vector2(); 46 | var zoomDelta = new THREE.Vector2(); 47 | 48 | var phiDelta = 0; 49 | var thetaDelta = 0; 50 | var scale = 1; 51 | 52 | var lastPosition = new THREE.Vector3( player.position.x, player.position.y, player.position.z ); 53 | var playerIsMoving = false; 54 | 55 | var keyState = {}; 56 | var STATE = { NONE: -1, ROTATE: 0, ZOOM: 1, PAN: 2 }; 57 | var state = STATE.NONE; 58 | 59 | // events 60 | 61 | var changeEvent = { type: 'change' }; 62 | 63 | this.rotateLeft = function ( angle ) { 64 | 65 | if ( angle === undefined ) { 66 | 67 | angle = getAutoRotationAngle(); 68 | 69 | } 70 | 71 | thetaDelta -= angle; 72 | 73 | }; 74 | 75 | this.rotateRight = function ( angle ) { 76 | 77 | if ( angle === undefined ) { 78 | 79 | angle = getAutoRotationAngle(); 80 | 81 | } 82 | 83 | thetaDelta += angle; 84 | 85 | }; 86 | 87 | this.rotateUp = function ( angle ) { 88 | 89 | if ( angle === undefined ) { 90 | 91 | angle = getAutoRotationAngle(); 92 | 93 | } 94 | 95 | phiDelta -= angle; 96 | 97 | }; 98 | 99 | this.rotateDown = function ( angle ) { 100 | 101 | if ( angle === undefined ) { 102 | 103 | angle = getAutoRotationAngle(); 104 | 105 | } 106 | 107 | phiDelta += angle; 108 | 109 | }; 110 | 111 | this.zoomIn = function ( zoomScale ) { 112 | 113 | if ( zoomScale === undefined ) { 114 | 115 | zoomScale = getZoomScale(); 116 | 117 | } 118 | 119 | scale /= zoomScale; 120 | 121 | }; 122 | 123 | this.zoomOut = function ( zoomScale ) { 124 | 125 | if ( zoomScale === undefined ) { 126 | 127 | zoomScale = getZoomScale(); 128 | 129 | } 130 | 131 | scale *= zoomScale; 132 | 133 | }; 134 | 135 | this.init = function() { 136 | 137 | this.camera.position.x = this.player.position.x + 2; 138 | this.camera.position.y = this.player.position.y + 2; 139 | this.camera.position.z = this.player.position.x + 2; 140 | 141 | this.camera.lookAt( this.player.position ); 142 | 143 | }; 144 | 145 | this.update = function() { 146 | 147 | this.checkKeyStates(); 148 | 149 | this.center = this.player.position; 150 | 151 | var position = this.camera.position; 152 | var offset = position.clone().sub( this.center ); 153 | 154 | // angle from z-axis around y-axis 155 | 156 | var theta = Math.atan2( offset.x, offset.z ); 157 | 158 | // angle from y-axis 159 | 160 | var phi = Math.atan2( Math.sqrt( offset.x * offset.x + offset.z * offset.z ), offset.y ); 161 | 162 | theta += thetaDelta; 163 | phi += phiDelta; 164 | 165 | // restrict phi to be between desired limits 166 | phi = Math.max( this.minPolarAngle, Math.min( this.maxPolarAngle, phi ) ); 167 | 168 | // restrict phi to be between EPS and PI-EPS 169 | phi = Math.max( EPS, Math.min( Math.PI - EPS, phi ) ); 170 | 171 | var radius = offset.length() * scale; 172 | 173 | radius = Math.max( this.minDistance, Math.min( this.maxDistance, radius ) ); 174 | 175 | offset.x = radius * Math.sin( phi ) * Math.sin( theta ); 176 | offset.y = radius * Math.cos( phi ); 177 | offset.z = radius * Math.sin( phi ) * Math.cos( theta ); 178 | 179 | if ( this.autoRotate ) { 180 | 181 | this.camera.position.x += this.autoRotateSpeed * ( ( this.player.position.x + 8 * Math.sin( this.player.rotation.y ) ) - this.camera.position.x ); 182 | this.camera.position.z += this.autoRotateSpeed * ( ( this.player.position.z + 8 * Math.cos( this.player.rotation.y ) ) - this.camera.position.z ); 183 | 184 | } else { 185 | 186 | position.copy( this.center ).add( offset ); 187 | 188 | } 189 | 190 | this.camera.lookAt( this.center ); 191 | 192 | thetaDelta = 0; 193 | phiDelta = 0; 194 | scale = 1; 195 | 196 | 197 | 198 | if ( state === STATE.NONE && playerIsMoving ) { 199 | 200 | this.autoRotate = true; 201 | 202 | } else { 203 | 204 | this.autoRotate = false; 205 | 206 | } 207 | 208 | if ( lastPosition.distanceTo( this.player.position) > 0 ) { 209 | 210 | 211 | lastPosition.copy( this.player.position ); 212 | 213 | } else if ( lastPosition.distanceTo( this.player.position) == 0 ) { 214 | 215 | playerIsMoving = false; 216 | 217 | } 218 | 219 | }; 220 | 221 | this.checkKeyStates = function () { 222 | 223 | if (keyState[38] || keyState[87]) { 224 | 225 | // up arrow or 'w' - move forward 226 | 227 | this.player.position.x -= this.moveSpeed * Math.sin( this.player.rotation.y ); 228 | this.player.position.z -= this.moveSpeed * Math.cos( this.player.rotation.y ); 229 | 230 | this.camera.position.x -= this.moveSpeed * Math.sin( this.player.rotation.y ); 231 | this.camera.position.z -= this.moveSpeed * Math.cos( this.player.rotation.y ); 232 | 233 | } 234 | 235 | if (keyState[40] || keyState[83]) { 236 | 237 | // down arrow or 's' - move backward 238 | playerIsMoving = true; 239 | 240 | this.player.position.x += this.moveSpeed * Math.sin( this.player.rotation.y ); 241 | this.player.position.z += this.moveSpeed * Math.cos( this.player.rotation.y ); 242 | 243 | this.camera.position.x += this.moveSpeed * Math.sin( this.player.rotation.y ); 244 | this.camera.position.z += this.moveSpeed * Math.cos( this.player.rotation.y ); 245 | 246 | } 247 | 248 | if (keyState[37] || keyState[65]) { 249 | 250 | // left arrow or 'a' - rotate left 251 | playerIsMoving = true; 252 | 253 | this.player.rotation.y += this.turnSpeed; 254 | 255 | } 256 | 257 | if (keyState[39] || keyState[68]) { 258 | 259 | // right arrow or 'd' - rotate right 260 | playerIsMoving = true; 261 | 262 | this.player.rotation.y -= this.turnSpeed; 263 | 264 | } 265 | if ( keyState[81] ) { 266 | 267 | // 'q' - strafe left 268 | playerIsMoving = true; 269 | 270 | this.player.position.x -= this.moveSpeed * Math.cos( this.player.rotation.y ); 271 | this.player.position.z += this.moveSpeed * Math.sin( this.player.rotation.y ); 272 | 273 | this.camera.position.x -= this.moveSpeed * Math.cos( this.player.rotation.y ); 274 | this.camera.position.z += this.moveSpeed * Math.sin( this.player.rotation.y ); 275 | 276 | } 277 | 278 | if ( keyState[69] ) { 279 | 280 | // 'e' - strage right 281 | playerIsMoving = true; 282 | 283 | this.player.position.x += this.moveSpeed * Math.cos( this.player.rotation.y ); 284 | this.player.position.z -= this.moveSpeed * Math.sin( this.player.rotation.y ); 285 | 286 | this.camera.position.x += this.moveSpeed * Math.cos( this.player.rotation.y ); 287 | this.camera.position.z -= this.moveSpeed * Math.sin( this.player.rotation.y ); 288 | 289 | } 290 | 291 | fbRef.child( "Players/" + playerID + "/orientation" ).update({ 292 | position: { 293 | x: this.player.position.x, 294 | y: this.player.position.y, 295 | z: this.player.position.z 296 | }, 297 | rotation: { 298 | x: this.player.rotation.x, 299 | y: this.player.rotation.y, 300 | z: this.player.rotation.z 301 | } 302 | }); 303 | }; 304 | 305 | function getAutoRotationAngle() { 306 | 307 | return 2 * Math.PI / 60 / 60 * scope.autoRotateSpeed; 308 | 309 | } 310 | 311 | function getZoomScale() { 312 | 313 | return Math.pow( 0.95, scope.userZoomSpeed ); 314 | 315 | } 316 | 317 | function onMouseDown( event ) { 318 | 319 | if ( scope.enabled === false ) return; 320 | if ( scope.userRotate === false ) return; 321 | 322 | event.preventDefault(); 323 | 324 | if ( event.button === 0 ) { 325 | 326 | state = STATE.ROTATE; 327 | 328 | rotateStart.set( event.clientX, event.clientY ); 329 | 330 | } else if ( event.button === 1 ) { 331 | 332 | state = STATE.ZOOM; 333 | 334 | zoomStart.set( event.clientX, event.clientY ); 335 | 336 | } 337 | 338 | document.addEventListener( 'mousemove', onMouseMove, false ); 339 | document.addEventListener( 'mouseup', onMouseUp, false ); 340 | 341 | } 342 | 343 | function onMouseMove( event ) { 344 | 345 | if ( scope.enabled === false ) return; 346 | 347 | event.preventDefault(); 348 | 349 | if ( state === STATE.ROTATE ) { 350 | 351 | rotateEnd.set( event.clientX, event.clientY ); 352 | rotateDelta.subVectors( rotateEnd, rotateStart ); 353 | 354 | scope.rotateLeft( 2 * Math.PI * rotateDelta.x / PIXELS_PER_ROUND * scope.userRotateSpeed ); 355 | scope.rotateUp( 2 * Math.PI * rotateDelta.y / PIXELS_PER_ROUND * scope.userRotateSpeed ); 356 | 357 | rotateStart.copy( rotateEnd ); 358 | 359 | } else if ( state === STATE.ZOOM ) { 360 | 361 | zoomEnd.set( event.clientX, event.clientY ); 362 | zoomDelta.subVectors( zoomEnd, zoomStart ); 363 | 364 | if ( zoomDelta.y > 0 ) { 365 | 366 | scope.zoomIn(); 367 | 368 | } else { 369 | 370 | scope.zoomOut(); 371 | 372 | } 373 | 374 | zoomStart.copy( zoomEnd ); 375 | } 376 | 377 | } 378 | 379 | function onMouseUp( event ) { 380 | 381 | if ( scope.enabled === false ) return; 382 | if ( scope.userRotate === false ) return; 383 | 384 | document.removeEventListener('mousemove', onMouseMove, false ); 385 | document.removeEventListener( 'mouseup', onMouseUp, false ); 386 | 387 | state = STATE.NONE; 388 | 389 | } 390 | 391 | function onMouseWheel( event ) { 392 | 393 | if ( scope.enabled === false ) return; 394 | if ( scope.userRotate === false ) return; 395 | 396 | var delta = 0; 397 | 398 | if ( event.wheelDelta ) { //WebKit / Opera / Explorer 9 399 | 400 | delta = event.wheelDelta; 401 | 402 | } else if ( event.detail ) { // Firefox 403 | 404 | delta = - event.detail; 405 | 406 | } 407 | 408 | if ( delta > 0 ) { 409 | 410 | scope.zoomOut(); 411 | 412 | } else { 413 | 414 | scope.zoomIn(); 415 | 416 | } 417 | 418 | } 419 | 420 | function onKeyDown( event ) { 421 | 422 | event = event || window.event; 423 | 424 | keyState[event.keyCode || event.which] = true; 425 | 426 | } 427 | 428 | function onKeyUp( event ) { 429 | 430 | event = event || window.event; 431 | 432 | keyState[event.keyCode || event.which] = false; 433 | 434 | } 435 | 436 | this.domElement.addEventListener('contextmenu', function( event ) { event.preventDefault(); }, false ); 437 | this.domElement.addEventListener('mousedown', onMouseDown, false ); 438 | this.domElement.addEventListener('mousewheel', onMouseWheel, false ); 439 | this.domElement.addEventListener('DOMMouseScroll', onMouseWheel, false ); // firefox 440 | this.domElement.addEventListener('keydown', onKeyDown, false ); 441 | this.domElement.addEventListener('keyup', onKeyUp, false ); 442 | 443 | }; 444 | 445 | THREE.PlayerControls.prototype = Object.create( THREE.EventDispatcher.prototype ); -------------------------------------------------------------------------------- /Project/public/js/main.js: -------------------------------------------------------------------------------- 1 | var container, scene, camera, renderer; 2 | 3 | var controls; 4 | 5 | init(); 6 | animate(); 7 | 8 | function init() { 9 | // Setup 10 | container = document.getElementById( 'container' ); 11 | 12 | scene = new THREE.Scene(); 13 | 14 | camera = new THREE.PerspectiveCamera( 50, window.innerWidth / window.innerHeight, 1, 1000 ); 15 | camera.position.z = 5; 16 | 17 | renderer = new THREE.WebGLRenderer( { alpha: true} ); 18 | renderer.setSize( window.innerWidth, window.innerHeight); 19 | 20 | 21 | // Load game world 22 | 23 | firebase.auth().onAuthStateChanged(function( user ) { 24 | if ( user ) { 25 | // User is signed in 26 | 27 | console.log( "Player is signed in " ); 28 | playerID = user.uid; 29 | 30 | fbRef.child( "Players/" + playerID + "/isOnline" ).once( "value" ).then( function( isOnline ) { 31 | 32 | if ( isOnline.val() === null || isOnline.val() === false ) { 33 | loadGame(); 34 | } else { 35 | alert( "Hey, only one session at a time buddy!" ); 36 | } 37 | }); 38 | 39 | 40 | } else { 41 | // User is signed out 42 | console.log( "Player is signed out " ); 43 | 44 | firebase.auth().signInAnonymously().catch(function(error) { 45 | console.log( error.code + ": " + error.message ); 46 | }) 47 | } 48 | }); 49 | 50 | 51 | // Events 52 | window.addEventListener( "resize", onWindowResize, false ); 53 | 54 | container.appendChild( renderer.domElement ); 55 | document.body.appendChild( container ); 56 | } 57 | 58 | function animate() { 59 | requestAnimationFrame( animate ); 60 | 61 | if ( controls ) { 62 | controls.update(); 63 | } 64 | 65 | render(); 66 | } 67 | 68 | function render() { 69 | 70 | renderer.clear(); 71 | renderer.render( scene, camera ); 72 | } 73 | 74 | function onWindowResize() { 75 | camera.aspect = window.innerWidth / window.innerHeight; 76 | camera.updateProjectionMatrix(); 77 | 78 | renderer.setSize( window.innerWidth, window.innerHeight ); 79 | } -------------------------------------------------------------------------------- /Project/public/js/player.js: -------------------------------------------------------------------------------- 1 | var Player = function( playerID ) { 2 | this.playerID = playerID; 3 | this.isMainPlayer = false; 4 | this.mesh; 5 | 6 | var cube_geometry = new THREE.BoxGeometry( 1, 1, 1 ); 7 | var cube_material = new THREE.MeshBasicMaterial( {color: 0x7777ff, wireframe: false} ); 8 | 9 | var scope = this; 10 | 11 | this.init = function() { 12 | scope.mesh = new THREE.Mesh( cube_geometry, cube_material ); 13 | scene.add( scope.mesh ); 14 | 15 | if ( scope.isMainPlayer ) { 16 | // Give player control of this mesh 17 | controls = new THREE.PlayerControls( camera , scope.mesh ); 18 | controls.init(); 19 | } 20 | }; 21 | 22 | this.setOrientation = function( position, rotation ) { 23 | if ( scope.mesh ) { 24 | scope.mesh.position.copy( position ); 25 | scope.mesh.rotation.x = rotation.x; 26 | scope.mesh.rotation.y = rotation.y; 27 | scope.mesh.rotation.z = rotation.z; 28 | 29 | } 30 | }; 31 | }; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Firebase and Three.js multiplayer game template 2 | 3 | ###Demo: https://mptemplate1.firebaseapp.com 4 | 5 | ###Tutorial: https://youtube.com/PiusNyakoojo --------------------------------------------------------------------------------