ShadowSpace un MMORPG en WebGL

ShadowSpace est un projet perso que je développe quand j'ai un peu de temps libre devant moi. Le but de ce projet est de créer un jeu spacial du type MMORPG/FPS
j'ai débuté ce projet avec un example disponible dans la librairie http://threejs.org/ , ensuite j'ai ajouté quelques éléments pour comprendre le mechanisme de la librairie et pour finir ma base, j'ai dev une librairie ajax en javascript , une librairie xml en php et créé un système de session multijoueur avec le tout.
Pour commencer je vous balance quelques captures d'écran:
J'ai inclus à la scène une sélection des objets avec la souris avec une aura jaune. Le point rouge est un autre joueur (en l'occurence c'est moi avec le navigateur Firefox). En cliquant dessus il affiche les informations relative au joueur en bas, il en va de même pour les autres objet (comme les planètes par exemple) de la scène.
page index.php:
<!DOCTYPE html> <html lang="fr"> <head> <title>Space Shadow</title> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0"> <style> body { background:#000; color: #eee; padding:0; margin:0; font-weight:bold; overflow:hidden; font-family:Monospace; font-size:13px; text-align:center; } #msg { position: absolute; bottom: 0px; width: 100%; padding: 5px; z-index:10000; } #result { position: absolute; top: 10px; left: 10px; width: 100%; padding: 5px; font-size:9px; z-index:10000; } #cockpit { position:absolute; top:0px;left:0px; width: 100%; height:100%; z-index:1000; /*background-image:url(textures/cockpits/view.png); background-repeat: no-repeat; background-attachment: fixed; */ background-size: 100% 100%; } a { color: #0080ff; } b { color:orange } </style> <script src="build/three.min.js"></script> <script src="js/controls/FlyControls.js"></script> <script src="js/shaders/CopyShader.js"></script> <script src="js/shaders/FilmShader.js"></script> <script src="js/postprocessing/EffectComposer.js"></script> <script src="js/postprocessing/ShaderPass.js"></script> <script src="js/postprocessing/MaskPass.js"></script> <script src="js/postprocessing/RenderPass.js"></script> <script src="js/postprocessing/FilmPass.js"></script> <script src="libajax.js"></script> <script src="js/Detector.js"></script> <!-- <script src="js/libs/stats.min.js"></script>--> </head> <body> <div id="msg"></div> <div id="result"></div> <div id="cockpit"></div> <!--<b>WASD</b> move, <b>R|F</b> up | down, <b>Q|E</b> roll, <b>up|down</b> pitch, <b>left|right</b> yaw<br/>--> <script> if ( ! Detector.webgl ) Detector.addGetWebGLMessage(); var projector, lightsolar, particule1,l1; var objects = []; var mouse = { x: 0, y: 0 }, INTERSECTED; var radius = 6371; var tilt = 0.41; var rotationSpeed = 0.02; var cloudsScale = 1.005; var moonScale = 0.23; var playerScale = 0.23; var marsScale = 10.00; var plutonScale = 100.00; var solarScale = 150.00; var targeting=false; var MARGIN = 0; var SCREEN_HEIGHT = window.innerHeight - MARGIN * 2; var SCREEN_WIDTH = window.innerWidth; var container, stats; var camera, controls, scene, sceneCube, renderer; var geometry, meshPlanet, meshClouds, meshMoon, meshMars, meshPluton,meshPlayer, meshSolar; var materialPlayer = new THREE.ParticleBasicMaterial( { color: 0xff0000, opacity:0.3, transparent: true } ); var materialPlayer_head = new THREE.ParticleBasicMaterial( { color: 0x00ff00, opacity:0.3, transparent: true } ); var meshPlayers = new Array(); var dirLight, pointLight, ambientLight; var d, dPlanet, dMoon, dMoonVec = new THREE.Vector3(); var dMars, dMarsVec = new THREE.Vector3(); var dPluton, dPlutonVec = new THREE.Vector3(); var dSolar, dSolarVec = new THREE.Vector3(); var clock = new THREE.Clock(); init(); animate(); function init() { container = document.createElement( 'div' ); document.body.appendChild( container ); camera = new THREE.PerspectiveCamera( 50, SCREEN_WIDTH / SCREEN_HEIGHT, 50, 1e7 ); camera.position.z = radius * 3; scene = new THREE.Scene(); scene.fog = new THREE.FogExp2( 0x000000, 0.00000025 ); controls = new THREE.FlyControls( camera ); controls.movementSpeed = 1; controls.domElement = container; controls.rollSpeed = Math.PI / 14; controls.autoForward = false; controls.dragToLook = true; zoneSelected = new THREE.Mesh( new THREE.SphereGeometry( 7000, 200, 200 ), new THREE.ParticleBasicMaterial( { color: 0xffff00, opacity:0.1, transparent:true } ) ); zoneSelected.name="select"; scene.add( zoneSelected ); ambientLight = new THREE.AmbientLight( 0x000000 ); scene.add( ambientLight ); var planetTexture = THREE.ImageUtils.loadTexture( "textures/planets/earth_atmos_2048.jpg" ); var cloudsTexture = THREE.ImageUtils.loadTexture( "textures/planets/earth_clouds_1024.png" ); var normalTexture = THREE.ImageUtils.loadTexture( "textures/planets/earth_normal_2048.jpg" ); var specularTexture = THREE.ImageUtils.loadTexture( "textures/planets/earth_specular_2048.jpg" ); var solarTexture = THREE.ImageUtils.loadTexture( "textures/planets/solar.jpg" ); var moonTexture = THREE.ImageUtils.loadTexture( "textures/planets/moon_1024.jpg" ); var marsTexture = THREE.ImageUtils.loadTexture( "textures/planets/mars.jpg" ); var plutonTexture = THREE.ImageUtils.loadTexture( "textures/planets/firya.jpg" ); var shader = THREE.ShaderLib[ "normalmap" ]; var uniforms = THREE.UniformsUtils.clone( shader.uniforms ); uniforms[ "tNormal" ].value = normalTexture; uniforms[ "uNormalScale" ].value.set( 0.85, 0.85 ); uniforms[ "tDiffuse" ].value = planetTexture; uniforms[ "tSpecular" ].value = specularTexture; uniforms[ "enableAO" ].value = false; uniforms[ "enableDiffuse" ].value = true; uniforms[ "enableSpecular" ].value = true; uniforms[ "uDiffuseColor" ].value.setHex( 0xffffff ); uniforms[ "uSpecularColor" ].value.setHex( 0x999999 ); uniforms[ "uAmbientColor" ].value.setHex( 0x999999 ); uniforms[ "uShininess" ].value = 10; var parameters = { fragmentShader: shader.fragmentShader, vertexShader: shader.vertexShader, uniforms: uniforms, lights: true, fog: true }; // planet var materialNormalMap = new THREE.ShaderMaterial( parameters ); geometry = new THREE.SphereGeometry( radius, 100, 50 ); geometry.computeTangents(); meshPlanet = new THREE.Mesh( geometry, materialNormalMap ); meshPlanet.rotation.y = 0; meshPlanet.rotation.z = tilt; meshPlanet.name="lifewater"; scene.add( meshPlanet ); // clouds var materialClouds = new THREE.MeshLambertMaterial( { color: 0xffffff, map: cloudsTexture, transparent: true } ); meshClouds = new THREE.Mesh( geometry, materialClouds ); meshClouds.scale.set( cloudsScale, cloudsScale, cloudsScale ); meshClouds.rotation.z = tilt; meshClouds.name="lifewater"; scene.add( meshClouds ); // moon var materialMoon = new THREE.MeshPhongMaterial( { color: 0xffffff, map: moonTexture } ); meshMoon = new THREE.Mesh( geometry, materialMoon ); meshMoon.position.set( radius * 5, 0, 0 ); meshMoon.scale.set( moonScale, moonScale, moonScale ); meshMoon.name="sat1"; scene.add( meshMoon ); //soleil lightsolar = new THREE.PointLight( 0xffffff); lightsolar.position.set( -6000000 , -50000, 500000 ); lightsolar.intensity = 1; scene.add( lightsolar ); var materialSolar = new THREE.ParticleBasicMaterial( { color: 0xffff00, opacity:0.7, transparent:true } ); meshSolar = new THREE.Mesh( geometry, materialSolar ); meshSolar.position.x = lightsolar.position.x; meshSolar.position.y = lightsolar.position.y; meshSolar.position.z = lightsolar.position.z; meshSolar.scale.set( solarScale, solarScale, solarScale ); meshSolar.name="solar9554"; scene.add( meshSolar ); // mars var materialMars = new THREE.MeshPhongMaterial( { color: 0xffffff, map: marsTexture } ); meshMars = new THREE.Mesh( geometry, materialMars ); meshMars.position.set( radius * 100, 50, 45 ); meshMars.scale.set( marsScale, marsScale, marsScale ); meshMars.name="m44"; scene.add( meshMars ); // pluton var materialPluton = new THREE.MeshPhongMaterial( { color: 0xffffff, map: plutonTexture } ); meshPluton = new THREE.Mesh( geometry, materialPluton ); meshPluton.position.set( radius * 300, -100, 90 ); meshPluton.scale.set( plutonScale, plutonScale, plutonScale ); meshPluton.name="firya"; scene.add( meshPluton ); //player /* meshPlayer = new THREE.Mesh( geometry, materialPlayer ); meshPlayer.position.x = camera.position.x; meshPlayer.position.y = camera.position.y; meshPlayer.position.z = camera.position.z; meshPlayer.scale.set( 0.10, 0.10, 0.10 ); meshPlayer.name=""; scene.add( meshPlayer ); */ // stars var i, r = radius, starsGeometry = [ new THREE.Geometry(), new THREE.Geometry() ]; for ( i = 0; i < 250; i ++ ) { var vertex = new THREE.Vector3(); vertex.x = Math.random() * 2 - 1; vertex.y = Math.random() * 2 - 1; vertex.z = Math.random() * 2 - 1; vertex.multiplyScalar( r ); starsGeometry[ 0 ].vertices.push( vertex ); } for ( i = 0; i < 1500; i ++ ) { var vertex = new THREE.Vector3(); vertex.x = Math.random() * 2 - 1; vertex.y = Math.random() * 2 - 1; vertex.z = Math.random() * 2 - 1; vertex.multiplyScalar( r ); starsGeometry[ 1 ].vertices.push( vertex ); } var stars; var starsMaterials = [ new THREE.ParticleBasicMaterial( { color: 0x555555, size: 2, sizeAttenuation: false } ), new THREE.ParticleBasicMaterial( { color: 0x555555, size: 1, sizeAttenuation: false } ), new THREE.ParticleBasicMaterial( { color: 0x333333, size: 2, sizeAttenuation: false } ), new THREE.ParticleBasicMaterial( { color: 0x3a3a3a, size: 1, sizeAttenuation: false } ), new THREE.ParticleBasicMaterial( { color: 0x1a1a1a, size: 2, sizeAttenuation: false } ), new THREE.ParticleBasicMaterial( { color: 0x1a1a1a, size: 1, sizeAttenuation: false } ) ]; for ( i = 10; i < 40; i ++ ) { stars = new THREE.ParticleSystem( starsGeometry[ i % 2 ], starsMaterials[ i % 6 ] ); stars.rotation.x = Math.random() * 12; stars.rotation.y = Math.random() * 12; stars.rotation.z = Math.random() * 12; s = i * 7; stars.scale.set( s, s, s ); stars.matrixAutoUpdate = false; stars.updateMatrix(); scene.add( stars ); } projector = new THREE.Projector(); renderer = new THREE.WebGLRenderer( { clearColor: 0x000000, clearAlpha: 1 } ); renderer.setSize( SCREEN_WIDTH, SCREEN_HEIGHT ); renderer.sortObjects = false; renderer.autoClear = false; container.appendChild( renderer.domElement ); /*stats = new Stats(); stats.domElement.style.position = 'absolute'; stats.domElement.style.top = '0px'; stats.domElement.style.zIndex = 100; container.appendChild( stats.domElement );*/ document.addEventListener( 'mouseup', onDocumentMouseLeftClick, false ); //document.addEventListener( 'dblclick', onDocumentMouseDoubleClick, false ); window.addEventListener( 'resize', onWindowResize, false ); // postprocessing var renderModel = new THREE.RenderPass( scene, camera ); var effectFilm = new THREE.FilmPass( 0.0, 0.0, 2048, false ); effectFilm.renderToScreen = true; composer = new THREE.EffectComposer( renderer ); composer.addPass( renderModel ); composer.addPass( effectFilm ); }; function onDocumentMouseLeftClick( event ) { event.preventDefault(); mouse.x = ( event.clientX / window.innerWidth ) * 2 - 1; mouse.y = - ( event.clientY / window.innerHeight ) * 2 + 1; } var DBLC=false; function onDocumentMouseDoubleClick( event ) { DBLC=true; } function onWindowResize( event ) { SCREEN_HEIGHT = window.innerHeight; SCREEN_WIDTH = window.innerWidth; renderer.setSize( SCREEN_WIDTH, SCREEN_HEIGHT ); camera.aspect = SCREEN_WIDTH / SCREEN_HEIGHT; camera.updateProjectionMatrix(); composer.reset(); }; function animate() { requestAnimationFrame( animate ); render(); //stats.update(); }; function sleep(milliseconds) { var start = new Date().getTime(); for (var i = 0; i < 1e7; i++) { if ((new Date().getTime() - start) > milliseconds){ break; } } } function _SESSION() { sendWithAjaxE4( 'get_session_xml.php', 'POST', 'eval(xh.responseText);', null, 'posx='+camera.position.x+'&posy='+camera.position.y+'&posz='+camera.position.z+'&pseudo='+pseudo ); } //_SESSION(); var pseudo=prompt('Tapez votre pseudo','null'); setInterval(_SESSION,1000); function render() { // rotate the planet and clouds var time = Date.now() * 0.0005; var delta = clock.getDelta(); meshPlanet.rotation.y += rotationSpeed * delta; meshClouds.rotation.y += 1.25 * rotationSpeed * delta; // slow down as we approach the surface dPlanet = camera.position.length(); dMoonVec.subVectors( camera.position, meshMoon.position ); dMoon = dMoonVec.length(); dMarsVec.subVectors( camera.position, meshMars.position ); dMars = dMarsVec.length(); dPlutonVec.subVectors( camera.position, meshPluton.position ); dPluton = dPlutonVec.length(); if ( dMoon < dPlanet ) { d = ( dMoon - radius * moonScale * 1.01 ); } if ( dMars < dPlanet ) { d = ( dMars - radius * marsScale * 1.01 ); } if ( dPluton < dPlanet ) { d = ( dPluton - radius * plutonScale * 1.01 ); } if (( dPluton > dPlanet ) &&( dMars > dPlanet ) && ( dMoon > dPlanet ) ) { d = ( dPlanet - radius * 1.01 ); } // find intersections var vector = new THREE.Vector3( mouse.x, mouse.y, 1 ); projector.unprojectVector( vector, camera ); var raycaster = new THREE.Raycaster( camera.position, vector.sub( camera.position ).normalize() ); var intersects = raycaster.intersectObjects( scene.children ); if ( intersects.length > 0 ) { if ( INTERSECTED != intersects[ 0 ].object ) { INTERSECTED = intersects[ 0 ].object; if (INTERSECTED.name!='select') { if (INTERSECTED.name!='solar9554') { zoneSelected.position.set(INTERSECTED.position.x,INTERSECTED.position.y,INTERSECTED.position.z); zoneSelected.scale.x = INTERSECTED.scale.x; zoneSelected.scale.y = INTERSECTED.scale.y; zoneSelected.scale.z = INTERSECTED.scale.z; } document.getElementById('msg').innerHTML=INTERSECTED.name+'<br> position x:'+INTERSECTED.position.x+' y:'+INTERSECTED.position.y+' z:'+INTERSECTED.position.z+'<br>dimension x:'+INTERSECTED.scale.x+' y:'+INTERSECTED.scale.y; } } } else { INTERSECTED = null; zoneSelected.position.set(0,0,0); zoneSelected.scale.x =0; zoneSelected.scale.y = 0; zoneSelected.scale.z = 0; } controls.movementSpeed = 0.33 * d; controls.update( delta ); //renderer.render( scene, camera ); renderer.clear(); composer.render( delta ); }; </script> </body> </html>
le script envoie une requête en Ajax au script get_session_xml.php:
<?php ini_set('display_errors', 1); error_reporting(E_ALL); require("libxml.php"); $fp = fopen("session.xml", "rw+"); while (!flock($fp, LOCK_EX)) { //echo "Impossible de verrouiller le fichier!"; } $dom=loadFileXML("session.xml"); $out=getAttribute($dom,"connect", "id"); $record=false; for ($i=0; $i<count($out); $i++) { if ($out[$i]==sessionXML()) { $record=true; break; } } if ($record) { //mise à jour des connections updateTagWithAttribute($dom, "connect", $_SERVER['REQUEST_TIME'], "id", $out[$i]); updateTagWithAttribute($dom, "posx", $_POST['posx'], "id", $out[$i]); updateTagWithAttribute($dom, "posy", $_POST['posy'], "id", $out[$i]); updateTagWithAttribute($dom, "posz", $_POST['posz'], "id", $out[$i]); $tim=getTagWithoutAttribute($dom,"connect", "id", $out[$i]); $attr=getAttributeWithout($dom,"connect", "id", $out[$i]); $posx=getTagWithoutAttribute($dom,"posx", "id", $out[$i]); $posy=getTagWithoutAttribute($dom,"posy", "id", $out[$i]); $posz=getTagWithoutAttribute($dom,"posz", "id", $out[$i]); $pseudo=getTagWithoutAttribute($dom,"pseudo", "id", $out[$i]); for($e=0; $e<count($tim); $e++) { if ($tim[$e]+10 < $_SERVER['REQUEST_TIME']) { echo "scene.remove(meshPlayers[\"".$attr[$e]."\"]);"; delTagWithAttribute($dom, "connect", "id", $attr[$e]); delTagWithAttribute($dom, "ua", "id", $attr[$e]); delTagWithAttribute($dom, "ip", "id", $attr[$e]); delTagWithAttribute($dom, "posx", "id", $attr[$e]); delTagWithAttribute($dom, "posy", "id", $attr[$e]); delTagWithAttribute($dom, "posz", "id", $attr[$e]); delTagWithAttribute($dom, "pseudo", "id", $attr[$e]); } else { echo "scene.remove(meshPlayers[\"".$attr[$e]."\"]);"; echo "meshPlayers[\"".$attr[$e]."\"]=new THREE.Mesh( geometry, materialPlayer );"; echo "meshPlayers[\"".$attr[$e]."\"].name=\"".$pseudo[$e]."\";"; echo "meshPlayers[\"".$attr[$e]."\"].position.x = ".$posx[$e].";"; echo "meshPlayers[\"".$attr[$e]."\"].position.y = ".$posy[$e].";"; echo "meshPlayers[\"".$attr[$e]."\"].position.z = ".$posz[$e].";"; echo "meshPlayers[\"".$attr[$e]."\"].scale.set( 0.1, 0.1, 0.1 );"; echo "scene.add( meshPlayers[\"".$attr[$e]."\"] );"; echo "scene.remove(meshPlayers[\"".$attr[$e]."_head\"]);"; echo "meshPlayers[\"".$attr[$e]."_head\"]=new THREE.Mesh( geometry, materialPlayer_head );"; echo "meshPlayers[\"".$attr[$e]."_head\"].name=\"".$pseudo[$e]."\";"; echo "meshPlayers[\"".$attr[$e]."_head\"].position.x = ".($posx[$e]).";"; echo "meshPlayers[\"".$attr[$e]."_head\"].position.y = ".($posy[$e]).";"; echo "meshPlayers[\"".$attr[$e]."_head\"].position.z = ".($posz[$e]).";"; echo "meshPlayers[\"".$attr[$e]."_head\"].scale.set( 0.025, 0.025, 0.025 );"; echo "scene.add( meshPlayers[\"".$attr[$e]."_head\"] );"; } } } else { updateTagWithAttribute($dom, "connect", $_SERVER['REQUEST_TIME'], "id", sessionXML()); updateTagWithAttribute($dom, "ua", $_SERVER['HTTP_USER_AGENT'], "id", sessionXML()); updateTagWithAttribute($dom, "ip", $_SERVER['SERVER_ADDR'], "id", sessionXML()); updateTagWithAttribute($dom, "posx", $_POST['posx'], "id", sessionXML()); updateTagWithAttribute($dom, "posy", $_POST['posy'], "id", sessionXML()); updateTagWithAttribute($dom, "posz", $_POST['posz'], "id", sessionXML()); $pseudo=$_POST['pseudo']; if ($pseudo == 'null') { $pseudo=sessionXML();} updateTagWithAttribute($dom, "pseudo", $pseudo, "id", sessionXML()); //mise à jour des connections $tim=getTagWithoutAttribute($dom,"connect", "id", sessionXML()); $attr=getAttributeWithout($dom,"connect", "id", sessionXML()); for($e=0; $e<count($tim); $e++) { if ($tim[$e]+10 < $_SERVER['REQUEST_TIME']) { echo "scene.remove(meshPlayers[\"".$attr[$e]."\"]);"; delTagWithAttribute($dom, "connect", "id", $attr[$e]); delTagWithAttribute($dom, "ua", "id", $attr[$e]); delTagWithAttribute($dom, "ip", "id", $attr[$e]); delTagWithAttribute($dom, "posx", "id", $attr[$e]); delTagWithAttribute($dom, "posy", "id", $attr[$e]); delTagWithAttribute($dom, "posz", "id", $attr[$e]); delTagWithAttribute($dom, "pseudo", "id", $attr[$e]); } } } saveFileXML($dom, "session.xml"); flock($fp, LOCK_UN); fclose($fp); ?>
Dans cette requête on stock les informations du joueur dans le fichiers session.xml et on récupère la liste des informations des autres joueurs. Le fichier session.xml se précente sous cette forme:
<?xml version="1.0" encoding="UTF-8" standalone="no"?> <references> <ua id="5c7e0562b63498e271beab81c4d8537e6d66c10b">Mozilla/5.0 (X11; Linux i686) AppleWebKit/537.22 (KHTML, like Gecko) Ubuntu Chromium/25.0.1364.160 Chrome/25.0.1364.160 Safari/537.22</ua> <ip id="5c7e0562b63498e271beab81c4d8537e6d66c10b">192.168.1.53</ip> <pseudo id="5c7e0562b63498e271beab81c4d8537e6d66c10b">Craft</pseudo> <connect id="5c7e0562b63498e271beab81c4d8537e6d66c10b">1370780315</connect> <posx id="5c7e0562b63498e271beab81c4d8537e6d66c10b">-22631.185738533248</posx> <posy id="5c7e0562b63498e271beab81c4d8537e6d66c10b">471.5070339740008</posy> <posz id="5c7e0562b63498e271beab81c4d8537e6d66c10b">54024.373695524286</posz> </references>
Grace à ces informations on peu rafraichir la position des joueurs dans la scène.
Vous pouvez télécharger la totalité du projet ici même
Dans le pack, j'ai ajouté un autre exemple qui va me servir de base pour la suite dans le dossiers fps....la partie FPS pure. Une fois cela en place je bosserai un scénario que j'agrémenterai petit à petit, à moins que quelqu'un se joigne à ce petit projet en cours de route.