python遊戲開發實戰:網路遊戲Demo(客戶端)

狡猾的皮球發表於2018-08-21

一.執行效果

二.介紹

原始碼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中掉線的玩家

相關文章