我最近偶然發現了科技影響者「the primeagen」的一句話,我不太記得了,但我記得它是這樣的:
「如果你所做的事情沒有失敗,那麼你就沒有學習。」
這讓我開始思考我的程式設計之旅。我已經非常熟悉構建後端,甚至可以編寫 import express from 'express';已經變成了一件苦差事。
為了建立我的第百萬個革命性的待辦事項應用程序,我沒有經歷學習另一個JavaScript 框架的經典事件(因為顯然,世界需要更多這樣的應用程式),我決定做點別的事情。我一直在閱讀有關 WebSocket 協定的內容,發現它處理伺服器和客戶端之間雙向非同步訊息的能力非常令人著迷。我想用它來建構一些東西,但我需要脫離 JavaScript。
經過一番考慮,我選擇了一款簡單的多人 2D 遊戲。它將涉及計算(碰撞檢測)、資料結構(鍊錶、雜湊圖)和玩家同步。貪食蛇遊戲似乎很完美,有一些簡單的規則:
吃水果可以讓你成長,並為你的分數加 1
撞到其他玩家的身體會讓你縮小,隨機重置你的位置,並將你的分數歸零
正面碰撞會導致雙方玩家縮小,重置位置,並將分數歸零
所有這些計算都發生在伺服器端,以防止玩家篡改遊戲邏輯。我們將使用 Python 3 和 Pygame 進行圖形處理,並使用 websockets 函式庫透過 asyncio 處理非同步訊息。
現在,讓我們深入研究您可能會發現它很混亂的程式碼,因為記住程式設計的第一條規則:
「如果它有效,請勿觸摸它。」
你已經讀夠了我的咆哮,讓我們繼續有趣的部分:編碼。但如果您想跳過廢話並直接投入使用,只需前往 GitHub 存儲庫即可。
如果您想做出貢獻,請隨時提出問題或提交拉取請求。歡迎任何改進或錯誤修復!
首先,我們定義資料結構:
class Object : def __init__(self , x : float , y :float , width:int , height :int): #################################### # init object's size and postion # #################################### self.x = x self.y = y self.height = height self.width = width def render(self , screen , color) : pygame.draw.rect(screen ,color ,pygame.Rect(self.x , self.y , self.width , self.height)) class Player(Object) : def __init__(self, x: float, y: float, width: int, height: int): super().__init__(x, y, width, height) self.next = None self.prev = None self.tail = self self.direction = 'LEFT' self.length = 1 self.color = 'red' # move the Snake to a certain direction # the "changed" will be a way to tell either to continue in the same direction # or change the direction of the head to the new direction # it is used in the game def change_direction(self, keys): changed = False if self.direction in ['LEFT', 'RIGHT']: if keys[pygame.K_w] and self.direction != 'DOWN': self.direction = 'UP' changed = True elif keys[pygame.K_s] and self.direction != 'UP': self.direction = 'DOWN' changed = True elif self.direction in ['UP', 'DOWN']: if keys[pygame.K_a] and self.direction != 'RIGHT': self.direction = 'LEFT' changed = True elif keys[pygame.K_d] and self.direction != 'LEFT': self.direction = 'RIGHT' changed = True return changed # move the Snake to a certain direction with a certain speed def move(self, screen, dt): speed = 150 * dt if self.direction == 'UP': self.move_all(screen, 0, -speed) elif self.direction == 'DOWN': self.move_all(screen, 0, speed) elif self.direction == 'LEFT': self.move_all(screen, -speed, 0) elif self.direction == 'RIGHT': self.move_all(screen, speed, 0) def bound(self , screen) : if self.y < 0 : self.y = screen.get_height() if self.y > screen.get_height() : self.y = 0 if self.x < 0 : self.x = screen.get_width() if self.x > screen.get_width() : self.x = 0 def get_pos(self) : arr = [] current = self while current : arr.append([current.x , current.y]) current = current.next return arr # move the snake and its body to some coordinates def move_all(self, screen, dx, dy): # Store old positions old_positions = [] current = self while current: old_positions.append((current.x, current.y)) current = current.next # Move head self.x += dx self.y += dy self.bound(screen) # self.render(screen, self.color) # Move body current = self.next i = 0 while current: current.x, current.y = old_positions[i] current.bound(screen) # current.render(screen, self.color) current = current.next i += 1 def add(self ): new = Player(self.tail.x+self.tail.width+10 , self.tail.y ,self.tail.width , self.tail.height) new.prev = self.tail self.tail.next = new self.tail = new self.length +=1 def shrink(self , x , y): self.next = None self.tail = self self.length = 1 self.x = x self.y = y # used for the and the opponent player when # receiving its coordinates def setall(self , arr) : self.x = arr[0][0] self.y = arr[0][1] self.next = None self.tail = self current = self for i in range(1 , len(arr)) : x = arr[i][0] y = arr[i][1] new = Player(x ,y ,self.width , self.height) current.next = new self.tail = new current = current.next # render the whole snake on the screen # used for both the current player and the opponent def render_all(self, screen, color): current = self if self.next : self.render(screen,'white') current = self.next while current : current.render(screen , color) current = current.next
Object 類別是遊戲物件的基類,而 Player 類別則使用蛇特定的功能擴展了它。 Player 類別包含用於改變方向、移動、增加、縮小和渲染蛇的方法。
接下來,我們有遊戲邏輯:
import pygame from objects import Player import websockets import asyncio import json uri = 'ws://localhost:8765' pygame.font.init() # Render the text on a transparent surface font = pygame.font.Font(None, 36) # playing the main theme (you should hear it) def play() : pygame.mixer.init() pygame.mixer.music.load('unknown.mp3') pygame.mixer.music.play() def stop(): if pygame.mixer.music.get_busy(): # Check if music is playing pygame.mixer.music.stop() # initialize players and the fruit def init(obj) : print(obj) player = Player(*obj['my'][0]) opp = Player(*obj['opp'][0] ) food = Food(*obj['food']) return (player , opp , food) async def main(): async with websockets.connect(uri) as ws: choice = int(input('1 to create a room \n2 to join a room\n>>>')) room_name = input('enter room name: ') await ws.send(json.dumps({ "choice" : choice , "room" : room_name })) ## waiting for the other player to connecet res = {} while True: res = await ws.recv() try: res = json.loads(res) break except Exception as e: pass player, opp, food = init(res) pygame.init() screen = pygame.display.set_mode((600, 400)) clock = pygame.time.Clock() running = True dt = 0 play() while running: for event in pygame.event.get(): if event.type == pygame.QUIT: running = False screen.fill("black") my_pos = { 'pos': player.get_pos(), 'len': player.length } await ws.send(json.dumps(my_pos)) response = await ws.recv() response = json.loads(response) # Update food position pygame.draw.rect(screen ,'green' ,pygame.Rect(response['food'][0] ,response['food'][1] ,20, 20)) # Handle actions if response['act'] == 'grow': player.add() elif response['act'] == 'shrinkall': player.shrink(*response['my'][0][0:2]) opp.shrink(*response['opp'][0][0:2]) elif response['act'] == 'shrink': player.shrink(*response['my'][0][0:2]) # restarting the song each time you bump into the other player stop() play() else: opp.setall(response['opp']) # Render everything once per frame player.render_all(screen, 'red') opp.render_all(screen, 'blue') ## score ## x | y => x you , y opponent text = font.render(f'{response["my_score"]} | {response["other_score"]}', True, (255, 255, 255)) screen.blit(text, (0, 0)) pygame.display.flip() keys = pygame.key.get_pressed() changed = player.change_direction(keys) # keep moving in the same direction if it is not changed if not changed: player.move(screen, dt) dt = clock.tick(60) / 1000 pygame.quit() asyncio.run(main())
這將連接您的伺服器並讓您創建並加入一個房間,它會將您的位置更新發送到伺服器並獲取對手的位置和水果位置以及對玩家發出命令的行為要么縮小,要么增長
最後是伺服器程式碼:
import asyncio import websockets import random import json def generate_food_position(min_x, max_x, min_y, max_y): x = random.randint(min_x, max_x) y = random.randint(min_y, max_y) return [x, y, 20, 20] rooms = {} def collide(a , b , width): return (abs(a[0] - b[0]) < width and abs(a[1] - b[1]) < width) # detecting possible collides : def collides(a , b , food ) : head_to_head = collide(a[0] , b[0] ,30) ; head_to_food = collide(a[0] , food ,25 ) head_to_body = False this_head = a[0] for part in b : if collide(this_head , part ,30) : head_to_body = True break return (head_to_head , head_to_body , head_to_food) # return response as (act ,opponents position(s) , my position(s) , the food and the scores) def formulate_response(id , oid , food , roomName) : this= rooms[roomName][id]['pos'] other= rooms[roomName][oid]['pos'] hh , hb , hf = collides(this ,other ,food) act = 'None' if hh : act = 'shrink' rooms[roomName][id]['pos'] = initPlayer() rooms[roomName][id]['score'] =0 # rooms[roomName][oid]['pos'] = initPlayer() # rooms[roomName][oid]['respawn'] = True elif hb : act = 'shrink' rooms[roomName][id]['pos'] = initPlayer() rooms[roomName][id]['score'] = 0 elif hf : act = 'grow' rooms[roomName]['food'] = generate_food_position(20, 580, 20, 380) rooms[roomName][id]['score']+=1 return { 'act' : act , 'opp' : rooms[roomName][oid]['pos'] , 'my' : rooms[roomName][id]['pos'] , 'food': rooms[roomName]['food'] , 'my_score' : rooms[roomName][id]['score'] , 'other_score' : rooms[roomName][oid]['score'] } def initPlayer(): return [[random.randint(30 , 600 ) , random.randint(30 , 400 ) , 30 , 30 ]] async def handler(websocket) : handshake = await websocket.recv() handshake = json.loads(handshake) roomName = handshake["room"] if handshake['choice'] == 1 : rooms[roomName] = {} rooms[roomName]['food'] =generate_food_position(30 ,570 ,30 ,370) rooms[roomName][websocket.id] = { 'socket' : websocket , 'pos' : initPlayer() , 'respawn' : False , 'score' :0 } if len(rooms[roomName]) >= 3 : await broadcast(rooms[roomName]) id = websocket.id while True : room = rooms[roomName] this_pos = await websocket.recv() ## synchrnisation issue with this ## after couple of times they collide head to head ## the cordinates shown on the screen aren't same ## as the server if room[id]['respawn']==True : rooms[roomName][id]['respawn'] = False # generate response : response = { 'act' : 'shrinkall', 'my' : room[id]['pos'] , 'opp' : get_other_pos(get_other_id(id ,room),room), 'food': room['food'] } await websocket.send(json.dumps(response)) else : # update player position this_pos = json.loads(this_pos) rooms[roomName][id]['pos'] = this_pos['pos'] rooms[roomName][id]['len'] = this_pos['len'] other_id = get_other_id(id , room) food = room['food'] response = formulate_response(id ,other_id ,food ,roomName) await websocket.send(json.dumps(response)) def get_other_id(id , room) : for thing in room.keys(): if thing != 'food' and thing != id : return thing def get_other_pos(id , room) : return room[id]['pos'] async def broadcast(room) : for thing in room.keys() : if thing!= 'food' : init = { 'my' : room[thing]['pos'] , 'opp' : room[get_opp(thing, room)]['pos'] , 'food': room['food'] } await room[thing]['socket'].send(json.dumps(init)) def get_opp(id , room) : for thing in room.keys() : if thing!= 'food' and thing != id: return thing async def main(): async with websockets.serve(handler , 'localhost' ,8765 ): await asyncio.Future() if __name__ == '__main__' : print('listenning ... ') asyncio.run(main())
它處理玩家的移動、碰撞和互動。它產生食物、偵測碰撞並同步玩家之間的遊戲狀態。伺服器也管理玩家連線並推送遊戲更新
這是一款多人貪吃蛇遊戲,它可能會成為遊戲世界的下一個重大事件。誰知道呢,它甚至可能會出現在下一次大型科技會議上!
現在,為什麼不嘗試一下,看看可以添加什麼?查看 GitHub 儲存庫並在貪吃蛇遊戲的下一個重大事件中留下您的印記。
快樂編碼!
以上是建立非 Todo 應用程式的東西:線上多人貪吃蛇遊戲的詳細內容。更多資訊請關注PHP中文網其他相關文章!