Multiplayer böngészőben Szoftverfejlesztés laboratórium 2. IIT1 mérés
Technológiák szerver kliens kommunikáció Node.js HTML5, JavaScript, WebGL kommunikáció sockets.io
Kiindulási állapot node.js szerver csak fileokat szolgáltat minden lokálisan zajlik
Vezérlés App.prototype.registerEventHandlers = function() { let theApp = this; document.onkeydown = function(event) { if(keyboardMap[event.keyCode] === 'W') { // LABTODO: üzenet a szervernek theApp.scene.setThrust(1);
Kilépésre eseményfigyelés window.addEventListener('beforeunload', function() { // LABTODO: üzenet a szervernek });
Mesh betöltés let addMesh = function(textureFileName, animTileSize) { let material = new Material(gl, theScene.texturedProgram); material.colorTexture.set( new Texture2D(gl, 'media/' + textureFileName + '.png') ); material.texScale.set(animTileSize); theScene[textureFileName + 'Mesh'] = new Mesh(theScene.quadGeometry, material); };
GameState - Fizikai állapotváltozók this.positionPool = new Vec2Array(1024); this.orientationPool = new Vec1Array(1024); this.velocityPool = new Vec2Array(1024); this.angVelocityPool = new Vec1Array(1024); this.invMassPool = new Vec1Array(1024); this.invAngMassPool = new Vec1Array(1024); this.backDragPool = new Vec1Array(1024); this.sideDragPool = new Vec1Array(1024); this.angDragPool = new Vec1Array(1024); this.thrustPool = new Vec1Array(1024); this.torquePool = new Vec1Array(1024);
GameState - Fizikai számításokhoz memória this.forcePool = new Vec2Array(1024); this.aheadPool = new Vec2Array(1024); this.aheadSpeedPool = new Vec1Array(1024); this.aheadVelocityPool = new Vec2Array(1024); this.sideVelocityPool = new Vec2Array(1024); this.backDragFactorPool = new Vec1Array(1024); this.sideDragFactorPool = new Vec1Array(1024); this.angDragFactorPool = new Vec1Array(1024);
GameState – objektumok, avatarok this.dynamicObjects = []; this.avatars = {}; this.resizeArrays();
GameState – resizeArrays GameState.prototype.resizeArrays = function() { this.positions = this.positionPool.subarray(0, this.dynamicObjects.length); ...
GameState – addPlayer GameState.prototype.addPlayer = function(playerId){ this.avatars[playerId] = {}; };
GameState – addObject GameState.prototype.addObject = function(playerId, isAvatar, meshName){ let i = this.dynamicObjects.length; this.dynamicObjects.push({ playerId:playerId, isAvatar:isAvatar, meshName:meshName, role:role}); if(isAvatar){ this.avatars[playerId].objectIndex = i; } this.positionPool.at(i).setRandom(0, 5); ...
GameState – dropPlayer GameState.prototype.dropPlayer = function(playerId){ // remove objects with a certain player id this.dynamicObjects = this.dynamicObjects.filter(function(obj){ return obj.playerId !== playerId; });
GameState – dropPlayer // refresh avatar indices delete this.avatars[playerId]; for(var i=0; i<this.dynamicObjects.length; i++){ let obj = this.dynamicObjects[i]; if(obj.isAvatar){ this.avatars[obj.playerId].objectIndex = i; } this.resizeArrays();
GameState update – Euler integrálás Newton egyenletekre this.aheads.cossin(this.orientations); this.forces.mulWithVec1s(this.aheads, this.thrusts); this.velocities.addScaled(this.velocities, this.forces, dt); this.angVelocities.addScaled(this.angVelocities, this.torques, dt); this.positions.addScaled(this.positions, this.velocities, dt); this.orientations.addScaled(this.orientations, this.angVelocities, dt); this.aheadSpeeds.dotVec2s(this.velocities, this.aheads); this.aheadVelocities.mulWithVec1s(this.aheads, this.aheadSpeeds); this.sideVelocities.sub(this.velocities, this.aheadVelocities); this.backDragFactors.exp(this.backDrags, dt); this.aheadVelocities.mulWithVec1s(this.aheadVelocities, this.backDragFactors); this.sideDragFactors.exp(this.sideDrags, dt); this.sideVelocities.mulWithVec1s(this.sideVelocities, this.sideDragFactors); this.velocities.add(this.aheadVelocities, this.sideVelocities); this.angDragFactors.exp(this.angDrags, dt); this.angVelocities.mul(this.angVelocities, this.angDragFactors);
Ütközésdetektálás és válasz let relativeVelocity = new Vec2(); let diff = new Vec2(); for(let i=0; i<this.dynamicObjects.length; i++) { for(let j=i+1; j<this.dynamicObjects.length; j++) { diff.set(this.positions.at(i)).sub(this.positions.at(j)); let dist2 = diff.dot(diff); if(dist2 < 4) { diff.mul( 1.0 / Math.sqrt(dist2) ); this.positions.at(i).addScaled(0.05, diff); this.positions.at(j).addScaled(-0.05, diff); var tangent = new Vec2(-diff.y, diff.x); var vi = this.velocities.at(i); var bi = this.angVelocities.at(i); var vj = this.velocities.at(j); var bj = this.angVelocities.at(j); relativeVelocity.set(vi).sub(vj).addScaled(bi.x - bj.x, tangent); let impulseLength = diff.dot(relativeVelocity); diff.mul( impulseLength * 1.5 /*restitution*/ ); let frictionLength = tangent.dot(relativeVelocity); tangent.mul(frictionLength * 0.5 /*friction*/); vi.sub(diff).sub(tangent); vj.add(diff).add(tangent); bi.add(frictionLength /* *radius*/ ); bj.add(frictionLength /* *radius*/ ); }
Kameramozgatás this.camera.position.set( this.gameState.positions.at(this.avatarIndex)); this.camera.updateViewProjMatrix();
Feladat ütközésválasz a statikus objektumokra minden ugyanaz, kivéve, hogy a statikusnak nulla a sebessége és a szögsebessége, valamint nem mozdul el
Feladat fizikai szimuláció a szerveren (is) szinkronizáció a kliensekke multiplayer játék az egyes lépésekhez segítenek a következő diák
Indítás most nem az index.html játszik már hanem az index.js package.json-ban van minden node shell / linux shell npm start csatlakozás: böngészőbe: localhost:8082
Szerver lehetőségek localhost cg.iit.bme.hu windows standalone node.js nodejs-portable.exe 2-es menüopció cd arena npm start cg.iit.bme.hu user/pass: sw2/node.js a home-ba mindenki gyárthat NEPTUN kód könyvtárat npm a pathban van, npm start működik
Debuggolás kliens: szerver (localhoston) chrome, jobb klikk, inspect chrome: about:inspect ha fut a szerver, kell itt lennie egy linknek a debuggolásához
Kapcsolódás a kliensből (App.js) //this.socket = io.connect('localhost'); // VAGY //this.socket = io.connect('http://cg.iit.bme.hu');
Socket.io szerveren (index.js) let io = require('socket.io')(server);
fogadás a szerveren io.on('connection', function(client) { let clientId = lastClientId++; console.log('User connected, id: ' + clientId); });
Próbáljuk ki
Kilépés client.on('leave', function(){ console.log('Client left. Id: ' + clientId); });
Page unloadkor a kliens theApp.socket.emit('leave', {});
Próbáljuk ki
Feladat néhány frameenként 'reqState' üzenet küldése a kliensről App update-ből üres objektumot elég átküldni írjuk ki, hogy az üzenet megjött próbáljuk ki
GameState objektum szerveroldalon let game = new GameState();
Szerveren: új játékos belépésekor objektumok létrehozása új játékos hozzáadása (gameState) új avatar objektum hozzáadása (gameState) syncObject küldése mindenkinek üzenet része a kliensId (tudja meg a kliens, hogy hányas) ha broadcastolunk, ne legyen benne kliensId (legyen null) plusz küldjük át a gameState objektumot client.broadcast.emit('syncObjects', { clientId:null, state:game });
App.js: üzenetfogadás a kliensen példa this.socket.on('syncObjects', function(data){ theScene.syncObjects(data.clientId, data.state); });
syncObjects példa Scene.prototype.syncObjects = function(clientId, serverState) { let theScene = this; var i = 0; this.dynamicObjects = serverState.dynamicObjects.map( function(obj){ return new GameObject( theScene[obj.meshName + 'Mesh'] ); }); this.gameState.dynamicObjects = serverState.dynamicObjects; this.gameState.resizeArrays(); this.syncState(clientId, serverState); };
syncState példa Scene.prototype.syncState = function(clientId, serverState) { if(clientId !== null) { this.playerId = clientId; } this.gameState.set(serverState); };
Próbáljuk ki új belépő esetén az új objektum és a játékállapot mindenkinek el lesz küldve
Szerveren: játékos kilépésekor játékos kidobása a gameStateből syncObjects küldése mindenkinek
Próbáljuk ki kilépés esetén sync
Game loop a szerveren var timeAtStart = new Date().getTime(); var timeAtLastFrame = 0.0; setInterval(function(){ let time = (new Date().getTime() - timeAtStart) / 1000.0; let dt = time - timeAtLastFrame; timeAtLastFrame = time; game.update(dt); }, 16);
Feladat legyen az avataroknak valamilyen kezdősebessége néhány frameenként 'reqState' üzenet küldése a kliensről válasz a szerverről: 'syncState' üzenetek
Próbáljuk ki sodródó, de szinkronizált hajók
Feladat setThrust és setTorque üzenetek küldése a kliensről, feldolgozása a szerveren game.avatars[clientId].objectIndex-et ne felejtsük
Próbáljuk ki multiplayer lökdösődés
További lehetőségek más objektumok ütközéskor típusfüggő reakció pickupok, lövedékek célszerű őket előre, kapcsolódáskor létrehozni a GameState dynamicObjectsben lehet tárolni, hogy az objektum miféle (role) ütközéskor típusfüggő reakció ha az egyik avatar, a másik meg pickup: avatar tulajdonságok változtatása ehhez lehet avatarleíróba új property-t felvenni az objectIndex mellé pl. tolóerő függjön attól, hány pickupot szedtük fel pickup respawn (random helyre ugrik) lövedék: ha talál, frag++, eltalált respawn