《Python多人游戏项目实战》第二节 使用pickle模块序列化数据

Posted la_vie_est_belle

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了《Python多人游戏项目实战》第二节 使用pickle模块序列化数据相关的知识,希望对你有一定的参考价值。

目录

2.1 设置游戏窗口

2.2 实现人物移动的功能

2.3 编写服务端代码

2.4 完善客户端代码 

2.5 完整代码下载地址


在本节,笔者会带大家开发一个联机版的人物移动程序,示例如下:

在上一节,客户端和服务端通信的JSON数据中包含玩家的id,坐标以及颜色,通过这几个值我们就可以更新各个玩家的状态,因为玩家操控的其实就是一个简单的小方块。但假如玩家操控的是一个拥有更多属性的对象呢?我们当然也可以把这些属性放在JSON数据中,并在客户端和服务端之间传送。不过这个JSON数据构造起来其实挺麻烦的,万一属性很多,那这个JSON数据就会很长很乱,而且对象的某个属性被修改掉的话, 那我们也需要在客户端和服务端处理JSON数据的地方修改对应的键值,代码耦合性比较高。

既然我们需要更新玩家状态,而各个状态属性都保存在Player对象中,那干脆直接将Player对象作为数据进行传输就可以了,不需要再将各个属性提取出来作为JSON数据的键值,省了一大麻烦。Python有一个内置的pickle模块,它可以序列化几乎所有的Python数据类型:列表、字典、集合、类等。

本项目结构显示如下:

├── client.py                # 客户端代码

├── pics                       # 图片文件夹

│   └── walk.png          # 方位图

├── player.py               # 包含Player类

└── server.py               # 服务端代码

在client.py中我们一共导入了以下模块或库:

import sys
import pygame
import pickle
import socket
from player import Player
from random import randint

在player.py中我们一共导入了以下模块或库:

import pygame

在server.py中我们一共导入了以下模块或库:

import socket
import pickle
from player import Player
from threading import Thread

2.1 设置游戏窗口

跟第一节一样,我们先设置好游戏窗口的标题、大小以及背景等属性。

# client.py
class GameWindow:
    def __init__(self):
        self.width = 500
        self.height = 500
        self.window = self.init_window()

        self.pic = pygame.image.load(f"./pics/walk.png")    # 1
        frame_width = self.pic.get_width() // 4
        frame_height = self.pic.get_height() // 4
 
    def init_window(self):
        pygame.init()
        pygame.display.set_caption('移动方块')
        return pygame.display.set_mode((self.width, self.height))
 
    def update_window(self):
        self.window.fill((255, 255, 255))
        pygame.display.update()
 
    def start(self):
        clock = pygame.time.Clock()
 
        while True:
            clock.tick(15)                # 2
 
            for event in pygame.event.get():
                if event.type == pygame.QUIT:
                    pygame.quit()
                    sys.exit()
 
            self.update_window()
 
 
if __name__ == "__main__":
    game = GameWindow()
    game.start()

代码解释如下:

1. 加载pics文件夹中的walk.png方位图并保存到pic变量中。每一行有4帧,一共有4行,所以每一帧的宽度就是self.pic.get_width() // 4,每一帧的高度就是self.pic.get_height() // 4

2. 我们需要将游戏帧率设置为15(也可以更小),否则人物移动会显得非常地快,不自然。

2.2 实现人物移动的功能

当按下上下左右键时,人物会按照指定方向移动并显示对应方向的图片。

 首先编写Player类,代码实现如下。

