Maison  >  Article  >  développement back-end  >  Construire quelque chose qui n'est pas une application Todo : un jeu de serpent multijoueur en ligne

Construire quelque chose qui n'est pas une application Todo : un jeu de serpent multijoueur en ligne

PHPz
PHPzoriginal
2024-08-21 06:19:32843parcourir

Je suis récemment tombé sur une citation de l'influenceur tech "the primeagen". Je ne m'en souviens pas exactement mais si je me souviens bien, ça ressemble à ceci :

"Si vous n'échouez pas dans ce que vous faites, alors vous n'apprenez pas."

Cela m'a fait réfléchir à mon parcours de codage. Je suis devenu assez à l'aise avec la construction de backends, au point même d'écrire import express à partir de « express » ; est devenu une corvée.

Au lieu de passer par l'événement canon consistant à apprendre encore un autre framework JavaScript afin de créer ma millionième application de tâches révolutionnaire (car clairement, le monde en a besoin de plus), j'ai décidé de faire autre chose. J'avais lu des articles sur le protocole WebSocket et j'ai trouvé fascinante sa capacité à gérer des messages asynchrones bidirectionnels entre le serveur et le client. Je voulais construire quelque chose avec, mais j'avais besoin d'une pause avec JavaScript.

Après réflexion, j'ai opté pour un simple jeu multijoueur 2D. Cela impliquerait des calculs (détection de collision), des structures de données (listes chaînées, hashmaps) et la synchronisation des joueurs. Un jeu de serpent semblait parfait, avec quelques règles simples :

  1. Manger un fruit fait grandir et ajoute 1 à votre score

  2. S'écraser sur le corps d'un autre joueur vous fait rétrécir, réinitialise votre position de manière aléatoire et remet à zéro votre score

  3. Les collisions face-à-face font rétrécir les deux joueurs, réinitialisent leurs positions et mettent leurs scores à zéro

Tous ces calculs sont effectués côté serveur pour empêcher les joueurs de falsifier la logique du jeu. Nous utiliserons Python 3 avec Pygame pour les graphiques et la bibliothèque websockets pour gérer les messages asynchrones via asyncio.

Maintenant, plongeons-nous dans le code, vous risquez de le trouver compliqué, car rappelez-vous la première règle de programmation :

"Si ça marche, n'y touchez pas."

Vous avez assez lu mes jappements, passons à la partie amusante : Codage. Mais si vous souhaitez éviter les discussions et plonger directement dans le vif du sujet, rendez-vous simplement sur le dépôt GitHub.
Si vous souhaitez contribuer, n'hésitez pas à ouvrir un ticket ou à soumettre une pull request. Toutes les améliorations ou corrections de bugs sont les bienvenues !

Tout d'abord, nous définissons nos structures de données :

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

La classe Object est une classe de base pour les objets de jeu, tandis que la classe Player l'étend avec des fonctionnalités spécifiques au serpent. La classe Player comprend des méthodes pour changer de direction, se déplacer, grandir, rétrécir et rendre le serpent.

Ensuite, nous avons la logique du jeu :

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())

cela vous connectera au serveur et vous permettra de créer et de rejoindre une salle, il enverra une mise à jour de votre position au serveur et obtiendra la position de l'adversaire et la position du fruit et un acte qui sera un ordre au joueur de soit rétrécir, soit grandir

Enfin, le code du serveur :

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())

il gère les mouvements, les collisions et les interactions des joueurs. Il génère de la nourriture, détecte les collisions et synchronise l'état du jeu entre les joueurs. Le serveur gère également les connexions des joueurs et envoie les mises à jour du jeu

Building Something That
Et voilà, un jeu de serpent multijoueur qui pourrait être la prochaine grande nouveauté dans le monde du jeu vidéo. Qui sait, il pourrait même être présent à la prochaine grande conférence technologique !
Pour l’instant, pourquoi ne pas faire un tour et voir ce que vous pouvez ajouter ? Consultez le dépôt GitHub et faites votre marque sur la prochaine grande nouveauté des jeux de serpent.
Bon codage !

Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

Déclaration:
Le contenu de cet article est volontairement contribué par les internautes et les droits d'auteur appartiennent à l'auteur original. Ce site n'assume aucune responsabilité légale correspondante. Si vous trouvez un contenu suspecté de plagiat ou de contrefaçon, veuillez contacter admin@php.cn