websockets_使用WebSockets建立3D MMO

websockets

Hi there! My name is Nick Janssen, creator of Ironbane, a 3D MMO that uses WebGL and WebSockets. With this article I would like to give you a better insight in MMO's and make you less afraid of the complexities involved in building one. From my experience I have found that people consider them very hard, while they are in fact quite easy to make using the web technologies of today.

嗨,您好! 我叫Nick Janssen, Ironbane的创建者, Ironbane是使用WebGL和WebSockets的3D MMO。 在这篇文章中,我想为您提供关于MMO的更好的见解,并让您减少构建MMO所涉及的复杂性。 从我的经验中,我发现人们认为它们非常困难,而实际上使用当今的Web技术非常容易。

MMO? 你不能那样做! (A MMO? You Can't Do That!)

MMO's are cool. Yet they are considered one of the hardest things to make when it comes to developing software. I believe MMO's are mainly intimidating to people because of historical reasons.

MMO很酷。 然而,在开发软件时,它们被认为是最难的事情之一 。 我认为MMO主要是由于历史原因而吓人。

In the old days, network programming used to be very hard. Complex socket calls were everywhere, multithreading was necessary and JSON was still unknown. A lot has changed since then with the coming of Node.js, its event loop and easy to use socket libraries.

在过去,网络编程非常困难。 到处都有复杂的套接字调用,必须使用多线程,而且JSON仍是未知的。 从那时起,随着Node.js的出现,它的事件循环和易于使用的套接字库,发生了很多变化。

In addition, writing a 3D game was a challenge on its own. You had to include the right libraries, install dependencies on the client, and write complicated engine calls to do trivial things such as creating a texture. Getting a triangle to show on the screen was already quite an accomplishment.

此外,编写3D游戏本身就是一个挑战。 您必须包括正确的库,在客户端上安装依赖项,并编写复杂的引擎调用以完成诸如创建纹理之类的琐碎事情。 在屏幕上显示一个三角形已经是一项很大的成就

使用DirectX10创建纹理 (Creating a Texture with 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 );


用Three.JS创建纹理 (Creating a texture with Three.JS)


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


起点 (The Beginnings)

For our MMO Ironbane I took things one at a time, and it worked out very well. Remember, Rome wasn't built in a day. But with today's technology you can achieve things at a much faster pace than what was ever possible before.

对于我们的MMO Ironbane,我一次只做一件事情,效果很好。 记住,罗马不是一天建成的。 但是,利用当今的技术,您可以比以往更快地实现目标。

I started from a three.js terrain demo and modified it step by step. Within a few days, I had a plane running around with a texture that looked like the pixelated back of a guy.

我从Three.js地形演示开始,并逐步对其进行了修改。 几天之内,我有一架飞机跑来跑去,纹理看上去像个家伙的后背。

First version of Ironbane

The next step was to make the player connect to a centralized server. Using Socket.IO I set up a very simple Node.js backend that responds to player connections, and puts them in a global unitList managed by a service called worldHandler:

下一步是使播放器连接到中央服务器。 使用Socket.IO,我建立了一个非常简单的Node.js后端,该后端响应播放器的连接,并将其放置在由一个名为worldHandler的服务管理的全局unitList中:


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


告诉玩家附近的其他玩家 (Telling Players About Other Players Nearby)

To let players know which other players are nearby, the server has to know at any time which players can see other players. To do so, every player instance on the server makes use of an otherUnits array. This array is simply filled with instances of other entities which are currently in the vicinity.

为了让玩家知道附近还有其他玩家,服务器必须随时知道哪些玩家可以看到其他玩家。 为此,服务器上的每个播放器实例都使用otherUnits数组。 该数组仅填充有当前附近其他实体的实例。

When a new player is added to the worldHandler, their otherUnits list gets updated depending on where they are in the world. Later, when they move around, this list is evaluated again, and any changes to this list are sent to the client in the form of addUnit and removeUnit socket events.

当新玩家添加到worldHandler时,他们的otherUnits列表会根据他们在世界上的位置而更新。 稍后,当他们四处移动时,将再次评估此列表,并且对该列表的任何更改都将以addUnit和removeUnit套接字事件的形式发送给客户端。

Now, I would like to point out that the first letter of MMO stands for Massive. For massive games, every player should not know about every other player because it will melt your server.

现在,我想指出的是M MO的首字母代表M assive。 对于大型游戏,每个玩家都不应该对其他玩家有所了解,因为这会熔化您的服务器。

空间分割 (Spatial Partioning)

To remedy this, you need spatial partioning. In a nutshell, this means that you divide your world into a grid. To visualize it, think of it as the server making use of a Snap To Grid option, to "snap" the position of the players to an imaginary grid. The positions of the players are not altered, rather the server just calculates what the new snapped position of the player would be.

为了解决这个问题,您需要空间划分。 简而言之,这意味着您将世界划分为一个网格。 为了使其可视化,可以将其视为服务器,它使用“对齐网格”选项将玩家的位置“捕捉”到虚构的网格。 播放器的位置不会改变,而是由服务器仅计算播放器的新捕捉位置。

Spatial partioning

With many players spanning over many different positions, some will have the same "snapped" position. A player then, should only know about all players that are snapped in the same position and all players that are just one cell away from them. You can easily convert between grid and world positions using these functions:

随着许多玩家跨越许多不同的位置,一些玩家将拥有相同的“抢夺”位置。 然后,一个玩家只应该知道所有被卡在同一位置的玩家以及所有距离他们只有一个格的玩家。 您可以使用以下功能轻松地在网格位置和世界位置之间转换:


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
  };
}


