自从SmartFoxServer 2X版本2.8以来,我们引入了一种新型的Room对象,即MMORoom,它支持用户和游戏对象之间的本地交互。
»概述
MMORoom通过添加感兴趣区域(简称AoI)来扩展常规房间的功能,以确定用户将收到的事件的空间范围。 AoI参数表示用户将彼此影响的区域,例如发送公共消息,更新用户变量等时。
默认情况下,当用户进入或离开房间时,MMORoom不会触发常规的USER_ENTER或USER_EXIT客户端事件。而是通过PROXIMITY_LIST_UPDATE事件更新房间的用户列表,该事件在AoI中提供当前用户列表的增量。
换句话说,接近度列表替代客户端上的常规用户列表,优化用户接收的更新数量。在服务器端,房间的完整用户列表仍然可以全部访问。
“ 有可能的使用
根据类名称,MMORoom对象可以创建非常大,几乎无限的区域,可以包含数千个玩家,而不会使客户端更新更新。 MMORoom可以配置为扼制PROXIMITY_LIST_UPDATE事件,以优化网络流量。
»连接并设置用户位置
与常规房间相反,MMORoom需要知道每个用户位于2D或3D空间中的位置。坐标系统是抽象和通用的,允许开发人员使用由32位整数或浮点值定义的任何度量单位(像素,英寸,米,英里等)。
当用户加入MMOROM时,他在世界上的位置仍然是未定义的,因此他将处于一个状态,直到发送第一个SetUserPosition请求。为了避免用户花费太多时间在这个不可见状态,每个MMORoom都可以配置为允许超时值,之后用户将从房间中删除(注意:超时仅适用于尚未设置在房间的初始位置)。
如概述所述,在常规客房中没有向其他用户发送USER_ENTER / EXIT_ROOM事件。玩家更新关于其邻近度的其他用户更改(换句话说,用户进入或离开AoI)的方式是通过客户端PROXIMITY_LIST_UPDATE事件。所有其他与房间相关的活动将像常规房间一样工作,包括USER_COUNT_CHANGE,使用户在同一个房间组中更新每个房间的客户总数。
我们来看一个视觉示例:
我们的球员周围的绿色区域(标记为“ME”)代表他的AoI:所有落在该区域内的用户将能够看到我们的球员与他交换消息/事件。此外,AoI内的所有玩家都从客户的角度来看现在的客户用户列表。在这张照片的具体情况下,我们会看到另外一个玩家Piggy,其中“看到”表示接收与该玩家有关的事件,请注意他在房间中的存在。
当然游戏过程中玩家的位置会发生变化:每个客户端都必须定期发送SetUserPosition请求,以使系统了解MMOROM中所有用户的当前位置,并相应地调度PROXIMITY_LIST_UPDATE事件。根据游戏的类型,应该设置位置的速度会有很大的变化。检查页面底部链接的教程,以便对此主题进行更深入的讨论。
我们假设在下一个移动用户Fozzie正在进入AoI并且用户Piggy正在离开它:下一个PROXIMITY_LIST_UPDATE事件将反映这种新的情况。特别地,事件提供了两个列表,其中包含所有进入AoI的用户以及自上次更新以来已经离开的所有用户。从客户端API角度来看,这是处理PROXIMITY_LIST_UPDATE事件的方式:
private function onProximityListUpdate(evt:SFSEvent):void
{
var added:Array = evt.params.addedUsers;
var removed:Array = evt.params.removedUsers;
// Add Users that have entered the proximity list添加已经输入接近列表的用户
for each (var user:User in added)
{
// Obtain the coordinates at which the User "appeared" in our range.获取用户“出现”在我们的范围内的坐标。
var entryPoint:Vec3D = user.aoiEntryPoint;
// Add new avatar on screen ---! Pseudo Code !---在屏幕上添加新的头像---! 伪代码!---
var avatarSprite = new AvatarSprite();
avatarSprite.x = entryPoint.px
avatarSprite.y = entryPoint.py
mainDisplayList.addChild(avatarSprite);
}
// Remove Users that have left the proximity list删除已经离开邻近列表的用户
for (var i:int = 0; i < removed.length; i++)
{
var userId:int = removed[i];
// Obtain the user to remove获取用户删除
var user:User = sfs.userManager.getUserById(userId);
// Remove the user from screen... etc...从屏幕上删除用户...等...
}
}
上述代码片段被呈现为一个通用示例,它应该适用于任何客户端类型(C#,AS3,Java等),无论我们是使用2D还是3D世界。 MMORoom坐标系始终与三维系统(X,Y,Z)一起工作,可以将二维应用程序缩小为X和Y。
代码显示了如何循环使用addedUsers和removedUsers列表,以便处理渲染方面的事情。
»用户变量和房间变量
UserVariables将在MMORoom中正常工作,仅影响请求发件人的AoI内的那些用户。相反,RoomVariables需要用简单的方式来避免为客户造成大量的流量。由于RoomVariables包含对MMORoom中所有用户感兴趣的数据,因此经常更新它们将生成非常大的广播消息,并带来服务器带宽饱和的风险。这里最重要的是它们的更新速率超过所使用变量的数量。
»地图限制
MMORoom对象接受一对表示虚拟映射在三轴(X,Y,Z)上的限制的Vec3D参数。在2D和2.5D应用程序中,开发人员可以使用X和Y坐标,使Z值始终设置为零。强烈建议您设置这些限制,以限制用户在虚拟世界范围内的移动。这样可以在服务器端检测到非法移动,并被系统拒绝。
注意
没有设置地图限制,恶意用户可能会通过尝试为极大的坐标值填充大量空格来耗尽系统内存的潜在风险。
»获取其他用户的入口点
知道用户进入玩家的AoI的坐标通常是有帮助的。通常,客户端代码将需要知道此位置以在正确的位置呈现精灵/头像。默认情况下,MMORoom始终发送每个新用户的入口位置。如果在您的应用程序中不需要这一点信息,则可以关闭它,以节省额外的流量。
在前面的代码示例中,通过读取User.aoiEntryPosition值来演示此功能的用法。
重要的是要明白,如属性名称所示,这些是当用户第一次进入玩家的AoI而不是当前坐标时的坐标。换句话说,SFS2X API不会自动将这些坐标与服务器保持同步。开发人员有责任根据游戏类型,客户端 - 服务器滞后等情况,找出客户端用户位置同步的最佳策略。检查页面底部链接的教程以进行更深入的讨论关于这个问题
»创建一个MMORoom
通过从客户端发送常规CreateRoomRequest或从服务器端发送等效的MMORoom(请参阅SFSApi.createRoom方法)来创建MMORoom。唯一的区别是必须是MMORoomSettings类的设置对象。我们来看下面的代码示例:
var cfg:MMORoomSettings = new MMORoomSettings("New MMORoom");
cfg.defaultAOI = new Vec3D(30,25,12);
cfg.maxUsers = 5000;
cfg.maxSpectators = 0;
cfg.mapLimits = new MapLimits(new Vec3D(-3000, -500, -3000), new Vec3D(3000, 500, 3000));
cfg.userMaxLimboSeconds = 20;
// Create the Room and autojoin创建房间和自动加入
sfs.addEventListener(SFSEvent.ROOM_JOIN, onRoomJoin);
sfs.send(new CreateRoomRequest(cfg, true));
// ...
// Here we handle the ROOM_JOIN event 这里我们处理ROOM_JOIN事件
private function onRoomJoin(evt:SFSEvent):void
{
if (evt.params.room is MMORoom)
{
// This is an MMORoom so we need to set the Player position to become visible这是一个MMORoom,所以我们需要将Player的位置设置为可见
// In a real case scenario we should find a free position on the virtual map在一个真实情况下,我们应该在虚拟地图上找到一个空闲的位置
sfs.send(new SetUserPositionRequest(new Vec3D(100, 100, 100)));
// This in turn will trigger a PROXIMITY_LIST_UPDATE with the users present in our Player's AoI 这反过来会触发一个PROXIMITY_LIST_UPDATE,用户出现在我们的玩家的AoI中
}
}
首先我们创建MMORoomSettings并配置一些MMORoom行为。特别是我们设置虚拟世界地图的物理限制,并将userMaxLimboSeconds属性降低到20秒(默认为50)。
我们还为我们的地图设置了5000个用户的假设限制。该值可以根据地图的物理尺寸,玩家体现的角色和实体的大小以及这些地图的可探索区域的大小而有很大的不同。
如前所述,当玩家加入MMOROM时,他对其他用户仍然是看不见的,直到物理位置最终被分配为止。为此,我们处理ROOM_JOIN事件,并确保立即发送SetUserPostionRequest。
确定地图上的正确入口点
在最后一个例子中,我们使用了简单的方法来将玩家定位在虚拟地图中,方法是使用硬编码的值。在实际情况下,通常需要一些额外的逻辑来找到正确的位置。
不可步行的区域和已经被其他玩家占据的斑点不能用于产生播放器,因此在产生播放器之前需要应用一些游戏逻辑。
根据游戏的运行方式和地图数据的处理方式,我们需要在客户端或服务器端应用这种产卵逻辑。通常它将是服务器端,因为从这个角度来看,我们可以控制整个地图和每个人的位置。可以有不同的方法来选择一个播放器的产卵位置,例如:
重复使用以前的玩家的位置(例如他/她离开最后一次的最后一个位置);
在最后一次玩家在同一区域内的随机位置;
在地图上提供的传送区域列表中的随机产生点;
随机选择任何免费的地方;
等等
在每种情况下,计算产卵位置所需的数据很可能位于服务器端,因此在扩展代码中处理此阶段更为方便。我们建议两种可能的方法:
从服务器端加入
客户端向扩展程序发送请求以加入MMORoom,服务器执行加入请求,计算入口点并立即调用SFSAPI.setUserPosition(...)方法。
从客户端加入,从服务器设置位置
客户端发送一个常规JoinRoomRequest并处理相对响应。服务器扩展还在监听ROOM_JOIN事件,并且当触发入口点逻辑被执行并且进行SFSAPI.setUserPosition(...)调用时。
在这两种情况下,客户端都将收到带有本地用户列表的PROXIMITY_LIST_UPDATE事件。
»扩展和用户的AoI
从服务器端代码还可以利用MMORoom的功能,并发送仅影响某个AoI内的多个用户的自定义事件。
为了显示它的运作方式,我们假设我们想把一个自定义的事件发送给所有的用户Piggy的AoI中的玩家。
MMORoom mmoRoom = (MMORoom) getParentZone().getRoomByName("VirtualMMO");
User piggy = mmoRoom.getUserByName("Piggy");
List<user> recipients = mmoRoom.getProximityList(piggy);
// Make sure we have 1 or more recipients确保我们有1个或多个收件人
if (recipients.size > 0)
{
ISFSObject message = new SFSObject();
//... populate the SFSObject with custom data ... ...用自定义数据填充SFSObject ...
// Send the object to the selected users 将对象发送给所选用户
send("customMessage", message, recipients);
}</user>
也可以为更高级的定位提供自定义的AoI。 例如,用户Piggy可以对围绕她的默认AoI中包含的总用户数进行影响更靠近她的用户的一部分的动作。 请确保咨询服务器端的javadoc了解更多详细信息。 所有链接都在本文末尾。
»MMOItems和MMOItem变量
MMORoom还为称为MMOItem的新实体提供支持,该实体表示地图内的非玩家实体。 MMOItems可以用作奖金,触发器,子弹等,或使用MMORoom的可见性规则(AoI)处理的任何其他非玩家对象。
这意味着,只要一个或多个MMOItem属于播放器的AoI,它将被通知给具有PROXIMITY_LIST_UPDATE事件的用户。 这是事件中提供的参数的完整列表:、
name type description
room MMORoom the target MMORoom
addedUsers Array/List the list of new visible players
removedUsers Array/List the list of removed players
addedItems Array/List the list of new visible MMOItems
removedItems Array/List the list of removed MMOItems
每个MMOItem由唯一的ID和可选的多个称为MMOItem变量的自定义变量来标识,它们像用户变量(MMOItemVariable类扩展UserVariable)一样表现为exaclty。
注意
用户变量和MMOItem变量之间有一个重要区别:后者只能从服务器端进行定义和更新。
»从服务器端创建MMOItem
我们来看看这个简单的服务器端的例子:
private void createMMOItem()
{
// Reference to the MMOApi object引用MMOApi对象
SFSMMOApi mmoApi = SmartFoxServer.getInstance().getAPIManager().getMMOApi();
// Reference to the game's MMORoom参考游戏的MMORoom
Room targetRoom = getParentZone().getRoomByName("My MMO Room");
// Prepare a list of variables for the MMOItem准备MMOItem的变量列表
List<IMMOItemVariables> variables = new LinkedList<IMMOItemVariable>();
variables.add( new MMOItemVariable("type", "bonus") );
variables.add( new MMOItemVariable("points", 250) );
variables.add( new MMOItemVariable("active", true) );
// Create the MMOItem创建MMOItem
MMOItem mmoItem = new MMOItem(variables);
// Deploy the MMOItem in the MMORoom's map在MMORoom的地图中部署MMOItem
mmoApi.setMMOItemPosition(mmoItem, new Vec3D(50, 40, 0), targetRoom);
}
为了在游戏室内部署一个新的MMOItem,我们需要三件事情:
参考游戏MMORoom,我们通过扩展区通过getRoomByName()方法获取;
将为MMOItem本身添加有意义的属性的变量列表;在这种情况下,我们定义一个奖励对象,具有一定的分数值和一个标志,以知道它是否在世界上是活跃的;
MMOItem对象本身是与变量列表相结合创建的。
我们最后调用setMMOItemPosition(…)来部署该项目,该项目又会出现在事件的addedItems属性中的客户端的下一个PROXIMITY_LIST_UPDATE中。
»在游戏中管理MMOItems
根据我们正在开发的游戏类型和虚拟地图的大小,我们可能会生成数百个MMOItem来描述游戏中的奖金,触发器,子弹和类似实体。
MMORoom通过跟踪添加的所有MMOItem来管理这些项目。如果我们只需要从其唯一标识中检索一个项目,我们就可以使用MMORoom.getMMOItemById(…)方法,否则我们将更好的为每个类型的对象使用单独的列表,游戏。示例:一个项目符号列表,一个触发器列表等。
在游戏中处理几类MMOItem的常见解决方案是通过添加我们需要的本地属性(例如服务器端)来扩展MMOItem类:
public class LaserBeamItem extends MMOItem
{
public static final String TYPE_RED = "red";
public static final String TYPE_YELLOW = "yellow";
public static final String TYPE_GREEN = "green";
private String type;
private int strength;
public LaserBeamItem(String type)
{
super();
this.type = type;
if (type.equals(TYPE_RED))
this.strength = 100;
else if (type.equals(TYPE_YELLOW))
this.strength = 50;
else if (type.equals(TYPE_GREEN))
this.strength = 25;
}
public String getType()
{
return this.type;
}
public int getStrength()
{
return this.strength;
}
// ...
// ...etc...
// ...
}
这样,通过MMOItemVariables处理公共属性,我们可以方便地为每个MMOItem添加我们自己的服务器端属性和状态。
如果MMOItems用于游戏中生命周期有限的子弹或类似对象,我们还必须确保在不再使用它们时将其从房间中删除。为此,我们可以调用MMOApi.removeMMOItem(…)方法,该方法又会通过通常的PROXIMITY_LIST_UPDATE更新所有受影响的客户端。
»微调MMORoom性能
通过实验MMORoom设置可以实现一些优化。
»更新速度
MMORoomSettings类中的proximityListUpdateMillis设置允许决定从服务器端触发PROXIMITY_LIST_UPDATE事件的速度。优化此方面对于获得正确的服务器和客户端性能至关重要。
默认值设置为250ms。这似乎有点“缓慢”。实际上,这个设置已经非常慷慨了,我们可能需要根据服务器中运行的流量来增加这个值。
PROXIMITY_LIST_UPDATE事件不是实时发送的原因是因为在250ms的时间里,屏幕上的任何头像很可能已经从他以前的位置移动了很少,所以很少有一点是不明显的。考虑到这一点,我们可以通过使用proximityListUpdateMillis参数来减少发送给每个客户端的事件数量。
在默认情况下,服务器将等待250ms。在发送用户已经输入或离开玩家的AoI的通知之前。如果在短暂的延迟期间发生相同类型的其他事件,它们将被汇总并立即发送,从而节省流量。在一个非常忙碌的MMORoom中,有数以百计的玩家可以在网络使用方面产生巨大的差异。
问:延迟会不会减慢游戏或MMO世界的动作?
不,因为PROXIMITY_LIST_UPDATE与我们如何在屏幕上管理玩家的身体动作无关。这部分取决于我们的游戏逻辑,MMORoom不会妨碍。 MMORoom只会在用户进入或离开玩家的AoI时发出信号。几百毫秒的延迟将不会产生任何视觉差异,但会节省显着的带宽。
问:什么是推荐的proximityListUpdateMillis设置?
对于点对点类型的交互(MMO虚拟世界等),更新间隔可以设置在250-1000ms的范围内。基于我们的渲染区域大小和整体交互速度。我们建议以默认值开始,然后通过以100ms为增量提高值进行实验。我们可以提高间隔越多,而不会在客户端中造成明显的滞后,这对带宽优化是最好的。
对于动作/实时游戏,默认设置(250ms)可能是好的,我们建议应用以前的微调方法,只能从较低的值开始,例如。 50ms,并逐渐增加,以检查它是否在游戏中产生任何“文物”。通过文物,我们意味着在屏幕上的字符/实体的外观可见滞后。
»优化更新大小
在MMORoom中,我们会发现自己使用相当多的用户变量和MMOItem变量来定义虚拟地图上每个玩家和项目的自定义属性。
为了从带宽/网络的角度挤压最佳性能,我们建议尽可能使用非常简短的名称和简洁的值来优化变量。例如,我们在上一节中提供了我们的例子:
List<IMMOItemVariables> variables = new LinkedList<IMMOItemVariable>();
variables.add( new MMOItemVariable("type", "bonus") );
variables.add( new MMOItemVariable("points", 250) );
variables.add( new MMOItemVariable("active", true) );
我们建议尽量减少变量名称:
List<IMMOItemVariables> variables = new LinkedList<IMMOItemVariable>();
variables.add( new MMOItemVariable("t", 0) );
variables.add( new MMOItemVariable("p", 250) );
variables.add( new MMOItemVariable("a", true) );
请注意,我们还用数字而不是字符串替换了第一个变量,以便尽可能地减少所有数据。一般规则是,每当我们使用一个字符串作为一些变量的描述符时,我们应该考虑是否可以使用一个数字,这个数字后来将通过键值表“解码”到其对应的值。
这个问题的一个例子是当我们需要为它们的变量中的每个用户指定一个头像类型。我们可以使用诸如0,1,2等简单的数字标识,而不是使用“Warrior”,“Wizard”,“Thief”等字符串值。会一起玩
»渲染区域与AoI
特别是在2D和2.5D虚拟世界中,根据屏幕上视口的大小为AoI选择合适的大小是非常重要的。使用略大于视口的AoI将允许以非常平滑的方式“隐藏”屏幕上的精灵突然出现/消失。
这个截图说明了这个技术:
外部虚线红色矩形显示实际的AoI,而内部矩形是可见区域。 通过使用这种策略,化身将以无缝的方式进入和离开视口。
为了更好地了解这是如何工作的,我们建议您查看Flash示例部分提供的简单MMO示例教程。
»进一步阅读
我们已经实现了许多在这里概述的概念,在一个现实生活中的例子来源:
在我们的简单MMO示例教程中从一个真实的例子中学习
研究先进的MMO API技术
还检查com.smartfoxserver.v2.mmo包的服务器端javadocs
翻译自http://docs2x.smartfoxserver.com/AdvancedTopics/mmo-rooms