# player.py
class Player:
    def __init__(self, p_id, x, y, frame_width, frame_height):
        self.id = p_id
        self.dis = 3
        self.x = x
        self.y = y

        self.frame_width = frame_width
        self.frame_height = frame_height
        self.frame_num = 0                                      # 1
        self.frame_rect = (self.frame_num * self.frame_width, 0 * self.frame_height,
                           self.frame_width, self.frame_height)

        self.current_dir = "下"                                  # 2
        self.last_dir = self.current_dir

    def move(self):                                             # 3
        keys = pygame.key.get_pressed()

        if keys[pygame.K_LEFT]:
            self.x -= self.dis
            self.current_dir = "左"                              # 4
            self.set_frame_rect(1)                              # 5

        elif keys[pygame.K_RIGHT]:
            self.x += self.dis
            self.current_dir = "右"
            self.set_frame_rect(2)

        elif keys[pygame.K_UP]:
            self.y -= self.dis
            self.current_dir = "上"
            self.set_frame_rect(3)

        elif keys[pygame.K_DOWN]:
            self.y += self.dis
            self.current_dir = "下"
            self.set_frame_rect(0)

        self.last_dir = self.current_dir

    def set_frame_rect(self, pic_row):
        self.frame_num += 1
        if self.current_dir != self.last_dir or self.frame_num > 3:
            self.frame_num = 0

        self.frame_rect = (self.frame_num * self.frame_width, pic_row * self.frame_height,
                           self.frame_width, self.frame_height)

    def draw(self, win, pic):                                    # 6
        win.blit(pic, (self.x, self.y), self.frame_rect)

代码解释如下:

1. frame_num变量表示当前运行到第几帧,0表示第一帧,3表示第4帧。方位图每一行只有4帧,所以frame_num最大值为3。frame_rect变量表示某一帧的矩形范围值,初始值设置为第1行的第1帧的矩形范围,也是下图中的红框部分。

2. current_dir变量表示玩家当前的行走方向,last_dir变量保存玩家上一次的移动方向,我们会在后面比较这两个变量的值,由此来判断玩家是否改变了方向。

3. move()函数用来改变玩家的坐标值,设定移动方向以及显示对应的移动图片。

4. 当左键被按下后,我们将current_dir的值设置为"左",其他方向同理。

5. set_frame_rect()函数用来显示对用的移动图片,传入的数字表示方位图的行数,0表示第1行。当current_dir和last_dir的值不一样时,说明玩家改变了移动方向,那我们就要把frame_num设置为0,表示从第1帧开始显示。如果current_dir和last_dir的值一样,表示玩家朝着某一方向在不断移动,但当frame_num大于3时(说明第4帧已经显示过了),我们就要重新从第1帧开始显示,也就是把frame_num设置为0。通过传入的行数和帧数,我们就能够轻松算出某一帧人物所在的矩形范围。

6. 在屏幕上显示方位图中的某一帧,在这里我们要从外部传入win对象和pic对象,因为pickle无法序列化pygame的对象。

现在我们在GameWindow类中实例化Player对象,让人物显示在游戏窗口上。

# client.py
class GameWindow:
    def __init__(self):
        ...

        self.player = Player(p_id=None,                    # 1
                             x=randint(0, self.width-frame_width),
                             y=randint(0, self.height-frame_height),
                             frame_width=frame_width,
                             frame_height=frame_height)

    ...

    def update_window(self):
        self.window.fill((255, 255, 255))

        self.player.move()                                  # 2
        self.player.draw(self.window)

        pygame.display.update()

代码解释如下:

1. 实例化一个Player对象并传入相关参数。因为还没有连接到服务端,所以p_id先设置为None。x和y坐标是随机的,减去frame_width和frame_height是为了让人物显示在窗口内。

2. 在update_window()函数中不断更新玩家的移动方向和帧图。

运行结果如下:

2.3 编写服务端代码

服务端的逻辑很简单,就是等待客户端玩家发送自身的数据,然后把其他所有玩家的数据返回到客户端。或者我们可以修改一点,就是将所有(包括自身)的数据全部返回,接着在客户端根据玩家id值更新玩家状态即可。如果服务端发送过来的数据中某个玩家id等于玩家自身的id,那就直接忽略这个玩家数据即可。这样改的话,服务端代码可以更加简洁。

# server.py
import socket
import pickle
from player import Player                               # 1
from threading import Thread


