Vytvoření 3D MMO pomocí WebSockets

Ahoj! Jmenuji se Nick Janssen, tvůrce Ironbane, 3D MMO, která používá WebGL a WebSockets. Tímto článkem bych vám rád poskytl lepší přehled o MMO a abyste se méně báli složitostí spojených s jejich budováním. Ze své zkušenosti jsem zjistil, že je lidé považují za velmi těžké, přičemž je ve skutečnosti docela snadné je vyrobit pomocí dnešních webových technologií.

MMO? To nemůžeš!

MMO jsou cool. Přesto jsou považovány za jednu z nejtěžších věcí, které je třeba udělat, pokud jde o vývoj softwaru. Věřím, že MMO lidi zastrašují hlavně z historických důvodů.

Za starých časů bylo síťové programování velmi těžké. Složitá volání soketů byla všude, multithreading byl nezbytný a JSON byl stále neznámý. Od té doby se mnoho změnilo s příchodem Node.js, jeho smyčky událostí a snadno použitelných soketových knihoven.

Navíc napsat 3D hru byla výzva sama o sobě. Museli jste zahrnout správné knihovny, nainstalovat závislosti na klientovi a napsat složitá volání enginu, abyste mohli dělat triviální věci, jako je vytvoření textury. Už to, že se na obrazovce zobrazil trojúhelník, byl docela úspěch.

Vytvoření textury pomocí DirectX10

D3DX10_IMAGE_LOAD_INFO loadInfo;
ZeroMemory( &loadInfo, sizeof(D3DX10_IMAGE_LOAD_INFO) );
loadInfo.BindFlags = D3D10_BIND_SHADER_RESOURCE;

ID3D10Resource *pTexture = NULL;
D3DX10CreateTextureFromFile( pDevice, L"crate.gif", &loadInfo, NULL, &pTexture, NULL );

Vytvoření textury pomocí Three.JS

var texture = THREE.ImageUtils.loadTexture('crate.gif'),

Začátky

Pro naše MMO Ironbane jsem bral věci jednu po druhé a fungovalo to velmi dobře. Pamatujte, že Řím nebyl postaven za den. Ale s dnešní technologií můžete dosáhnout věcí mnohem rychlejším tempem, než bylo možné kdy předtím.

Vycházel jsem z terénního dema three.js a upravoval jsem ho krok za krokem. Během několika dní mi kolem běhalo letadlo s texturou, která vypadala jako pixelovaná záda chlapa.

Dalším krokem bylo připojení přehrávače k ​​centralizovanému serveru. Pomocí Socket.IO jsem nastavil velmi jednoduchý backend Node.js, který reaguje na připojení hráčů a vkládá je do globálního unitList spravovaného službou nazvanou worldHandler:

io.sockets.on("connection", function (socket) {
  socket.unit = null;
  socket.on("connectServer", function (data, reply) {
      var unit = new IB.Player(data);
      worldHandler.addUnit(unit);
  });
});

Povídání hráčům o ostatních hráčích v okolí

Aby hráči věděli, kteří další hráči jsou poblíž, musí server kdykoli vědět, kteří hráči vidí ostatní hráče. K tomu využívá každá instance hráče na serveru pole otherUnits. Toto pole je jednoduše vyplněno instancemi jiných entit, které jsou aktuálně v blízkosti.

Když je do worldHandler přidán nový hráč, jeho seznam otherUnits se aktualizuje v závislosti na tom, kde na světě se nachází. Později, když se pohybují, je tento seznam znovu vyhodnocen a veškeré změny tohoto seznamu jsou odeslány klientovi ve formě událostí addUnit a removeUnit socket.

Nyní bych rád upozornil, že první písmeno M MO znamená M asivní. U masivních her by každý hráč neměl vědět o každém druhém hráči, protože to roztaví váš server.

Prostorové rozdělení

Chcete-li to napravit, potřebujete prostorové rozdělení. V kostce to znamená, že svůj svět rozdělíte na mřížku. Chcete-li si to představit, představte si to jako server využívající možnost Snap To Grid, k „přichycení“ pozice hráčů k pomyslné mřížce. Pozice hráčů se nemění, server pouze vypočítá, jaká by byla nová pozice hráče.

Vzhledem k tomu, že mnoho hráčů překrývá mnoho různých pozic, někteří budou mít stejnou „uchopenou“ pozici. Hráč by pak měl vědět pouze o všech hráčích, kteří jsou zachyceni ve stejné pozici, ao všech hráčích, kteří jsou od nich vzdáleni jen jednu buňku. Pomocí těchto funkcí můžete snadno převádět mezi mřížkou a světovými pozicemi:

