目錄
1.1 設(shè)置游戲窗口
1.2 繪制一個(gè)方塊
1.3 編寫服務(wù)端代碼
1.4?完善客戶端代碼
1.5?完整代碼下載地址
在本節(jié),我們將通過一個(gè)簡(jiǎn)單的方塊移動(dòng)程序進(jìn)入多人聯(lián)機(jī)游戲的大門。每個(gè)玩家打開游戲窗口后都可以控制一個(gè)方塊,當(dāng)某個(gè)玩家移動(dòng)方塊后,其余玩家的窗口上會(huì)自動(dòng)更新該玩家的方塊位置。運(yùn)行示例如下:
本項(xiàng)目結(jié)構(gòu)顯示如下:
├── client.py? ? ? ? ?# 客戶端代碼
└── server.py? ? ? ? # 服務(wù)端代碼
在client.py中我們一共導(dǎo)入了以下幾個(gè)模塊或庫:
import sys
import json
import pygame
import socket
from random import randint
在server.py中我們一共導(dǎo)入了以下幾個(gè)模塊或庫:
import json
import socket
from threading import Thread
1.1 設(shè)置游戲窗口
我們首先要設(shè)置好游戲窗口的相關(guān)屬性,比如窗口標(biāo)題、寬高以及背景等等。
# client.py
class GameWindow:
def __init__(self):
self.width = 500
self.height = 500
self.window = self.init_window()
def init_window(self): # 1
pygame.init()
pygame.display.set_caption('移動(dòng)方塊')
return pygame.display.set_mode((self.width, self.height))
def update_window(self): # 2
self.window.fill((255, 255, 255))
pygame.display.update()
def start(self): # 3
clock = pygame.time.Clock()
while True:
clock.tick(60)
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. init_window()函數(shù)用來初始化pygame窗口的相關(guān)屬性,我們?cè)谠摵瘮?shù)中設(shè)置了窗口的標(biāo)題和大小。
2. update_window()函數(shù)用來更新窗口,將窗口背景設(shè)置為白色,后續(xù)我們也會(huì)在該函數(shù)中不斷更新玩家的狀態(tài)。
3. start()函數(shù)是游戲入口,重點(diǎn)是要調(diào)用update_window()函數(shù)不斷更新窗口。
運(yùn)行結(jié)果如下:
1.2 繪制一個(gè)方塊
游戲窗口設(shè)置好了之后,我們就可以往窗口上添加方塊了。一個(gè)方塊代表一個(gè)玩家,我們就用Player類來實(shí)現(xiàn)方塊的相關(guān)功能。
class Player:
def __init__(self, win, p_id, x, y, color):
self.win = win # 1
self.id = p_id # 2
self.dis = 3 # 3
self.x = x
self.y = y
self.width = 100
self.height = 100
self.color = color
def move(self): # 4
keys = pygame.key.get_pressed()
if keys[pygame.K_LEFT]:
self.x -= self.dis
elif keys[pygame.K_RIGHT]:
self.x += self.dis
elif keys[pygame.K_UP]:
self.y -= self.dis
elif keys[pygame.K_DOWN]:
self.y += self.dis
def draw(self): # 5
pygame.draw.rect(self.win, self.color, (self.x, self.y, self.width, self.height))
代碼解釋如下:
1. Player類接收一個(gè)游戲窗口實(shí)例,后面我們會(huì)在GameWindow類中實(shí)例化一個(gè)Player對(duì)象并傳入游戲窗口實(shí)例的。
2. 每個(gè)玩家都會(huì)擁有一個(gè)id,該id會(huì)在服務(wù)端生成并從服務(wù)端獲取過來。
3.?dis變量為方塊每次移動(dòng)的距離。方塊的寬高都為100,坐標(biāo)和顏色是隨機(jī)的。
4. 根據(jù)按鍵改變方塊位置。
5. 將方塊繪制到窗口上。
玩家類已經(jīng)編寫好了,接下來就是要在游戲窗口上添加玩家了。
# client.py
class GameWindow:
def __init__(self):
...
self.player = Player(win=self.window, # 1
p_id=None,
x=randint(0, self.width - 100),
y=randint(0, self.height - 100),
color=(randint(0, 200), randint(0, 200), randint(0, 200)))
...
def update_window(self):
self.window.fill((255, 255, 255))
self.player.move() # 2
self.player.draw()
pygame.display.update()
...
代碼解釋如下:
1. 實(shí)例化一個(gè)Player對(duì)象并傳入相關(guān)參數(shù)。因?yàn)檫€沒有連接到服務(wù)端,所以p_id先設(shè)置為None。x和y坐標(biāo)是隨機(jī)的,減去100(也就是方塊的寬高)是為了讓方塊顯示在窗口內(nèi)。之所以將顏色值設(shè)定在0-200之間,是為了防止方塊和窗口背景顏色太接近。假如方塊顏色特別接近白色,那和窗口背景就混在一起,很難辨別了。
2. 在update_window()函數(shù)中調(diào)用Player實(shí)例對(duì)象的move()和draw()方法,不斷在游戲窗口中更新自身的狀態(tài)。
運(yùn)行結(jié)果如下:
1.3 編寫服務(wù)端代碼
服務(wù)端的代碼邏輯很簡(jiǎn)單,就是創(chuàng)建套接字等待客戶端連接,然后將各個(gè)客戶端發(fā)送過來的玩家數(shù)據(jù)保存起來,整理之后再發(fā)送出去。代碼編寫如下:
# server.py
import json
import socket
from threading import Thread
class Server:
def __init__(self):
self.port = 5000 # 1
self.host = "127.0.0.1"
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.players_data = {} # 2
def start(self): # 3
self.get_socket_ready()
self.handle_connection()
def get_socket_ready(self): # 4
self.sock.bind((self.host, self.port))
self.sock.listen()
print("服務(wù)器已準(zhǔn)備接收客戶端連接")
def handle_connection(self): # 5
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): # 6
while True:
try:
data = conn.recv(2048)
if not data:
print("未接收到數(shù)據(jù),關(guān)閉連接")
self.players_data.pop(str(id(conn)))
conn.close()
break
else:
data = json.loads(data.decode("utf-8"))
self.update_one_player_data(data)
conn.sendall(json.dumps(self.get_other_players_data(data["id"])).encode("utf-8"))
except Exception as e:
print(repr(e))
break
def update_one_player_data(self, data):
key = data["id"]
pos = data["pos"]
color = data["color"]
self.players_data[key] = {"pos": pos, "color": color}
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. 服務(wù)端監(jiān)聽的地址為127.0.0.1:5000,所以后續(xù)再客戶端中編寫代碼時(shí)要連接到這個(gè)地址。
2. players_data是一個(gè)字典變量,用來存儲(chǔ)各個(gè)玩家的數(shù)據(jù)。該字典的各個(gè)鍵是玩家id,值包含玩家的位置和顏色。示例如下:
{
"玩家id": {
"pos" [x坐標(biāo)值, y坐標(biāo)值],
"color": (r, g, b)
}
}
3. start()函數(shù)是程序入口。
4.? 在get_socket_ready()函數(shù)中,我們讓套接字綁定監(jiān)聽了127.0.0.1:5000,準(zhǔn)備就緒。
5. 如果服務(wù)端收到了來自客戶端的連接,那就會(huì)將str(id(conn))作為玩家id值發(fā)送到客戶端,而客戶端也會(huì)將該值保存到Player對(duì)象的id屬性中。為了不讓連接堵塞,我們使用多線程技術(shù)來處理后續(xù)的消息通信。
6. 在handle_message()函數(shù)中,我們不斷接收來自客戶端的消息。如果客戶端沒有發(fā)送消息過來(data為空),說明客戶端已經(jīng)被關(guān)閉,玩家離開游戲了,那我們就要從players_data變量中刪除對(duì)應(yīng)玩家的數(shù)據(jù)并關(guān)閉套接字。如果有消息發(fā)送過來(data不為空),那么我們就將該玩家的數(shù)據(jù)保存或更新到player_data變量中,通過update_one_player_data()函數(shù)實(shí)現(xiàn)。最后將其他玩家的數(shù)據(jù)全部發(fā)送給該玩家,好讓客戶端窗口更新其他玩家的位置信息,其他玩家的數(shù)據(jù)通過get_other_players_data()函數(shù)獲取。
簡(jiǎn)而言之:玩家A發(fā)送自身數(shù)據(jù)到服務(wù)端,服務(wù)端保存并返回除A之外的其他所有玩家的數(shù)據(jù)。玩家B發(fā)送自身數(shù)據(jù)到服務(wù)端,服務(wù)端保存并返回除B之外的其他所有玩家的數(shù)據(jù)。
注:或者也可以這樣通信,玩家A發(fā)送自身數(shù)據(jù)到服務(wù)端,服務(wù)端將該數(shù)據(jù)發(fā)送給其他所有玩家。玩家B發(fā)送自身數(shù)據(jù)到服務(wù)端,服務(wù)端將該數(shù)據(jù)發(fā)送給其他所有玩家。這種通信方式有個(gè)缺點(diǎn),就是服務(wù)端的發(fā)送的消息次數(shù)會(huì)很多。假如現(xiàn)在有10個(gè)玩家,當(dāng)每個(gè)玩家發(fā)送1次數(shù)據(jù)到服務(wù)端,服務(wù)端還需要發(fā)送9次將數(shù)據(jù)同步給其他玩家。
運(yùn)行結(jié)果如下:
1.4?完善客戶端代碼
最后一步就是要在客戶端程序中添加和服務(wù)端通信的代碼。客戶端會(huì)把玩家數(shù)據(jù)發(fā)送到服務(wù)端,然后把服務(wù)端接收過來的其他玩家的數(shù)據(jù)更新到窗口上。代碼編寫如下:
class GameWindow:
def __init__(self):
...
self.port = 5000 # 1
self.host = "127.0.0.1"
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.connect() # 2
self.other_players_dict = {} # 3
...
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): # 4
data = {
"id": self.player.id,
"pos": [self.player.x, self.player.y],
"color": self.player.color
}
self.sock.send(json.dumps(data).encode("utf-8"))
return self.sock.recv(2048).decode("utf-8")
def update_window(self): # 5
self.window.fill((255, 255, 255))
self.player.move()
self.player.draw()
other_players_data = json.loads(self.send_player_data())
self.update_other_players_data(other_players_data)
self.delete_offline_players(other_players_data)
pygame.display.update()
def update_other_players_data(self, data): # 6
for key, value in data.items():
if not self.other_players_dict.get(key):
self.add_one_player(key, value)
else:
pos = value["pos"]
self.other_players_dict[key].x = pos[0]
self.other_players_dict[key].y = pos[1]
self.other_players_dict[key].draw()
def add_one_player(self, player_id, value): # 7
pos = value["pos"]
color = value["color"]
self.other_players_dict[player_id] = Player(self.window, player_id, pos[0], pos[1], color)
def delete_offline_players(self, data): # 8
new_dict = {}
for key in self.other_players_dict.keys():
if data.get(key):
new_dict[key] = self.other_players_dict[key]
self.other_players_dict = new_dict
...
代碼解釋如下:
1. 客戶端要連接到127.0.0.1:5000,即服務(wù)端監(jiān)聽的地址。
2. 在connect()函數(shù)中,我們讓客戶端和服務(wù)端進(jìn)行了連接。連接后,服務(wù)端會(huì)發(fā)送str(id(conn))作為玩家id,我們將該值保存到Player對(duì)象的id屬性中。
3. other_players_dict字典變量用來保存其他所有玩家的數(shù)據(jù),字典的鍵是玩家id,值是代表該玩家的Player對(duì)象,格式如下所示。
{
"玩家id": Player實(shí)例對(duì)象
}
4. send_player_data()函數(shù)用來將當(dāng)前玩家的數(shù)據(jù)發(fā)送到服務(wù)端,并返回從服務(wù)端接收到的其他玩家的數(shù)據(jù)。
5. 在update_window()函數(shù)中,我們不僅要更新當(dāng)前玩家的狀態(tài),還要更新其他所有玩家的狀態(tài)。
6.&7. 在update_other_players_data()函數(shù)中,我們分析從服務(wù)端接收回來的數(shù)據(jù),如果出現(xiàn)一個(gè)新的玩家id,那么就實(shí)例化一個(gè)Player對(duì)象并保存到other_players_dict字典變量中。如果該玩家id已存在,則更新該玩家的位置即可。
8. 如果某個(gè)玩家退出了游戲,那從服務(wù)端接收回來的數(shù)據(jù)中,肯定會(huì)少一個(gè)玩家id。但是該玩家id之前又已經(jīng)保存到other_players_dict字典變量中,所以我們每次還要更新下other_players_dict,把不需要的玩家id給去掉。
現(xiàn)在先運(yùn)行服務(wù)端程序,然后再運(yùn)行任意數(shù)量的客戶端程序,筆者這里就打開三個(gè)客戶端。我們發(fā)現(xiàn),每個(gè)游戲窗口上都會(huì)出現(xiàn)三個(gè)方塊,在任何一個(gè)窗口上移動(dòng)方塊,其他兩個(gè)窗口也會(huì)立即更新方塊位置。運(yùn)行結(jié)果如下:
1.5?完整代碼下載地址
鏈接:https://pan.baidu.com/s/15uFqYp98R0LoH92LOsRzug ?文章來源:http://www.zghlxwxcb.cn/news/detail-418665.html
密碼:18ik文章來源地址http://www.zghlxwxcb.cn/news/detail-418665.html
到了這里,關(guān)于《Python多人游戲項(xiàng)目實(shí)戰(zhàn)》第一節(jié) 簡(jiǎn)單的方塊移動(dòng)的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!