项目源码地址:https://github.com/zxf20180725/pygame-jxzj,求赞求星星~
哎呀呀,好久没更新了,这几天一直在搞我的pygame新项目仙剑奇侠传二战棋版。今天抽空把这一系列的教程给完结了!
在上一章中,我们已经把游戏服务端写的差不多了,这次我稍微改了一下服务端代码,就给玩家增加了一个role_id属性,这个属性可以让不同的玩家显示不同的形象,具体代码直接在github上下载。
客户端代码是基于4_4那一篇博客开发的。
我们先简单分析一下客户端实现的大致思路:
1.客户端要加入socket,第一个问题就是socket的recv是阻塞的。所以我们网络部分还是得新开一个线程来处理。也就是说游戏逻辑和渲染是一个线程,socket数据的接收是另一个进程。
2.怎么展示其他玩家呢?我们可以创建一个列表,把其他玩家都丢到这个列表里里,渲染的时候也记得渲染就行了。
3.玩家移动的时候记得告诉服务端。
实现以上3点,本章效果就能做出来了,简单吧~
我们新建一个net.py文件,把客户端所有的网络操作都放在这个文件里。
在这个文件里新建一个Client类,专门处理网络相关的逻辑
class Client:
"""
客户端与服务端网络交互相关的操作
"""
def __init__(self, socket, game):
self.socket = socket # 客户端socket
self.game = game # Game对象
# 创建一个线程专门处理数据接收
thread = Thread(target=self.recv_data)
thread.setDaemon(True)
thread.start()
构造方法有两个参数,socket和game,socket就是与服务端建立连接的那个socket啦。game就是main.py中Game类的实例对象,为啥要把它传进来呢?因为待会会用到里面的一些属性。
构造方法里还新建了一个线程,专门用来处理客户端接收数据的。
下面我们就来看看client的其他代码,我直接一次性把所有代码贴出来:
import json
import traceback
from threading import Thread
from core import Player, CharWalk
from game_global import g
class Client:
"""
客户端与服务端网络交互相关的操作
"""
def __init__(self, socket, game):
self.socket = socket # 客户端socket
self.game = game # Game对象
# 创建一个线程专门处理数据接收
thread = Thread(target=self.recv_data)
thread.setDaemon(True)
thread.start()
def data_handler(self):
# 给每个连接创建一个独立的线程进行管理
thread = Thread(target=self.recv_data)
thread.setDaemon(True)
thread.start()
def deal_data(self, bytes):
"""
处理数据
"""
# 将字节流转成字符串
pck = bytes.decode()
# 切割数据包
pck = pck.split('|#|')
# 处理每一个协议,最后一个是空字符串,不用处理它
for str_protocol in pck[:-1]:
protocol = json.loads(str_protocol)
# 根据协议中的protocol字段,直接调用相应的函数处理
self.protocol_handler(protocol)
def recv_data(self):
# 接收数据
try:
while True:
bytes = self.socket.recv(4096)
if len(bytes) == 0:
self.socket.close()
# TODO:掉线处理
break
# 处理数据
self.deal_data(bytes)
except:
self.socket.close()
# TODO:异常掉线处理
traceback.print_exc()
def send(self, py_obj):
"""
给服务器发送协议包
py_obj:python的字典或者list
"""
self.socket.sendall((json.dumps(py_obj, ensure_ascii=False) + '|#|').encode())
def protocol_handler(self, protocol):
"""
处理服务端发来的协议
"""
if protocol['protocol'] == 'ser_login':
# 登录协议的相关逻辑
if not protocol['result']:
# 登录失败,继续调用登录方法
print("登录失败:", protocol['msg'])
self.login()
return
# 登录成功
# 创建玩家
self.game.role = Player(self.game.hero, protocol['player_data']['role_id'], CharWalk.DIR_DOWN,
protocol['player_data']['x'], protocol['player_data']['y'],
name=protocol['player_data']['nickname'], uuid=protocol['player_data']['uuid'])
# 把玩家存到全局对象中,后面有用
g.player = self.game.role
self.game.game_state = 1 # 设置游戏的登录状态为已登录
elif protocol['protocol'] == 'ser_player_list':
# 玩家列表
print(protocol)
for player_data in protocol['player_list']:
player = Player(self.game.hero, player_data['role_id'], CharWalk.DIR_DOWN,
player_data['x'], player_data['y'],
name=player_data['nickname'], uuid=player_data['uuid']
)
self.game.other_player.append(player)
elif protocol['protocol'] == 'ser_move':
# 其他玩家移动了
for p in self.game.other_player:
if p.uuid == protocol['player_data']['uuid']:
p.goto(protocol['player_data']['x'], protocol['player_data']['y'])
break
elif protocol['protocol'] == 'ser_online':
# 有其他玩家上线
player_data = protocol['player_data']
player = Player(self.game.hero, player_data['role_id'], CharWalk.DIR_DOWN,
player_data['x'], player_data['y'],
name=player_data['nickname'], uuid=player_data['uuid']
)
self.game.other_player.append(player)
def login(self):
"""
登录
"""
print("欢迎进入间隙之间~")
username = input("请输入账号:")
password = input("请输入密码:")
data = {
'protocol': 'cli_login',
'username': username,
'password': password
}
self.send(data)
def move(self, player):
"""
玩家移动
"""
data = {
'protocol': 'cli_move',
'x': player.next_mx,
'y': player.next_my
}
self.send(data)
数据处理这一部分其实跟服务端是差不多的,上面代码注释也挺详细的,我就不多说了。
接下来就说说Client怎么用。
回到我们的Game类中。
我们需要在__init_game方法中新增连接服务器的功能:
def __init_game(self):
"""
我们游戏的一些初始化操作
"""
self.hero = pygame.image.load('./img/character/hero.png').convert_alpha()
self.map_bottom = pygame.image.load('./img/map/0.png').convert_alpha()
self.map_top = pygame.image.load('./img/map/0_top.png').convert_alpha()
self.game_map = GameMap(self.map_bottom, self.map_top, 0, 0)
self.game_map.load_walk_file('./img/map/0.map')
self.role = None # CharWalk(self.hero, 48, CharWalk.DIR_DOWN, 5, 10)
self.other_player = []
self.game_state = 0 # 0未登录 1已登录
# 与服务端建立连接
s = socket.socket()
s.connect(('127.0.0.1', 6666)) # 与服务器建立连接
self.client = Client(s, self)
g.client = self.client # 把client赋值给全局对象上,以便到处使用
# 登录
self.client.login()
注意上面的代码,我把self.role给置空了,因为我们是需要根据服务端返回的数据来创建主角的(具体代码看Client的protocol_handler)
self.other_player就是用来存放其他玩家对象的。
self.game_state是用来判断游戏当前的状态
接下来就是创建socket对象,与服务端建立连接。这里我们创建了刚刚写的Client类的对象。
注意一下,这个时候多了一个g.client=self.client。
这个g是一个单例类,用来存放一些全局变量的,创建这个g的作用就是防止某些地方import各种东西,搞的代码错综复杂,也可以解决到处传参,传的自己都晕了的问题。(g的代码我就不在文章中贴了,你们可以把完整代码下载下来慢慢看)
最后调用client.login(),这里帐号密码是需要在控制台里面输入的,pygame没提供文本框,我也很无奈呀~不过这个问题也可以解决,在这里我提供两个思路:
1.自己实现文本框(在我之前的文章里有一篇是讲这个的),但是自己实现中文输入是很困难的(需要实现内置输入法)。
2.用tk或者pyqt或者其他语言实现一个外置窗口,外置窗口里提供输入框,然后用socket与游戏通信,把输入框的内容传过来。
还有最后一点,玩家在移动的时候需要告诉服务器,这个我们把代码写在CharWalk类的goto方法中:
def goto(self, x, y):
"""
:param x: 目标点
:param y: 目标点
"""
self.next_mx = x
self.next_my = y
# 设置人物面向
if self.next_mx > self.mx:
self.dir = CharWalk.DIR_RIGHT
elif self.next_mx < self.mx:
self.dir = CharWalk.DIR_LEFT
if self.next_my > self.my:
self.dir = CharWalk.DIR_DOWN
elif self.next_my < self.my:
self.dir = CharWalk.DIR_UP
self.is_walking = True
# 告诉服务端自己移动了(其他玩家也属于player对象噢,所以这里得避免一下other_player里面的player也调用这个方法)
if g.player is self:
g.client.move(self)
最后,差点忘了,我还实现了一个Player类,它是CharWalk的子类:
class Player(CharWalk):
"""
玩家类
"""
def __init__(self, *args, **kwargs):
self.name = kwargs['name'] # 昵称
self.uuid = kwargs['uuid'] # uuid 玩家的唯一标识
super().__init__(*args, **kwargs)
很简单的一个类,就是新增了name和uuid属性(name还没用上)
最后最后,记得去渲染other_player列表呀,不然怎么看得见别人呢?
def update(self):
while True:
self.clock.tick(self.fps)
if self.game_state == 0: # 还未登录的时候,没必要执行这些逻辑
continue
# 逻辑更新
self.role.logic()
self.event_handler()
# 其他玩家逻辑(移动逻辑)
for player in self.other_player:
player.logic()
self.game_map.roll(self.role.x, self.role.y)
# 画面更新
self.game_map.draw_bottom(self.screen_surf)
self.role.draw(self.screen_surf, self.game_map.x, self.game_map.y)
# 绘制其他玩家
for player in self.other_player:
player.draw(self.screen_surf, self.game_map.x, self.game_map.y)
self.game_map.draw_top(self.screen_surf)
# self.game_map.draw_grid(self.screen_surf)
pygame.display.update()
ok,撒花完结~~~~~
额,这个最终项目我还是留了一些坑没填的,大家试试自己去实现吧:
1.玩家下线了,其他玩家还是能在屏幕上看到他。解决方案:客户端实现心跳机制(每隔几秒钟给服务端发一个协议),如果服务端长时间没收到这个协议,那么就认定这个客户端掉线了,服务端再告诉其他在线客户端,这个客户端下线了。其他客户端把它从other_player中删除即可。
2.名字还没显示出来呢,我们可以在人物的脚底或者头顶显示他的名字。
有啥其他问题,可以加Q群交流:812095339