├── .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
--------------------------------------------------------------------------------