When a new player is created on the server, they automatically add themselves to a multidimensional array of units on the worldHandler, using the grid position. In Ironbane we even use an additional zone index, since most MMO's have multiple areas where players can reside.

在服务器上创建新玩家时,他们会使用网格位置将自己自动添加到worldHandler上的多维单元阵列中。 在Ironbane中,我们甚至使用了额外的区域索引,因为大多数MMO都有玩家可以居住的多个区域。


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


更新附近的玩家列表 (Updating the List of Players Nearby)

Once they are added to the list of units on the server, the next step is calculate which other players are nearby.

将他们添加到服务器上的单位列表后,下一步就是计算附近还有哪些其他玩家。


// 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]);
    }
}


Here, addOtherUnit() adds that player to their otherUnits array, and sends a packet to the client informing a new player has entered in their vicinity. This packet will contain the initial position, velocity, name and other metadata which only needs to be sent once. removeOtherUnit() simply removes the player from their array, and tells the client to destroy that player.

在这里,addOtherUnit()将该玩家添加到其otherUnits数组中,并向客户端发送一个数据包,通知新玩家已进入其附近。 该数据包将包含初始位置,速度,名称和其他元数据,仅需发送一次。 removeOtherUnit()只是将播放器从其数组中删除,并告诉客户端销毁该播放器。


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

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


发送数据包给玩家 (Sending Packets to the Players)

Now, we have the beating heart of an MMO. The final step is to inform the players on a regular basis the positions of the other players in their vicinity. We do this step only twice per second, because we do not want to overload the server.

现在,我们拥有MMO的跳动之心。 最后一步是定期通知玩家附近其他玩家的位置。 我们每秒只执行两次此步骤,因为我们不想使服务器超载。


_.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);    
                }
                
            ));
        });
    });
}); 


结论 (Conclusion)

That's really all there is to building an MMO. The only things left to do now are building the features that are unique to your game, fine-tuning and security.

这实际上是构建MMO的全部内容。 现在剩下要做的就是构建游戏独有的功能,微调和安全性。

I hope I have given you fresh insights into MMO programming, and above all courage to start working on them. At Ironbane we are surely looking for collaborators! You can find the full source code of Ironbane straight on GitHub, and you should be able to install it on your machine with ease.

我希望我能给您关于MMO编程的新见解,并且最重要的是有勇气开始着手研究它们。 在Ironbane,我们肯定正在寻找合作者! 您可以直接在GitHub找到 Ironbane的完整源代码,并且应该能够轻松地将其安装在计算机上。

翻译自: https://davidwalsh.name/3d-websockets

websockets

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
下面是一个使用 Python 的 Flask 和 Flask-SocketIO 库实现的简单 WebSocket 示例: ```python from flask import Flask, render_template from flask_socketio import SocketIO, emit app = Flask(__name__) app.config['SECRET_KEY'] = 'secret!' socketio = SocketIO(app) @app.route('/') def index(): return render_template('index.html') @socketio.on('connect') def test_connect(): emit('my response', {'data': 'Connected'}) @socketio.on('disconnect') def test_disconnect(): print('Client disconnected') if __name__ == '__main__': socketio.run(app) ``` 在上面的代码中,我们创建了一个 Flask 应用程序,并使用 Flask-SocketIO 扩展来启用 WebSocket 功能。我们定义了两个事件处理程序:`test_connect` 用于处理客户端连接事件,`test_disconnect` 用于处理客户端断开连接事件。我们还定义了一个简单的路由处理程序,用于返回一个 HTML 模板。 在客户端,我们可以使用 JavaScript 代码来连接到 WebSocket 服务器,并发送和接收消息: ```javascript var socket = io.connect('http://' + document.domain + ':' + location.port); socket.on('connect', function() { socket.emit('my event', {data: 'I\'m connected!'}); }); socket.on('my response', function(msg) { console.log(msg.data); }); ``` 在上面的代码中,我们使用 SocketIO 客户端库来连接到 WebSocket 服务器,并发送和接收消息。当客户端连接成功时,`connect` 事件将被触发,并发送一个 `my event` 消息。服务器将收到该消息,并发送一个 `my response` 消息作为响应。客户端将收到该响应,并将消息内容输出到浏览器的控制台中。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值