python游戏开发实战:网络游戏Demo(客户端)

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/qq_39687901/article/details/81904180

一.运行效果

二.介绍

源码github:https://github.com/zxf20180725/pygame-online-demo.git

这只是一个简单的联网程序Demo,代码有很多不严谨的地方,仅当抛砖引玉了。

运行客户端程序,会随机取一个名字进入游戏。使用wsad移动头像(蓝葵~)。

三.代码解析

注意,这里只会贴出部分核心代码,完整代码请在上面的github链接中下载。

全局部分的代码,这些都有注释了,具体作用,后面会讲到。foxyball.cn是我的服务器域名,这一年内应该都是有效的。

import random
import sys
import time
from random import randint
from threading import Thread

import pygame
import socket  # 导入 socket 模块

from base import Protocol

ADDRESS = ('127.0.0.1', 8712)  # ('foxyball.cn', 8712)  # 如果服务端在本机,请使用('127.0.0.1', 8712)

WIDTH, HEIGHT = 640, 480  # 窗口大小

g_font = None

g_screen = None  # 窗口的surface

g_sur_role = None  # 人物的role

g_player = None  # 玩家操作的角色

g_other_player = []  # 其他玩家

g_client = socket.socket()  # 创建 socket 对象

看一个程序的代码,应该从它的入口开始看。

if __name__ == '__main__':
    # 初始化
    init_game()
    # 游戏循环
    main_loop()

入口很简单,就调用了两个函数,那么我们先看看init_game()做了什么。

def init_game():
    """
    初始化游戏
    """
    global g_screen, g_sur_role, g_player, g_font

    # 初始化pygame
    pygame.init()
    pygame.display.set_caption('网络游戏Demo')
    g_screen = pygame.display.set_mode([WIDTH, HEIGHT])
    g_sur_role = pygame.image.load("./role.png").convert_alpha()  # 人物图片
    g_font = pygame.font.SysFont("fangsong", 24)
    # 初始化随机种子
    random.seed(int(time.time()))
    # 创建角色
    # 随机生成一个名字
    last_name = ['赵', '钱', '孙', '李', '周', '吴', '郑', '王', '冯', '陈', '褚', '卫',
                 '蒋', '沈', '韩', '杨', '朱', '秦', '尤', '许', '何', '吕', '施', '张',
                 '孔', '曹', '严', '华', '金', '魏', '陶', '姜', '戚', '谢', '邹', '喻', ]
    first_name = ['梦琪', '忆柳', '之桃', '慕青', '问兰', '尔岚', '元香', '初夏', '沛菡',
                  '傲珊', '曼文', '乐菱', '痴珊', '孤风', '雅彤', '宛筠', '飞松', '初瑶',
                  '夜云', '乐珍']
    name = random.choice(last_name) + random.choice(first_name)
    print("你的昵称是:", name)
    g_player = Role(randint(100, 500), randint(100, 300), name)

    # 与服务器建立连接
    g_client.connect(ADDRESS)
    # 开始接受服务端消息
    thead = Thread(target=msg_handler)
    thead.setDaemon(True)
    thead.start()
    # 告诉服务端有新玩家
    send_new_role()

从与服务器建立连接开始讲吧(28行)。如果对python的socket不太熟悉的话,可以先看看这两篇文章:https://blog.csdn.net/qq_39687901/article/details/81531101https://blog.csdn.net/qq_39687901/article/details/81536641,g_client是一个socket对象,与指定的服务端建立连接。

接收服务端消息部分,我新开了一个线程进行处理(因为recv是阻塞线程的)。处理服务端消息的函数是msg_handler,这个函数稍后再讲,我们继续往下看send_new_role函数。

def send_new_role():
    """
    告诉服务端有新玩家加入
    """
    # 构建数据包
    p = Protocol()
    p.add_str("newrole")
    p.add_int32(g_player.x)
    p.add_int32(g_player.y)
    p.add_str(g_player.name)
    data = p.get_pck_has_head()
    # 发送数据包
    g_client.sendall(data)

Protocol是我们自定义的游戏数据包协议,关于Protocol的设计思路都在这篇文章:https://blog.csdn.net/qq_39687901/article/details/81541967

这里,我们构造了一个名字叫“newrole”的数据包,并且加入了玩家的信息(坐标和昵称),最后把这个数据包发送给服务端。这个“newrole”的作用就是告诉服务端有一个新玩家加入游戏啦,然后服务端又会告诉其他玩家有个新玩家加入了(这就实现了可以在窗口里看到其他玩家的功能)。我会在下一篇文章详细的讲解服务端的设计,这里就不多说了。

回到我们的程序入口来,接下来就该执行main_loop啦。

def main_loop():
    """
    游戏主循环
    """
    while True:
        # FPS=60
        pygame.time.delay(32)
        # 逻辑更新
        update_logic()
        # 视图更新
        update_view()

