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

news2025/1/20 5:51:07

目录

1.1 设置游戏窗口

1.2 绘制一个方块

1.3 编写服务端代码

1.4 完善客户端代码

1.5 完整代码下载地址


在本节,我们将通过一个简单的方块移动程序进入多人联机游戏的大门。每个玩家打开游戏窗口后都可以控制一个方块,当某个玩移动方块后,其余玩家的窗口上会自动更新该玩家的方块位置。运行示例如下:

本项目结构显示如下:

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

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

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

import sys
import json
import pygame
import socket
from random import randint

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

import json
import socket
from threading import Thread

1.1 设置游戏窗口

我们首先要设置好游戏窗口的相关属性,比如窗口标题、宽高以及背景等等。

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

    def init_window(self):                      # 1
        pygame.init()
        pygame.display.set_caption('移动方块')
        return pygame.display.set_mode((self.width, self.height))

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

    def start(self):                            # 3
        clock = pygame.time.Clock()

        while True:
            clock.tick(60)

            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. init_window()函数用来初始化pygame窗口的相关属性,我们在该函数中设置了窗口的标题和大小。

2. update_window()函数用来更新窗口,将窗口背景设置为白色,后续我们也会在该函数中不断更新玩家的状态。

3. start()函数是游戏入口,重点是要调用update_window()函数不断更新窗口。

运行结果如下:

1.2 绘制一个方块

游戏窗口设置好了之后,我们就可以往窗口上添加方块了。一个方块代表一个玩家,我们就用Player类来实现方块的相关功能。

class Player:
    def __init__(self, win, p_id, x, y, color):
        self.win = win              # 1
        self.id = p_id              # 2
        self.dis = 3                # 3
        self.x = x
        self.y = y
        self.width = 100
        self.height = 100
        self.color = color

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

        if keys[pygame.K_LEFT]:
            self.x -= self.dis
        elif keys[pygame.K_RIGHT]:
            self.x += self.dis
        elif keys[pygame.K_UP]:
            self.y -= self.dis
        elif keys[pygame.K_DOWN]:
            self.y += self.dis

    def draw(self):                 # 5
        pygame.draw.rect(self.win, self.color, (self.x, self.y, self.width, self.height))

代码解释如下:

1. Player类接收一个游戏窗口实例,后面我们会在GameWindow类中实例化一个Player对象并传入游戏窗口实例的。

2. 每个玩家都会拥有一个id,该id会在服务端生成并从服务端获取过来。

3. dis变量为方块每次移动的距离。方块的宽高都为100,坐标和颜色是随机的。

4. 根据按键改变方块位置。

5. 将方块绘制到窗口上。

玩家类已经编写好了,接下来就是要在游戏窗口上添加玩家了。

# client.py
class GameWindow:
    def __init__(self):
        ...
        self.player = Player(win=self.window,           # 1
                             p_id=None,
                             x=randint(0, self.width - 100),
                             y=randint(0, self.height - 100),
                             color=(randint(0, 200), randint(0, 200), randint(0, 200)))

    ...

    def update_window(self):
        self.window.fill((255, 255, 255))
        self.player.move()                              # 2
        self.player.draw()
        pygame.display.update()
    
    ...

代码解释如下:

1. 实例化一个Player对象并传入相关参数。因为还没有连接到服务端,所以p_id先设置为None。x和y坐标是随机的,减去100(也就是方块的宽高)是为了让方块显示在窗口内。之所以将颜色值设定在0-200之间,是为了防止方块和窗口背景颜色太接近。假如方块颜色特别接近白色,那和窗口背景就混在一起,很难辨别了。

2. 在update_window()函数中调用Player实例对象的move()和draw()方法,不断在游戏窗口中更新自身的状态。

运行结果如下:

1.3 编写服务端代码

服务端的代码逻辑很简单,就是创建套接字等待客户端连接,然后将各个客户端发送过来的玩家数据保存起来,整理之后再发送出去。代码编写如下:

