一.运行效果



二.介绍

源码github:https://github.com/zxf20180725/pygame-online-demo.git
<https://github.com/zxf20180725/pygame-online-demo.git>

这只是一个简单的联网程序Demo,代码有很多不严谨的地方,仅当抛砖引玉了。

运行客户端程序,会随机取一个名字进入游戏。使用wsad移动头像(蓝葵~)。

三.代码解析

注意,这里只会贴出部分核心代码,完整代码请在上面的github链接中下载。

全局部分的代码,这些都有注释了,具体作用,后面会讲到。foxyball.cn是我的服务器域名,这一年内应该都是有效的。
import random import sys import time from random import randint from threading
import Thread import pygame import socket # 导入 socket 模块 from base import
Protocol ADDRESS = ('127.0.0.1', 8712) # ('foxyball.cn', 8712) #
如果服务端在本机,请使用('127.0.0.1', 8712) WIDTH, HEIGHT = 640, 480 # 窗口大小 g_font = None
g_screen = None # 窗口的surface g_sur_role = None # 人物的role g_player = None #
玩家操作的角色 g_other_player = [] # 其他玩家 g_client = socket.socket() # 创建 socket 对象
看一个程序的代码,应该从它的入口开始看。
if __name__ == '__main__': # 初始化 init_game() # 游戏循环 main_loop()
入口很简单,就调用了两个函数,那么我们先看看init_game()做了什么。
def init_game(): """ 初始化游戏 """ global g_screen, g_sur_role, g_player, g_font #
初始化pygame pygame.init() pygame.display.set_caption('网络游戏Demo') g_screen =
pygame.display.set_mode([WIDTH, HEIGHT]) g_sur_role =
pygame.image.load("./role.png").convert_alpha() # 人物图片 g_font =
pygame.font.SysFont("fangsong", 24) # 初始化随机种子 random.seed(int(time.time())) #
创建角色 # 随机生成一个名字 last_name = ['赵', '钱', '孙', '李', '周', '吴', '郑', '王', '冯', '陈',
'褚', '卫', '蒋', '沈', '韩', '杨', '朱', '秦', '尤', '许', '何', '吕', '施', '张', '孔', '曹',
'严', '华', '金', '魏', '陶', '姜', '戚', '谢', '邹', '喻', ] first_name = ['梦琪', '忆柳',
'之桃', '慕青', '问兰', '尔岚', '元香', '初夏', '沛菡', '傲珊', '曼文', '乐菱', '痴珊', '孤风', '雅彤',
'宛筠', '飞松', '初瑶', '夜云', '乐珍'] name = random.choice(last_name) +
random.choice(first_name) print("你的昵称是:", name) g_player = Role(randint(100,
500), randint(100, 300), name) # 与服务器建立连接 g_client.connect(ADDRESS) # 开始接受服务端消息
thead = Thread(target=msg_handler) thead.setDaemon(True) thead.start() #
告诉服务端有新玩家 send_new_role()
从与服务器建立连接开始讲吧(28行)。如果对python的socket不太熟悉的话,可以先看看这两篇文章:
https://blog.csdn.net/qq_39687901/article/details/81531101
<https://blog.csdn.net/qq_39687901/article/details/81531101>,
https://blog.csdn.net/qq_39687901/article/details/81536641
<https://blog.csdn.net/qq_39687901/article/details/81536641>
,g_client是一个socket对象,与指定的服务端建立连接。


接收服务端消息部分,我新开了一个线程进行处理(因为recv是阻塞线程的)。处理服务端消息的函数是msg_handler,这个函数稍后再讲,我们继续往下看send_new_role函数。
def send_new_role(): """ 告诉服务端有新玩家加入 """ # 构建数据包 p = Protocol()
p.add_str("newrole") p.add_int32(g_player.x) p.add_int32(g_player.y)
p.add_str(g_player.name) data = p.get_pck_has_head() # 发送数据包
g_client.sendall(data)
Protocol是我们自定义的游戏数据包协议,关于Protocol的设计思路都在这篇文章:
https://blog.csdn.net/qq_39687901/article/details/81541967
<https://blog.csdn.net/qq_39687901/article/details/81541967>


这里,我们构造了一个名字叫“newrole”的数据包,并且加入了玩家的信息(坐标和昵称),最后把这个数据包发送给服务端。这个“newrole”的作用就是告诉服务端有一个新玩家加入游戏啦,然后服务端又会告诉其他玩家有个新玩家加入了(这就实现了可以在窗口里看到其他玩家的功能)。我会在下一篇文章详细的讲解服务端的设计,这里就不多说了。

回到我们的程序入口来,接下来就该执行main_loop啦。
def main_loop(): """ 游戏主循环 """ while True: # FPS=60 pygame.time.delay(32) #
逻辑更新 update_logic() # 视图更新 update_view()

每个游戏都必不可少的游戏主循环。循环里很简单,就调用了3个函数。pygame.time.delay(32)让每次循环间隔32毫秒,也就是说每秒循环执行60次左右。然后就是逻辑更新和视图更新了,这两个函数请尽可能的解耦。