class Server:
    def __init__(self):
        self.port = 5000
        self.host = "127.0.0.1"
        self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.players_data =                           # 2

    def start(self):
        self.get_socket_ready()
        self.handle_connection()

    def get_socket_ready(self):
        self.sock.bind((self.host, self.port))
        self.sock.listen()
        print("服务器已准备接收客户端连接")

    def handle_connection(self):
        while True:
            conn, addr = self.sock.accept()
            print(f"接收到来自addr的连接")
            conn.send(str(id(conn)).encode("utf-8"))
            Thread(target=self.handle_message, args=(conn, )).start()

    def handle_message(self, conn):
        while True:
            try:
                data = conn.recv(2048)
                if not data:
                    print("未接收到数据,关闭连接")
                    self.players_data.pop(str(id(conn)))
                    conn.close()
                    break
                else:
                    data = pickle.loads(data)           # 3
                    self.update_one_player_data(data)   # 4
                    conn.sendall(pickle.dumps(self.get_other_players_data(data["id"])))  # 5
            except Exception as e:
                print(repr(e))
                break

    def update_one_player_data(self, data):
        key = data["id"]
        value = data["player"]
        self.players_data[key] = value

    def get_other_players_data(self, current_player_id):
        data = 
        for key, value in self.players_data.items():
            if key != current_player_id:
                data[key] = value
        return data


if __name__ == '__main__':
    server = Server()
    server.start()


代码解释如下:

1. 因为传送的数据中包含Player对象,所以我们需要从player.py中导入Player类,服务端会在数据处理过程中使用到Player类。

2. player_data变量用来保存所有玩家的数据,结构如下所示。


    "玩家id": Player对象

3. 用pickle.loads()函数来加载序列化后的数据,代替了原来的json.loads()。

4. update_one_player_data()函数会将玩家发送过来的数据保存到player_data变量中。

5. 调用pickle.dumps()函数将其他所有玩家的数据序列化,并发送到客户端。

运行结果如下:

2.4 完善客户端代码 

最后一步就是在客户端中添加发送数据到服务端的相关代码,并且将服务端发送过来的其他玩家的数据更新到窗口上。

# client.py
class GameWindow:
    def __init__(self):
        ...

        self.port = 5000
        self.host = "127.0.0.1"
        self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

        self.connect()

    ...

    def connect(self):
        self.sock.connect((self.host, self.port))
        self.player.id = self.sock.recv(2048).decode("utf-8")

    def send_player_data(self):                 # 1
        data = 
            "id": self.player.id,
            "player": self.player
        
        self.sock.send(pickle.dumps(data))
        return self.sock.recv(2048)

    def update_window(self):                    # 2
        self.window.fill((255, 255, 255))

        self.player.move()
        self.player.draw(self.window, self.pic)

        other_players_data = pickle.loads(self.send_player_data())
        self.update_other_players_data(other_players_data)

        pygame.display.update()

    def update_other_players_data(self, data):  # 3
        for player in data.values():
            player.draw(self.window, self.pic)
   
    ...

代码解释如下:

1. send_player_data()函数用来将当前玩家的数据发送到服务端,并返回从服务端接收到的其他玩家的数据。发送的数据需要被pickle.dumps()函数序列化。

2. 调用pickle.loads()加载服务端发送过来的其他所有玩家的数据,然后交给update_other_players_data()函数将这些玩家的数据更新到窗口上。

3. 通过data.values()方法获取所有player对象,然后调用draw()方法就可以了,相比第一节中的代码简洁了很多。

现在先运行服务端程序,然后再运行任意数量的客户端程序,笔者这里就打开三个客户端。我们发现,每个游戏窗口上都会出现一个人物,在任何一个窗口上进行移动,其他两个窗口也会立即更新人物的移动状态。运行结果如下:

2.5 完整代码下载地址

链接:https://pan.baidu.com/s/1c4HeBgJSZKT7g7V41LybJg  

密码:3utl

以上是关于《Python多人游戏项目实战》第二节 使用pickle模块序列化数据的主要内容,如果未能解决你的问题,请参考以下文章

《Python多人游戏项目实战》第一节 简单的方块移动

《Python多人游戏项目实战》第一节 简单的方块移动

《Python多人游戏项目实战》第四节 实现房间功能

《Python多人游戏项目实战》第四节 实现房间功能

《Python多人游戏项目实战》第五节 断线重连

《Python多人游戏项目实战》第五节 断线重连