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

news2024/11/28 10:59:34

目录

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类,代码实现如下。

# client.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,那就直接忽略这个玩家数据即可。这样改的话,服务端代码可以更加简洁。

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 完善客户端代码 

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

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/88243.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

CentOS7中安装字体库中文字体

若存在中文乱码的情况,这是因为操作系统中没有安装中文字体。 安装字体库 yum install fontconfig -y 安装更新字体命令 yum install mkfontscale -y添加中文字体 # 新建目录 mkdir /usr/share/fonts/chinese # 切换到中文字体目录下,上传windows里宋…

浅谈人工智能生成内容(AIGC)

兴趣了解 [OpenAI ]人工智能绘画产品 DALLE: 在计算机上输入一句话,DALLE 就能够理解这句话、然后自动生成一幅意思相应的图像,且该图像是全网首发、独一无二。[谷歌 ] 5400 亿参数大模型 PaLM: PaLM 的文本理解能力与逻辑推理能力大幅提升,…

[附源码]Nodejs计算机毕业设计基于web的校园闲置物品交易系统Express(程序+LW)

该项目含有源码、文档、程序、数据库、配套开发软件、软件安装教程。欢迎交流 项目运行 环境配置: Node.js Vscode Mysql5.7 HBuilderXNavicat11VueExpress。 项目技术: Express框架 Node.js Vue 等等组成,B/S模式 Vscode管理前后端分…

PyTorch中利用LSTMCell搭建多层LSTM实现时间序列预测

前言 前面已经写过不少时间序列预测的文章: 深入理解PyTorch中LSTM的输入和输出(从input输入到Linear输出)PyTorch搭建LSTM实现时间序列预测(负荷预测)PyTorch中利用LSTMCell搭建多层LSTM实现时间序列预测PyTorch搭建…

为什么AI距离智能越来越远?

2021年讨论了人机混合智能里的深度态势感知和人的算计与机器的计算如何结合的问题。之后有一位朋友问了我五个问题。第一,关于数学和逻辑的关系问题。这个问题是百年来数学的基础问题,迄今为止似乎没有定论。从实用主义角度说,“把数学等同于…

企业在项目中采用工时管理系统的好处

在如今疫情的影响下,不少企业面对经济形势愈发严峻的情况下,对项目员工工时的管理也是越来越注重。如何在确保企业正常运转的前提下提升企业发展空间,人员降低工作成本呢?根据目前研究表明,很多企业都选择使用项目工时…

Android Kotlin使用AspectJ进行AOP面向切面编程

前言 什么是面向切面编程?首先我们来了解下两个概念: OOP(面向对象编程):针对业务处理过程的实体及其属性和行为进行抽象封装,以获得更加清晰高效的逻辑单元划分。 AOP(面向切面编程):则是针对业务处理过程…

html好看的生日祝福,生日表白(源码)

文章目录1.设计来源1.1 主界面1.2 秘密基地1.3 甜言蜜语2.效果和源码2.1 动态效果2.2 源代码2.3 自定义背景图片代码2.4 自定义每次生日记录代码2.5 自定义背景音乐代码源码下载作者:xcLeigh 文章地址:https://blog.csdn.net/weixin_43151418/article/de…

Java实现Google第三方登录

文章目录前言一、了解OAuth2.0二、注册开发者账号1.登录开发者平台2.创建应用三、代码实现1.实现流程1.点击登录2.接受回调中的code获取accessToken3.获取用户信息2.注意事项前言 Google API 使用 OAuth 2.0 协议进行身份验证和授权。Google 支持常见的 OAuth 2.0 场景&#x…

高分子点击试剂DBCO-PEG-Hydrazide,二苯并环辛炔-聚乙二醇-酰基

一、试剂基团反应特点(Reagent group reaction characteristics): DBCO-PEG-Hydrazide属于高分子点击试剂,“点击化学"一般由叠氮化物(azide)和炔烃(alkyne)作用形共价键&#…

老港综合填埋场二期配套渗滤液工程电能管理系统的设计和应用-Susie 周

1、概述 本项目为老港综合填埋场二期配套渗滤液工程电能管理系统。根据配电系统管理的要求,需要对(老港综合填埋场二期配套渗滤液工程电能管理系统项目的配电柜进行电能管理,以保证用电的安全、可靠。 Acrel-3000电能管理系统充分利用了现代…

Mybatis源码分析(一)Mybatis 基本使用

目录一 知识回顾1.1 简介1.2 其他二 基本使用官网:mybatis – MyBatis 3 | 简介 一 知识回顾 1.1 简介 MyBatis 是一款优秀的持久层框架,它支持自定义 SQL、存储过程以及高级映射。MyBatis 免除了几乎所有的 JDBC 代码以及设置参数和获取结果集的工作…

图片怎么转换成excel文档?

当我们创建excel文档中,里面无疑是需要各种表格内容,而如果是我们一个一个编辑起来,这就会比较繁琐。而现在许多需求可以通过网络很容易地得到满足。比如有把图片转换成excel表格的需求。下载一个小工具,这就相当方便了&#xff0…

不愧是阿里资深架构师,这本“分布式架构笔记”写得如此透彻明了

前言: Mybatis 是一款优秀的持久层框架。其封装了 JDBC 操作, 免去了开发人员编写 JDBC 代码以及设置参数和获取结果集的重复性工作。通过编写简单的 XML 或 Java 注解即可映射数据库 CRUD 操作。本文介绍的是阿里资深架构师十年经验整理,My…

JAVA 中的注解可以继承吗?

前言 注解想必大家都用过,也叫元数据,是一种代码级别的注释,可以对类或者方法等元素做标记说明,比如 Spring 框架中的Service,Component等。那么今天我想问大家的是类被继承了,注解能否继承呢?…

基于springboot在线答疑系统

教师权限:首页、个人中心、疑难解答管理、试卷管理、试题管理、考试管理。 学生权限;首页、个人中心、问题发布管理、疑难解答管理、考试管理等功能模块的管理维护等操作,系统结构图如下图4-1所示。 图4-1 系统功能图 截图 目 录 摘 要 I …

[附源码]Node.js计算机毕业设计扶贫产品展销平台小程序Express

项目运行 环境配置: Node.js最新版 Vscode Mysql5.7 HBuilderXNavicat11Vue。 项目技术: Express框架 Node.js Vue 等等组成,B/S模式 Vscode管理前后端分离等等。 环境需要 1.运行环境:最好是Nodejs最新版,我…

matlab 的help没了

前两天还正常用,今天输入help 关键字 回复是没有相关的内容。 解决办法: 按照如下选择就行了 然后输入 help help 就会有显示了 help - Help for functions in Command Window This MATLAB function displays the help text for the functionalit…

大数据MapReduce学习案例:倒排索引

文章目录一,案例分析(一)倒排索引介绍(二)案例需求二,案例实施(一)准备数据文件(1)启动hadoop服务(2)虚拟机上创建文本文件&#xff0…

数据结构双向链表

双向链表也叫双链表,是链表的一种,它的每个数据结点中都有两个指针,分别指向直接后继和直接前驱。所以,从双向链表中的任意一个结点开始,都可以很方便地访问它的前驱结点和后继结点。一般我们都构造双向循环链表。 那…