pygame网络游戏_5_5:网络编程_游戏客户端【终章!】

项目源码地址:https://github.com/zxf20180725/pygame-jxzj,求赞求星星~

1.本章效果

2.回顾

哎呀呀,好久没更新了,这几天一直在搞我的pygame新项目仙剑奇侠传二战棋版。今天抽空把这一系列的教程给完结了!

在上一章中,我们已经把游戏服务端写的差不多了,这次我稍微改了一下服务端代码,就给玩家增加了一个role_id属性,这个属性可以让不同的玩家显示不同的形象,具体代码直接在github上下载。

3.客户端代码实现

客户端代码是基于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

发布了42 篇原创文章 · 获赞 112 · 访问量 13万+

猜你喜欢

转载自blog.csdn.net/qq_39687901/article/details/104564337