私は最近、テクノロジー系インフルエンサー「the primeagen」の引用を偶然見つけました。正確には覚えていませんが、私の記憶では次のようなものです。
「自分のやっていることに失敗していないのなら、学んでいないということだ。」
これをきっかけに、自分のコーディングの道のりについて考えるようになりました。私はバックエンドの構築にかなり慣れてきて、「express」から import Express を書くことさえできるようになりました。面倒なことになってしまった。
100 万番目の革新的な Todo アプリを構築するために、さらに別の JavaScript フレームワークを学習するという標準的なイベントを実行する代わりに (明らかに、世界はもっとそれらのフレームワークを必要としているため)、私は別のことをすることにしました。私は WebSocket プロトコルについて読んでいて、サーバーとクライアントの間で双方向の非同期メッセージを処理できるその機能に魅力を感じました。これを使って何かを構築したかったのですが、JavaScript から離れる必要がありました。
いくつか検討した結果、シンプルなマルチプレイヤー 2D ゲームに落ち着きました。これには、計算 (衝突検出)、データ構造 (リンク リスト、ハッシュマップ)、およびプレーヤーの同期が含まれます。いくつかの簡単なルールを備えたヘビ ゲームは完璧に見えました:
フルーツを食べると成長し、スコアに 1 が加算されます
他のプレイヤーの体に衝突すると体が縮み、位置がランダムにリセットされ、スコアがゼロになります
直接衝突すると、両方のプレーヤーが縮み、位置がリセットされ、スコアがゼロになります
これらの計算はすべてサーバー側で行われ、プレイヤーがゲーム ロジックを改ざんするのを防ぎます。グラフィックスには Python 3 と Pygame を使用し、asyncio を介して非同期メッセージを処理するために WebSocket ライブラリを使用します。
それでは、コードを詳しく見ていきましょう。プログラミングの最初のルールを思い出してください。
「うまくいくなら、触らないでください。」
私の愚痴はもう十分読んだので、楽しい部分である コーディング に移りましょう。ただし、おしゃべりをスキップしてすぐに始めたい場合は、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 中国語 Web サイトの他の関連記事を参照してください。