那我们继续看逻辑更新,这个demo的游戏逻辑很简单,就是用wasd控制角色移动。
def update_logic(): """ 逻辑更新 """ # 事件处理 handler_event() def handler_event(): #
事件处理 for event in pygame.event.get(): if event.type == pygame.QUIT: sys.exit()
elif event.type == pygame.KEYDOWN: if event.key == pygame.K_w: g_player.y -= 5
elif event.key == pygame.K_s: g_player.y += 5 elif event.key == pygame.K_a:
g_player.x -= 5 elif event.key == pygame.K_d: g_player.x += 5 send_role_move()
# 告诉服务器,自己移动了
角色每次移动之后,要把最新的坐标告诉给服务端,服务端再把这个角色的最新坐标发送给其他客户端,这样其他客户端就能看到你在移动了。
send_role_move函数就是把当前坐标发送给服务端。
def send_role_move(): """ 发送角色的坐标给服务端 """ # 构建数据包 p = Protocol()
p.add_str("move") p.add_int32(g_player.x) p.add_int32(g_player.y) data =
p.get_pck_has_head() # 发送数据包 g_client.sendall(data)
回到我们的游戏主循环,继续看视图更新。
def update_view(): """ 视图更新 """ g_screen.fill((0, 0, 0)) # 画角色
g_screen.blit(g_player.sur_name, (g_player.x, g_player.y - 20))
g_screen.blit(g_sur_role, (g_player.x, g_player.y)) # 画其他角色 for r in
g_other_player: g_screen.blit(r.sur_name, (r.x, r.y - 20))
g_screen.blit(g_sur_role, (r.x, r.y)) # 刷新 pygame.display.flip()

其中,g_other_player是一个存着其他在线玩家的列表。那么这个列表中的内容是从哪里来的呢?内容当然是从服务端发过来的。还记得本文最开始提到的新开一个线程处理服务端消息吗?就是那个msg_handler函数,现在来研究研究它。
def msg_handler(): """ 处理服务端返回的消息 """ while True: bytes = g_client.recv(1024)
# 以包长度切割封包 while True: # 读取包长度 length_pck = int.from_bytes(bytes[:4],
byteorder='little') # 截取封包 pck = bytes[4:4 + length_pck] # 删除已经读取的字节 bytes =
bytes[4 + length_pck:] # 把封包交给处理函数 pck_handler(pck) # 如果bytes没数据了,就跳出循环 if
len(bytes) == 0: break
外层的while循环是用来接收服务端的消息,内层的while循环是用来切割数据包的(tcp粘包分包了解一下)。但这里还有个问题,是我在代码里没有去处理的。
那就是tcp分包问题,这里内层while循环只解决了粘包。
分包的问题,在以后的文章中会讲。内层while循环的逻辑为什么要这么写,大家还是去看看我之前发的那篇文章吧。
https://blog.csdn.net/qq_39687901/article/details/81541967
<https://blog.csdn.net/qq_39687901/article/details/81541967>

数据包切割好了之后,就调用pck_handler函数处理数据包。
def pck_handler(pck): p = Protocol(pck) pck_type = p.get_str() if pck_type ==
'playermove': # 玩家移动的数据包 x = p.get_int32() y = p.get_int32() name = p.get_str()
for r in g_other_player: if r.name == name: r.x = x r.y = y break elif pck_type
== 'newplayer': # 新玩家数据包 x = p.get_int32() y = p.get_int32() name = p.get_str()
r = Role(x, y, name) g_other_player.append(r) elif pck_type == 'logout': # 玩家掉线
name = p.get_str() for r in g_other_player: if r.name == name:
g_other_player.remove(r) break

我们这个小demo一共就设计了三个协议类型,"playermove"、"newplayer"和"logout"。"playermove"是在其他玩家移动的时候,服务端给我们的,让我们更新其他玩家的位置(这样就能看到其他玩家的移动效果了)。剩下的两个就不用多说了吧。

四.总结

网络流程:

登录流程:

1.客户端登录,发送"newrole"数据包给服务端

2.服务端收到"newrole"数据包,然后发送"newplayer"数据包给其他客户端

3.其他客户端收到"newplayer",向g_other_player列表中添加一个玩家

 

移动流程:

1.客户端移动,发送"move"数据包给服务端

2.服务端收到"move"数据包,然后发送"playermove"数据包给其他客户端

3.其他客户端收到"playermove",更新g_other_player的相关数据

 

下线流程:

1.服务端检测到有客户端掉线,发送"logout"数据包给其他在线客户端

2.其他客户端收到"playermove",删除g_other_player中掉线的玩家

友情链接
KaDraw流程图
API参考文档
OK工具箱
云服务器优惠
阿里云优惠券
腾讯云优惠券
华为云优惠券
站点信息
问题反馈
邮箱:[email protected]
QQ群:637538335
关注微信