我最近偶然发现了科技影响者“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中文网其他相关文章!