# server.py
import json
import socket
from threading import Thread


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

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

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

    def handle_connection(self):        # 5
        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):     # 6
        while True:
            try:
                data = conn.recv(2048)
                if not data:
                    print("未接收到数据,关闭连接")
                    self.players_data.pop(str(id(conn)))
                    conn.close()
                    break
                else:
                    data = json.loads(data.decode("utf-8"))
                    self.update_one_player_data(data)
                    conn.sendall(json.dumps(self.get_other_players_data(data["id"])).encode("utf-8"))
            except Exception as e:
                print(repr(e))
                break

    def update_one_player_data(self, data):
        key = data["id"]
        pos = data["pos"]
        color = data["color"]
        self.players_data[key] = {"pos": pos, "color": color}

    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. 服务端监听的地址为127.0.0.1:5000,所以后续再客户端中编写代码时要连接到这个地址。

2. players_data是一个字典变量,用来存储各个玩家的数据。该字典的各个键是玩家id,值包含玩家的位置和颜色。示例如下:

{
    "玩家id": {
        "pos" [x坐标值, y坐标值],
        "color": (r, g, b)
    }
}

3. start()函数是程序入口。

4.  在get_socket_ready()函数中,我们让套接字绑定监听了127.0.0.1:5000,准备就绪。

5. 如果服务端收到了来自客户端的连接,那就会将str(id(conn))作为玩家id值发送到客户端,而客户端也会将该值保存到Player对象的id属性中。为了不让连接堵塞,我们使用多线程技术来处理后续的消息通信。

6. 在handle_message()函数中,我们不断接收来自客户端的消息。如果客户端没有发送消息过来(data为空),说明客户端已经被关闭,玩家离开游戏了,那我们就要从players_data变量中删除对应玩家的数据并关闭套接字。如果有消息发送过来(data不为空),那么我们就将该玩家的数据保存或更新到player_data变量中,通过update_one_player_data()函数实现。最后将其他玩家的数据全部发送给该玩家,好让客户端窗口更新其他玩家的位置信息,其他玩家的数据通过get_other_players_data()函数获取。

简而言之:玩家A发送自身数据到服务端,服务端保存并返回除A之外的其他所有玩家的数据。玩家B发送自身数据到服务端,服务端保存并返回除B之外的其他所有玩家的数据。

注:或者也可以这样通信,玩家A发送自身数据到服务端,服务端将该数据发送给其他所有玩家。玩家B发送自身数据到服务端,服务端将该数据发送给其他所有玩家。这种通信方式有个缺点,就是服务端的发送的消息次数会很多。假如现在有10个玩家,当每个玩家发送1次数据到服务端,服务端还需要发送9次将数据同步给其他玩家。

运行结果如下:

1.4 完善客户端代码

最后一步就是要在客户端程序中添加和服务端通信的代码。客户端会把玩家数据发送到服务端,然后把服务端接收过来的其他玩家的数据更新到窗口上。代码编写如下:

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

        self.connect()                  # 2
        self.other_players_dict = {}    # 3

    ...

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

    def send_player_data(self):         # 4
        data = {
            "id": self.player.id,
            "pos": [self.player.x, self.player.y],
            "color": self.player.color
        }
        self.sock.send(json.dumps(data).encode("utf-8"))
        return self.sock.recv(2048*2).decode()

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

        self.player.move()
        self.player.draw()

        other_players_data = json.loads(self.send_player_data())
        self.update_other_players_data(other_players_data)
        self.delete_offline_players(other_players_data)

        pygame.display.update()

    def update_other_players_data(self, data):  # 6
        for key, value in data.items():
            if not self.other_players_dict.get(key):
                self.add_one_player(key, value)
            else:
                pos = value["pos"]
                self.other_players_dict[key].x = pos[0]
                self.other_players_dict[key].y = pos[1]
                self.other_players_dict[key].draw()

    def add_one_player(self, player_id, value): # 7
        pos = value["pos"]
        color = value["color"]
        self.other_players_dict[player_id] = Player(self.window, player_id, pos[0], pos[1], color)

    def delete_offline_players(self, data):     # 8
        new_dict = {}
        for key in self.other_players_dict.keys():
            if data.get(key):
                new_dict[key] = self.other_players_dict[key]
        self.other_players_dict = new_dict

    ...

代码解释如下:

1. 客户端要连接到127.0.0.1:5000,即服务端监听的地址。

