目录
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