【GMS 联机入门】对联机这一块的有需求的人肯定不少,那就进来吧
gamemaker吧
全部回复
仅看楼主
level 3
犬酥咸饼 楼主
此教程为GMS教程,且含yoyo官方范例魔改。
2018年02月05日 12点02分 1
level 3
犬酥咸饼 楼主
2018年02月05日 12点02分 2
哇!厉害,支持楼主
2018年02月05日 12点02分
level 3
犬酥咸饼 楼主
开讲前,首先声明,此教程只是提供基础的联机思想,并不一定能够达到非常理想、专业的数据同步程度。这一块知识讨论的比较少,与其说教程,可能说个人心得更加准确。同时我也是初学者,会有不少知识盲区,可能出现讲错的地方,以及此贴缓更,望各位海涵。
2018年02月05日 12点02分 3
level 3
犬酥咸饼 楼主
联机用什么?
socket 套接字(用不着你去下载扩展,插件什么的,GMS自带,而GM8没有)
网络上的两个程序通过一个双向的通信连接实现数据的交换,这个连接的一端称为一个socket。(来自百度百科)
不管你有多萌新,你一定听过服务端,客户端。一个服务端和多个客户端之间进行通信连接,数据交换,那么这每一端都称为一个socket(包括服务端),且有各自不相同的编号,也就是socket ID,有了这个ID编号,我们就能知道彼此谁是谁了。一般来说,第一个创建服务器的玩家,他的编号是1,后续接二连三的玩家根据加入的顺序分别就是2,3,4,5……
玩过星界边境、Minecraft、Lost Castle、Risk of rain、War 3冰封王座等诸如此类的游戏的玩家应该知道这些游戏可通过输入外网IP直接连入等候的主机来进行联机游戏,这类联机和网游不同,IP直联游戏里有主机(开房玩家)和副机(加入房间玩家),主机玩家的电脑在进行游戏的同时,也要担当服务器的工作。所以主机玩家通常有两个socket,一个是主机的服务端socket,一个是主机的客户端socket。为什么主机还要分出两个端?因为服务端和客户端完成的功能工作不同,通常为了不混淆建议区分开来写。这么一来,实际情况主机玩家的客户端socket ID一般是2,服务端socket ID编号为1,后续加入的玩家就是3,4,5……
2018年02月05日 12点02分 4
level 15
支持一个
另外讲解的时候可以附上一些图方便新人理解
2018年02月05日 13点02分 5
level 3
犬酥咸饼 楼主
TCP协议?UDP协议?
在开始动手写之前,我们得大致确认一件事,我们选用哪一个协议(这里指大致介绍TCP与UDP协议)。TCP协议,UDP协议在百度上能轻松搜索到解释,如果觉得术语太专业看不懂,没关系。下面打个粗劣的比方。
服务端与客户端之间不同的传输协议,就好比我们选用不同的交通工具,不同的交通工具具有不同的特性。
TCP协议好比轨道交通:(如有不恰当或不正确的地方欢迎指出)
TCP是基于连接的协议,也就是说在正式收发数据前,必须和对方建立可靠的连接,比如你给别人打电话,必须等线路接通了、对方拿起话筒才能相互通话。同理,我们要为服务端和每个客户端之间铺设好轨道,才可以开始进行传输。我们可以把每辆列车视为数据包,数据包都是有序地往一端进行输送。TCP是可靠的传输协议,所有的数据包到达一端后,基本都是有序且完整没有遗漏的。
UDP协议好比宽敞的公路:
UDP是面向非连接的协议,不必要与对方先建立起连接就可以进行传输,好比你给别人发短信,输入个手机号码发送就性了,无需经过对方的应答。同理,把每个小车视为数据包,在宽敞的公路上不同的车有快有慢,数据包不一定会按顺序达到另一端,甚至有的小车会停在路中央,不到达另一端,也就是丢包。因此,UDP传输协议是不可靠的(但是UDP是可以保证每个数据包里的数据的完整性,这点跟TCP一样可以放心,您使用的交通工具里的乘客不会中途丢失……)。
说到这里,也许你会觉得TCP多么美好,能够保证质量且有序的传输协议,直接就选它了。然而TCP并不是完美的。TCP传输的最终结果基本上不会丢包,但不代表TCP在传输过程中没有丢包倾向。轨道上运行的列车同样会发生故障而停止在中途,如果是马路上抛锚的汽车,其他的人根本不会关心你,他们可以选择超车继续前进,而轨道交通发生这种现象,后面的列车就无法顺利到达终点端,因为他被前面的故障列车阻塞住了!那么后面的列车不会坐视不管,轨道交通会采用救援连挂的方式将故障列车推进或者牵引到终点,所以最终的结果依然是有序的且保证质量没有遗漏地全部抵达终点。于是这就造成了TCP的一大缺点——慢。
所以在TCP和UDP的选择上,似乎就成了在质量与效率之间的权衡。TCP好!UDP快!如果是回合制,棋牌等一类游戏,玩家在游戏过程中有较长的等待时间,TCP慢点察觉不到太大的问题,玩家可以接受。如果是动作类,即时策略,格斗类,同步性要求非常高的,似乎TCP有些乏力。但UDP的丢包特性,也是我们特别不希望的。
那么,我们可以对UDP进行改良,让它变得可靠,也就是RUDP。假如数据包从服务端送过来没有顺序,我们可以在他们包头加入序列号来进行标序,到达客户端后再在客户端里内部算法进行运算排序。针对丢包,比如我们可以用冗余的手段 (帧同步可以用到,后续可能会讲到),把前一两帧的数据包也发送过来,相当于每次发送多个包,一旦发生丢包,我们有机会可以从重复多余的包里取得数据。
在书写方面,TCP由于有自己一套有序保质的规则(且这个规则是我们无法更改的),用起来会更简便一点
,不需要自己去写序列号和校验丢包。UDP则需要用户自己去定义一套完整的规则,相对麻烦得多。(我还不知道怎么拿gml来写称RUDP,愿有大神们来交流交流)
接下来开始具体用gml来展示具体代码部分。就寝先。
2018年02月05日 14点02分 6
666 来学习一下
2018年02月05日 14点02分
补充一下:UDP是本身就可以在不同数据包间区分开的,而TCP本身是无法区分连续发送的几个数据包的,只是gms帮我们写好了TCP下区分数据包。另外UDP包有大小限制。
2018年02月06日 00点02分
@sunyubokkkkk 感谢补充[大拇指]
2018年02月06日 02点02分
level 12
支持一波
2018年02月05日 16点02分 7
level 3
犬酥咸饼 楼主
先从易上手的TCP开始介绍
所有的资源及房间一览:
我们来开始建立连接,现在要实现的内容有:
1、房间0里有两个按钮,让玩家选择创建游戏或加入游戏。
2、选择成功后进入下一个房间,会看到左上角蓝色圆点表示服务器,橙红色圆点表示客户端。
3、在窗口左侧可以看到已加入玩家游戏的socket列表,窗口顶端可以看到自己客户端的uid(socket id)
2018年02月06日 03点02分 8
level 3
犬酥咸饼 楼主
首先是一些初始化设定,本来是应该单独建立一个obj_init,但这里偷懒,就把初始化的代码内容写在了蓝色的“服务端”按钮(oFB_Server)里。
创建列表:用于统计所有玩家socket ID的列表
global.list_socket = ds_list_create()
CMD指令:这里我是采用的枚举的方式自定义的一些常量,我不想使用macro(宏)的原因是CMD指令写多了以后,macro里不便查找。服务端,客户端根据“if (cmd=某个指令)”来判断读取不同的数据包,最后作出不同的行为。我这里的“SC_”前缀表示服务器发送给客户端的指令,没有这个前缀的指令默认都是客户端发送给服务器的指令,有没有必要区分开来看个人习惯。
enum cmd
{
SC_UID = 0, //服务器分配给客户端 uid
SC_LIST_SOCKET = 1, //服务器分配给客户端 socket列表
KEY = 2, //客户端按键事件
}
玩家状态和按键指令稍后提到,这里不用。
鼠标左键松开事件:创建主机,进入下一个房间。
global.Server = instance_create(0,0,oServer)
room_goto(room1)
2018年02月06日 03点02分 9
level 3
犬酥咸饼 楼主
橙色的“客户端”(oFB_Client)按钮,没什么好讲的,就一个鼠标左键松开事件,进入下一个房间。
但是有一点需要注意的是,我们的room1(第二个房间),是已经摆放好了客户端(橙色小圆点),而不是通过手写代码创建的。
2018年02月06日 03点02分 10
level 3
犬酥咸饼 楼主
oServer的创建事件里:
创建一个端口为250,最大联机数为4人的tcp协议服务器,创建一个玩家buffer缓冲区。
什么是buffer?百度buffer还不能理解的话,简单的说Buffer的核心作用是用来缓冲,缓和冲击。比如你每秒要写100次硬盘,对系统冲击很大。用个buffer暂存起来,变成每10秒写一次硬盘,对系统的冲击就很小。我们都是通过把数据传入buffer,然后把buffer打包发送给另一端,另一端再读取buffer,这样循环。
创建buffer需要填写好尺寸和类型等参数,如果不确定类型,就选用buffer_grow增长型。最后一个对齐参数可查询F1,不同数据类型写入buffer有需要有不同的对齐。
server = network_create_server(network_socket_tcp,250,4)
player_buffer = buffer_create(16384, buffer_fixed, 1);
2018年02月06日 14点02分 11
level 3
犬酥咸饼 楼主
接下来是异步事件Networking。不懂异步事件概念也没关系,这里不作详讲,这是网络通信机制,只用记住重要的一点,无论是客户端还是服务端,接受数据一定要在异步事件里,而发送数据不建议写在异步事件里。
async_load是GMS提供的映射map,我们可以查找键key,得到我们想要的值value。
我可以查阅的键有:
type:事件类型,其中事件类型有3件:
端口连接时触发的事件 connect
断开连接时触发的事件 disconnect
数据写入时触发的事件 data
id:一般情况下,这个id是指事件触发者的socket id。但如果事件是在服务器里触发的,那么这个id就是发送数据的客户端socket id。
ip:可以得到ip地址字符串
port:与IP地址相关联的端口
socket:这个键,可以得到联入或者断线的socket id
我们可在异步事件里从async_load里用以上5个键key,得到我们想要的值value。
上面的代码中global.client是返回oClient里客户端socket
client = network_create_socket(network_socket_tcp)
global.client = client
ConnectDisconnectClient脚本用来处理联入或断线事件
ServerRecievedData脚本用来处理接收到的数据事件
2018年02月06日 15点02分 12
level 3
犬酥咸饼 楼主
ConnectDisconnectClient脚本内容:
{
// 连接状态
var type = ds_map_find_value(async_load, "type");
// 获取socket id 或者 断开socket
sock = ds_map_find_value(async_load, "socket");
// 获取来自socket的 IP
var ip = ds_map_find_value(async_load, "ip");
// 连接与断开
switch (type)
{
// 连接时...
case network_type_connect:
// 将客户端添加到服务器列表
ListRefresh = 1
ds_list_add( global.list_socket, sock );
break;
// 断开时...
case network_type_disconnect:
ListRefresh = 1
// 将断开的客户端的socket从服务器列表删除
var index = ds_list_find_index( global.list_socket, sock );
ds_list_delete(global.list_socket,index);
break;
}
}
其中sock和ListRefresh不是var临时变量,而是成员变量,因为我们在其他事件里会需要使用到这两个变量。ListRefresh = 1时,socket列表刷新,每当有玩家加入或者断开时,我们就刷新socket列表。
2018年02月07日 12点02分 13
level 11
楼主快更啊!
2018年02月07日 12点02分 14
level 3
犬酥咸饼 楼主
补充async_load,之前漏掉了两个。
buffer:我们读取buffer里的数据必然会用到
size:获得接收到的buffer大小(字节单位)
2018年02月07日 14点02分 15
哇!楼主更新了,顶楼主
2018年02月07日 17点02分
1 2 尾页