2. 在connect()函数中,我们让客户端和服务端进行了连接。连接后,服务端会发送str(id(conn))作为玩家id,我们将该值保存到Player对象的id属性中。

3. other_players_dict字典变量用来保存其他所有玩家的数据,字典的键是玩家id,值是代表该玩家的Player对象,格式如下所示。

{
    "玩家id": Player实例对象
}

4. send_player_data()函数用来将当前玩家的数据发送到服务端,并返回从服务端接收到的其他玩家的数据。

5. 在update_window()函数中,我们不仅要更新当前玩家的状态,还要更新其他所有玩家的状态。

6.&7. 在update_other_players_data()函数中,我们分析从服务端接收回来的数据,如果出现一个新的玩家id,那么就实例化一个Player对象并保存到other_players_dict字典变量中。如果该玩家id已存在,则更新该玩家的位置即可。

8. 如果某个玩家退出了游戏,那从服务端接收回来的数据中,肯定会少一个玩家id。但是该玩家id之前又已经保存到other_players_dict字典变量中,所以我们每次还要更新下other_players_dict,把不需要的玩家id给去掉。

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

1.5 完整代码下载地址

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

密码:75vs

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

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

相关文章

面试八股-Java并发

1.线程 和进程区别: 进程:独立地址空间 代码、数据、堆栈 资源分配基本单位线程:共享地址空间 线程ID、指令指针、寄存器集合和堆栈 调度分派基本单位 1.1.使用 实现Runable接口,run方法为实际逻辑实现Callable接口&#xff0…

离散数学与组合数学-数理逻辑-02谓词演算及其形式系统

文章目录第二章 谓词演算及其形式系统2.1 个体谓词和量词2.1.1 个体谓词演算永真式谓词公式的前束范式一阶谓词演算形式系统谓词逻辑的等值演算与推理第二章 谓词演算及其形式系统 2.1 个体谓词和量词 2.1.1 个体 个体常元(constants):确定的个体用a,b,ca,b,ca,b,c等小写字母…

羟基聚乙二醇叠氮 HO-PEG-N3/Azide的结构式

羟基聚乙二醇叠氮(HO-PEG-N3)是异双功能PEG衍生物之一。叠氮化物在铜离子催化的水溶液中与炔基有效反应。炔烃和叠氮化物之间的1,3-偶极环加成反应是一种高产率的点击化学反应,可实现两个相应分子的高效结合。叠氮化物也可以与应变促进的环辛炔反应,不需…

Pr:使用作品

利用作品 Production,可将大型复杂工作流拆分为多个可管理的 Pr 项目,并可在作品内跨项目、跨平台(macOS 和 Windows)引用媒体,无论这些资源是在本地存储或是共享的网络存储,作品可让一切保持同步。作品&am…

真希望你也明白runtime.Map和sync.Map

Map 官方介绍 One of the most useful data structures in computer science is the hash table. Many hash table implementations exist with varying properties, but in general they offer fast lookups, adds, and deletes. Go provides a built-in map type that imple…

Grafana监控大屏配置参数介绍(二)

Grafana 系列文章,版本:OOS v9.3.1 Grafana 的介绍和安装Grafana监控大屏配置参数介绍(一)Grafana监控大屏配置参数介绍(二) 上一篇文章已经介绍了图表可视化配置部分的 Panel options、Tooltip、Legend 3类…

音视频行业大势如何,优势在哪?

电信行业的变革: 从1G语音、2G短信、3G图片语音、4G视频到5G未来可期的新时代,见证了音视频行业的磅礴发展。 技术更新慢且门槛高 技术更新慢,技术门槛高,大部分技术沿用至今,依然保持生命力,技术人员成型…

http协议和websocket协议

http协议 HTTP 即超文本传输协议,是一种获取网络资源 (例如图像、HTML 文档) 的应用层协议,它是互联网数据通信的基础,由请求和响应构成。通常,首先客户端会发送 HTTP 请求(在请求报文中会指定资源的 URL),然后用传输…

DocArray 0.20.0 发布!新增 Milvus 后端支持,更好地嵌套数据搜索,新增 RGB-D 格式的 3D 模型表示...