function worldToGridCoordinates(x, y, gridsize) {
  if ( gridsize % 2 != 0 ) console.error("gridsize not dividable by 2!");

  var gridHalf = gridsize / 2;

  x = Math.floor((x + gridHalf)/gridsize);
  y = Math.floor((y + gridHalf)/gridsize);

  return {
    x: x,
    y: y
  };
}

function gridToWorldCoordinates(x, y, gridsize) {
  if ( gridsize % 2 != 0 ) console.error("gridsize not dividable by 2!");

  x = (x * gridsize);
  y = (y * gridsize);

return { x: x, y: y }; }

Když je na serveru vytvořen nový hráč, automaticky se přidá do vícerozměrného pole jednotek na worldHandler pomocí pozice mřížky. V Ironbane dokonce používáme další index zóny, protože většina MMO má více oblastí, kde mohou hráči sídlit.

worldHandler.world[this.zone][this.cellX][this.cellY].units.push(this);

Aktualizace seznamu hráčů v okolí

Jakmile jsou přidáni do seznamu jednotek na serveru, dalším krokem je vypočítat, kteří další hráči jsou poblíž.

// We have two lists
// There is a list of units we currently have, and a list that we will have once we recalculate
// If an item is in the first list, but no longer in the second list, do removeOtherUnit
// If an item is in the first & second list, don't do anything
// If an item is only in the last list, do addOtherUnit
var firstList = this.otherUnits;
var secondList = [];

// Check for all players that are nearby and add them to secondList
var gridPosition = worldToGridPosition(this.x, this.y, 50);

var cx = gridPosition.x;
var cy = gridPosition.y;

for (var x = cx - 1; x <= cx + 1; x++) {
  for (var y = cy - 1; y <= cy + 1; y++) {
    _.each(worldHandler.units[this.zone][x][y], function(unit) {
        if (unit !== this) {
            secondList.push(unit);
        }
    }, this);
  }
}

for (var i = 0; i < firstList.length; i++) {
  if (secondList.indexOf(firstList[i]) === -1) {
    // Not found in the second list, so remove it
    this.removeOtherUnit(firstList[i]);
  }
}
for (var i = 0; i < secondList.length; i++) {
    if (firstList.indexOf(secondList[i]) === -1) {
        // Not found in the first list, so add it
        this.addOtherUnit(secondList[i]);
    }
}

Zde addOtherUnit() přidá tohoto hráče do jejich pole otherUnits a odešle klientovi paket informující o vstupu nového hráče do jejich blízkosti. Tento paket bude obsahovat počáteční polohu, rychlost, jméno a další metadata, která stačí odeslat pouze jednou. removeOtherUnit() jednoduše odstraní hráče z jejich pole a řekne klientovi, aby tohoto hráče zničil.

var packet = {
    id: id,
    position: unit.position,
    name: unit.name,
    isGameMaster: true
};

this.socket.emit("addUnit", packet);

Posílání paketů hráčům

Nyní máme tlukoucí srdce MMO. Posledním krokem je pravidelné informování hráčů o pozicích ostatních hráčů v jejich blízkosti. Tento krok provádíme pouze dvakrát za sekundu, protože nechceme přetížit server.

_.each(this.world, function(zone) {
    _.each(zone, function(cellX) {
        _.each(cellX, function(cellY) {
            _.each(cellY.units, function(unit) {

                var snapshot = [];

                _.each(unit.otherUnits, function(otherUnit) {
                    var packet = {
                        id:otherUnit.id,
                        x:otherUnit.x,
                        y:otherUnit.y
                    };

                    snapshot.push(packet);
                ));
            
                if ( snapshot.length ) {
                    unit.socket.emit("snapshot", snapshot);    
                }
                
            ));
        });
    });
}); 

Závěr

To je opravdu vše, co je k budování MMO. Jediné, co nyní zbývá udělat, je vytvořit funkce, které jsou jedinečné pro vaši hru, doladit a zabezpečit.

Doufám, že jsem vám dal čerstvé poznatky o programování MMO a především odvahu na nich začít pracovat. V Ironbane určitě hledáme spolupracovníky! Úplný zdrojový kód Ironbane najdete přímo na GitHubu a měli byste jej snadno nainstalovat na svůj počítač.