歲月不居,時節(jié)如流,轉(zhuǎn)眼已來到了這個系列的最后一篇
上回說到采用socket+多線程的網(wǎng)絡(luò)模型來實(shí)現(xiàn)服務(wù)器與客戶端的通信,今天我們就在這個基礎(chǔ)上完整的實(shí)現(xiàn)多人在線的游戲版本
我們會把之前的1~5節(jié)所講的內(nèi)容全都用上,見證一款可以玩的游戲的誕生

再回顧一下服務(wù)器與客戶端的邏輯分工:
服務(wù)器:
1.與客戶端建立網(wǎng)絡(luò)連接,并為該玩家創(chuàng)建一個robot(副線程)
2.接收客戶端陸續(xù)發(fā)過來的消息,將消息解析成玩家的具體操作(副線程)
3.在自己的游戲世界中模擬robot與子彈的變化(主線程)
4.將游戲世界中發(fā)生的事時刻同步給所有的客戶端(主線程)
客戶端:
1.與服務(wù)器建立連接(主線程)
2.捕捉玩家的鍵盤行為,描述成一組消息發(fā)送給服務(wù)器(主線程)
3.接收服務(wù)器發(fā)送的同步消息,修改自己存儲的游戲世界(副線程)
4.將游戲世界在屏幕上繪制出來(主線程)
我們可以與第4篇的“單機(jī)版”對比一下
單機(jī)版是相當(dāng)于是“客戶端”捕捉玩家鍵盤行為后自己模擬世界的變化
網(wǎng)絡(luò)版是客戶端將行為發(fā)給服務(wù)器,由服務(wù)器模擬,再由服務(wù)器告訴客戶端世界的變化
從上面的描述中可以看到客戶端與服務(wù)器之間有兩種消息交互:
1.客戶端到服務(wù)器:
玩家的移動指令、發(fā)射指令
2.服務(wù)器到客戶端:
所有玩家的位置信息、所有子彈的位置信息
為了區(qū)分不同的玩家,服務(wù)器在創(chuàng)建robot時會對應(yīng)生成一個id來標(biāo)志這個玩家,并把這個id回傳給客戶端
客戶端在之后的所有指令消息中會附帶上這個id
下面我們來看關(guān)鍵部分的代碼實(shí)現(xiàn)
服務(wù)器與客戶端建立網(wǎng)絡(luò)連接,并為該玩家創(chuàng)建一個robot
def accept_client():
global total_robot_num,lock,g_need_syna
while True:
client, _ = g_socket_server.accept()# 阻塞,等待客戶端連接
g_conn_pool.append(client)
thread = Thread(target=message_handle, args=(client,))#創(chuàng)建線程接收后續(xù)指令消息
thread.setDaemon(True)
thread.start()
lock.acquire()#線程鎖加鎖
total_robot_num += 1
robot = Robot(total_robot_num)#創(chuàng)建玩家控制的robot
robot_list.append(robot)
g_need_syna = 1#告訴主線程世界發(fā)生了變化,需要同步
res_msg = "accept "+str(total_robot_num)
client.send(bytes(res_msg,'utf-8'))#將robot的id發(fā)送給客戶端
lock.release()#線程鎖釋放
我們在這個函數(shù)中加入了一個新的東西“lock”,這是一個線程鎖。
這是由于游戲世界(所有的robot,所有的子彈)只有一份,是公用的。而主線程和副線程都會對游戲世界做修改,為了避免幾個線程同時修改世界(可以類比幾個人都在搶一張火車票),在一個線程獲得了修改權(quán)限時,需要把游戲世界鎖起來,別的線程只有等它修改結(jié)束了才能繼續(xù)修改。
而客戶端收到這個“accep xxx"之后,就會把xxx作為自己的id記下來
def RecvMsg(client):
global lock,robot_list,bullet_list,self_robot_id
while True:
data = client.recv(8192)
data = data.decode()
str_list = data.split('\n')
if(len(str_list)):
first_line = str_list[0]
fl = first_line.split(' ')
if(len(fl)):
if(fl[0]=="accept"):#連接成功
self_robot_id = int(fl[1])
再看客戶端發(fā)送操作指令的消息:
key_press = pygame.key.get_pressed()
if(key_press[K_LEFT]):#按下方向鍵左
msg = str(self_robot_id) + " 3"#與服務(wù)器約定3表示向左移動一次
client.send(msg.encode("utf-8"))
以及服務(wù)器接收到指令后的處理:
def message_handle(client):
global lock,g_need_syna,total_bullet,RobotSize,BulletSize
while True:
data = client.recv(8192)
data = data.decode('utf-8')
if len(data) == 0:
client.close()
g_conn_pool.remove(client)#刪除連接
else:
str_list = data.split(' ')
if(len(str_list)>=2):
robot_id = int(str_list[0])#id
move_dir = int(str_list[1])#移動移動指令
lock.acquire()
for rbt in robot_list:
if rbt.id == robot_id:#找到對應(yīng)的機(jī)器人
rbt.Move(move_dir)#控制機(jī)器人移動
g_need_syna = 1
lock.release()
最后是服務(wù)器對整個游戲世界的同步:
syna_msg = []
syna_msg.append("robots\n")
for rbt in robot_list:#所有玩家的位置
syna_msg.append(str(rbt.id)+" ")
syna_msg.append(str(rbt.x)+" ")
syna_msg.append(str(rbt.y)+" ")
syna_msg.append(str(rbt.z)+" ")
syna_msg.append(str(rbt.dir)+"\n")
syna_msg.append("bullets\n")
for bul in bullet_list:#所有子彈的位置
syna_msg.append(str(bul.id)+" ")
syna_msg.append(str(bul.fa)+" ")
syna_msg.append(str(bul.x)+" ")
syna_msg.append(str(bul.y)+" ")
syna_msg.append(str(bul.z)+"\n")
syna_str = ''.join(syna_msg)
for clt in g_conn_pool:#發(fā)送給所有客戶端
clt.sendall(bytes(syna_str,'utf-8'))
以及客戶端收到同步的消息后需要對應(yīng)修改自己記錄的世界(代碼太長就不貼了)
完整的代碼可以從這里獲取(代碼細(xì)節(jié)還是挺多的,就不一一說明了。而且這只是我自己實(shí)現(xiàn)的方式,我對python也不是很熟,湊合著看吧)
服務(wù)器
客戶端
至此整個游戲的初版已經(jīng)完成。
后續(xù)可以對游戲進(jìn)行貼圖(美術(shù)換皮),魔改子彈和技能,以及玩家視野上做一些處理。目前已經(jīng)在0.1版本中加入了追蹤導(dǎo)彈的元素。
如果有時間的話會在GitHub上繼續(xù)更新。也歡迎鐵子們跟我一起交流嗷。