每个游戏都必不可少的游戏主循环。循环里很简单,就调用了3个函数。pygame.time.delay(32)让每次循环间隔32毫秒,也就是说每秒循环执行60次左右。然后就是逻辑更新和视图更新了,这两个函数请尽可能的解耦。

那我们继续看逻辑更新,这个demo的游戏逻辑很简单,就是用wasd控制角色移动。

def update_logic():
    """
    逻辑更新
    """
    # 事件处理
    handler_event()
def handler_event():
    # 事件处理
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            sys.exit()
        elif event.type == pygame.KEYDOWN:
            if event.key == pygame.K_w:
                g_player.y -= 5
            elif event.key == pygame.K_s:
                g_player.y += 5
            elif event.key == pygame.K_a:
                g_player.x -= 5
            elif event.key == pygame.K_d:
                g_player.x += 5
            send_role_move()  # 告诉服务器,自己移动了

角色每次移动之后,要把最新的坐标告诉给服务端,服务端再把这个角色的最新坐标发送给其他客户端,这样其他客户端就能看到你在移动了。send_role_move函数就是把当前坐标发送给服务端。

def send_role_move():
    """
    发送角色的坐标给服务端
    """
    # 构建数据包
    p = Protocol()
    p.add_str("move")
    p.add_int32(g_player.x)
    p.add_int32(g_player.y)
    data = p.get_pck_has_head()
    # 发送数据包
    g_client.sendall(data)

回到我们的游戏主循环,继续看视图更新。

def update_view():
    """
    视图更新
    """
    g_screen.fill((0, 0, 0))
    # 画角色
    g_screen.blit(g_player.sur_name, (g_player.x, g_player.y - 20))
    g_screen.blit(g_sur_role, (g_player.x, g_player.y))
    # 画其他角色
    for r in g_other_player:
        g_screen.blit(r.sur_name, (r.x, r.y - 20))
        g_screen.blit(g_sur_role, (r.x, r.y))
    # 刷新
    pygame.display.flip()

其中,g_other_player是一个存着其他在线玩家的列表。那么这个列表中的内容是从哪里来的呢?内容当然是从服务端发过来的。还记得本文最开始提到的新开一个线程处理服务端消息吗?就是那个msg_handler函数,现在来研究研究它。

def msg_handler():
    """
    处理服务端返回的消息
    """
    while True:
        bytes = g_client.recv(1024)
        # 以包长度切割封包
        while True:
            # 读取包长度
            length_pck = int.from_bytes(bytes[:4], byteorder='little')
            # 截取封包
            pck = bytes[4:4 + length_pck]
            # 删除已经读取的字节
            bytes = bytes[4 + length_pck:]
            # 把封包交给处理函数
            pck_handler(pck)
            # 如果bytes没数据了,就跳出循环
            if len(bytes) == 0:
                break

外层的while循环是用来接收服务端的消息,内层的while循环是用来切割数据包的(tcp粘包分包了解一下)。但这里还有个问题,是我在代码里没有去处理的。那就是tcp分包问题,这里内层while循环只解决了粘包。分包的问题,在以后的文章中会讲。内层while循环的逻辑为什么要这么写,大家还是去看看我之前发的那篇文章吧。https://blog.csdn.net/qq_39687901/article/details/81541967

数据包切割好了之后,就调用pck_handler函数处理数据包。

def pck_handler(pck):
    p = Protocol(pck)
    pck_type = p.get_str()

    if pck_type == 'playermove':  # 玩家移动的数据包
        x = p.get_int32()
        y = p.get_int32()
        name = p.get_str()
        for r in g_other_player:
            if r.name == name:
                r.x = x
                r.y = y
                break
    elif pck_type == 'newplayer':  # 新玩家数据包
        x = p.get_int32()
        y = p.get_int32()
        name = p.get_str()
        r = Role(x, y, name)
        g_other_player.append(r)
    elif pck_type == 'logout':  # 玩家掉线
        name = p.get_str()
        for r in g_other_player:
            if r.name == name:
                g_other_player.remove(r)
                break

我们这个小demo一共就设计了三个协议类型,"playermove"、"newplayer"和"logout"。"playermove"是在其他玩家移动的时候,服务端给我们的,让我们更新其他玩家的位置(这样就能看到其他玩家的移动效果了)。剩下的两个就不用多说了吧。

四.总结

网络流程:

登录流程:

1.客户端登录,发送"newrole"数据包给服务端

2.服务端收到"newrole"数据包,然后发送"newplayer"数据包给其他客户端

3.其他客户端收到"newplayer",向g_other_player列表中添加一个玩家

移动流程:

1.客户端移动,发送"move"数据包给服务端

2.服务端收到"move"数据包,然后发送"playermove"数据包给其他客户端

3.其他客户端收到"playermove",更新g_other_player的相关数据

下线流程:

1.服务端检测到有客户端掉线,发送"logout"数据包给其他在线客户端

2.其他客户端收到"playermove",删除g_other_player中掉线的玩家

猜你喜欢

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