岁月不居,时节如流,转眼已来到了这个系列的最后一篇
上回说到采用socket+多线程的网络模型来实现服务器与客户端的通信,今天我们就在这个基础上完整的实现多人在线的游戏版本
我们会把之前的1~5节所讲的内容全都用上,见证一款可以玩的游戏的诞生
![](https://img.haomeiwen.com/i11132565/1c8a8ad1d36749a8.gif)
再回顾一下服务器与客户端的逻辑分工:
服务器:
1.与客户端建立网络连接,并为该玩家创建一个robot(副线程)
2.接收客户端陆续发过来的消息,将消息解析成玩家的具体操作(副线程)
3.在自己的游戏世界中模拟robot与子弹的变化(主线程)
4.将游戏世界中发生的事时刻同步给所有的客户端(主线程)
客户端:
1.与服务器建立连接(主线程)
2.捕捉玩家的键盘行为,描述成一组消息发送给服务器(主线程)
3.接收服务器发送的同步消息,修改自己存储的游戏世界(副线程)
4.将游戏世界在屏幕上绘制出来(主线程)
我们可以与第4篇的“单机版”对比一下
单机版是相当于是“客户端”捕捉玩家键盘行为后自己模拟世界的变化
网络版是客户端将行为发给服务器,由服务器模拟,再由服务器告诉客户端世界的变化
从上面的描述中可以看到客户端与服务器之间有两种消息交互:
1.客户端到服务器:
玩家的移动指令、发射指令
2.服务器到客户端:
所有玩家的位置信息、所有子弹的位置信息
为了区分不同的玩家,服务器在创建robot时会对应生成一个id来标志这个玩家,并把这个id回传给客户端
客户端在之后的所有指令消息中会附带上这个id
下面我们来看关键部分的代码实现
服务器与客户端建立网络连接,并为该玩家创建一个robot
def accept_client():
global total_robot_num,lock,g_need_syna
while True:
client, _ = g_socket_server.accept()# 阻塞,等待客户端连接
g_conn_pool.append(client)
thread = Thread(target=message_handle, args=(client,))#创建线程接收后续指令消息
thread.setDaemon(True)
thread.start()
lock.acquire()#线程锁加锁
total_robot_num += 1
robot = Robot(total_robot_num)#创建玩家控制的robot
robot_list.append(robot)
g_need_syna = 1#告诉主线程世界发生了变化,需要同步
res_msg = "accept "+str(total_robot_num)
client.send(bytes(res_msg,'utf-8'))#将robot的id发送给客户端
lock.release()#线程锁释放
我们在这个函数中加入了一个新的东西“lock”,这是一个线程锁。
这是由于游戏世界(所有的robot,所有的子弹)只有一份,是公用的。而主线程和副线程都会对游戏世界做修改,为了避免几个线程同时修改世界(可以类比几个人都在抢一张火车票),在一个线程获得了修改权限时,需要把游戏世界锁起来,别的线程只有等它修改结束了才能继续修改。
而客户端收到这个“accep xxx"之后,就会把xxx作为自己的id记下来
def RecvMsg(client):
global lock,robot_list,bullet_list,self_robot_id
while True:
data = client.recv(8192)
data = data.decode()
str_list = data.split('\n')
if(len(str_list)):
first_line = str_list[0]
fl = first_line.split(' ')
if(len(fl)):
if(fl[0]=="accept"):#连接成功
self_robot_id = int(fl[1])
再看客户端发送操作指令的消息:
key_press = pygame.key.get_pressed()
if(key_press[K_LEFT]):#按下方向键左
msg = str(self_robot_id) + " 3"#与服务器约定3表示向左移动一次
client.send(msg.encode("utf-8"))
以及服务器接收到指令后的处理:
def message_handle(client):
global lock,g_need_syna,total_bullet,RobotSize,BulletSize
while True:
data = client.recv(8192)
data = data.decode('utf-8')
if len(data) == 0:
client.close()
g_conn_pool.remove(client)#删除连接
else:
str_list = data.split(' ')
if(len(str_list)>=2):
robot_id = int(str_list[0])#id
move_dir = int(str_list[1])#移动移动指令
lock.acquire()
for rbt in robot_list:
if rbt.id == robot_id:#找到对应的机器人
rbt.Move(move_dir)#控制机器人移动
g_need_syna = 1
lock.release()
最后是服务器对整个游戏世界的同步:
syna_msg = []
syna_msg.append("robots\n")
for rbt in robot_list:#所有玩家的位置
syna_msg.append(str(rbt.id)+" ")
syna_msg.append(str(rbt.x)+" ")
syna_msg.append(str(rbt.y)+" ")
syna_msg.append(str(rbt.z)+" ")
syna_msg.append(str(rbt.dir)+"\n")
syna_msg.append("bullets\n")
for bul in bullet_list:#所有子弹的位置
syna_msg.append(str(bul.id)+" ")
syna_msg.append(str(bul.fa)+" ")
syna_msg.append(str(bul.x)+" ")
syna_msg.append(str(bul.y)+" ")
syna_msg.append(str(bul.z)+"\n")
syna_str = ''.join(syna_msg)
for clt in g_conn_pool:#发送给所有客户端
clt.sendall(bytes(syna_str,'utf-8'))
以及客户端收到同步的消息后需要对应修改自己记录的世界(代码太长就不贴了)
完整的代码可以从这里获取(代码细节还是挺多的,就不一一说明了。而且这只是我自己实现的方式,我对python也不是很熟,凑合着看吧)
服务器
客户端
至此整个游戏的初版已经完成。
后续可以对游戏进行贴图(美术换皮),魔改子弹和技能,以及玩家视野上做一些处理。目前已经在0.1版本中加入了追踪导弹的元素。
如果有时间的话会在GitHub上继续更新。也欢迎铁子们跟我一起交流嗷。
网友评论