DocArray 是一个用于处理、传输和存储多模态数据的 Python 工具包。DocArray 提供便捷的多模态数据处理功能,具备基于 Protobuf 提供高性能的网络传输性能,同时也为多种向量存储方案提供统一的 API 接口。GitHub:github.com/docarray/docarra…

AU如何为你的人声增加空旷感?

你知道怎么使用AU给你的声音添加延迟效果,让你的声音具有空旷感和弱回声的效果。在这里我们可以使用插件达到这个目的。 在使用模拟延迟插件之前呢,我们可以去创建一个立体声总音轨,创建方式如图,跟着序号走,我们就可以…

CSS -- 06. CSS高阶技巧总结

文章目录CSS高阶技巧1 精灵图(sprites)1.1 为什么使用精灵图1.2 精灵图的使用2 字体图标2.1 字体图标的产生2.2 字体图标的优点2.3 字体图标的下载2.4 字体图标的引入2.5 字体图标的追加3 CSS三角形4 CSS用户界面样式4.1 鼠标样式 cursor4.2 表单的轮廓线4.3 防止拖拽文本域 re…

JAVA毕业设计——基于Springboot+vue的心理咨询管理系统(源代码+数据库)

github代码地址 https://github.com/ynwynw/psychlolgyhealth-public 毕业设计所有选题地址 https://github.com/ynwynw/allProject 基于Springbootvue的心理咨询管理系统(源代码数据库) 一、系统介绍 本项目分为管理员与普通用户两种角色 管理员角色包含以下功能&#xff…

[附源码]Python计算机毕业设计SSM基于Web美食网站设计(程序+LW)

项目运行 环境配置: Jdk1.8 Tomcat7.0 Mysql HBuilderX(Webstorm也行) Eclispe(IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持)。 项目技术: SSM mybatis Maven Vue 等等组成,B/S模式 M…

Kotlin标准库函数

Kotlin标准库中包含了几个函数,它们的目的就是可以在对象的上下文中执行代码块。当我们调用该Lambda表达式时,它会形成一个临时的作用域。在该范围内,可以访问不带名称的对象,此类函数称为作用域函数。包括: apply函数let函数run…

Java开发才不到3年,来面试开口要25K,面完连10K都不想给

前言 我的好朋友兼大学同学老左家庭经济情况不错,毕业之后没两年自己存了点钱加上家里的支持,自己在杭州开了一家网络公司。由于公司不是很大所以公司大部分的开发人员都是自己面试的,近期公司发展的不错,打算扩招也面试了不少人。…

[l论文解析]Classifier-Free Diffusion Guidance

paper link:https://openreview.net/pdf?idqw8AKxfYbI 文章目录OverviewWhat problem is addressed in the paper?What is the key to the solution?What is the main contribution?Potential fundamental flaws; how this work can be improved?Content关于 c…

Java 字符串 split 的一个反直觉陷阱

最近生产环境遇到一个奇怪的数组下标越界报错,如下图代码所示,我们可以肯定的是 fieldName 变量不为空(不是空字符串,也不是 null),但是代码执行到读取 names[0] 变量的时候,抛出了一个 数组下标…

5G无线技术基础自学系列 | 抗衰落技术

素材来源:《5G无线网络规划与优化》 一边学习一边整理内容,并与大家分享,侵权即删,谢谢支持! 附上汇总贴:5G无线技术基础自学系列 | 汇总_COCOgsta的博客-CSDN博客 无线信道是随机时变信道,信…

【云计算与大数据技术】文件存储格式行式、列式、GFS、HDFS的讲解(图文解释 超详细)

一、分布式文件系统 文件系统最后都需要以一定的格式存储数据文件,常见的文件存储布局有行式存储、列式存储以及混合式存储三种,不同的类别各有其优缺点和适用的场景,在目前的大数据分析系统中,列式存储和混合式存储方案因其特殊…

mysql 数据库设计三大范式

1. 什么是设计范式 设计表的依据,按照范式设计出来的表,不会出现数据的冗余 数据库的设计范式是数据库设计所需要满足的规范,满足这些规范的数据库是简洁的、结构清晰的;反之则是乱七八糟,不仅会给开发人员制造麻烦&a…