Это моя первая статья и серия из статьей о написании мультиплеерной игры на Python с использованием библиотеки Pygame. В ходе этих "серий" мы напишем небольшую 2Д игру-стрелялку с мультиплеерным режимом.
Как работает мультиплеер? Введение
В мультиплеерных играх клиенты игры взаимодействуют с сервером, получая данные от него и отправляя новые(свои). Request - отправка своих данных, response - полученные данные от сервера. Клиент может отправить данные о своем передвижении, сервер проверяет это и меняет позицию игрока. А сервер постоянно делится данными с клиентами, отправляя им общие данные о позиции, полете и прочей игровой информацией.

Пишем базовую часть игры
Для начала напишем код для игры, чтобы создавалось окно, и локально выводился спрайт игрока. Использовать для этого как говорил ранее мы будем библиотеку Pygame. Она предназначена для создания легких игр в 2Д пространстве, и используется в частности программистами начального и среднего уровней, которые заинтересованы в создании игр и улучшении собственных навыков кодирования на Python.
import pygame from player import Player pygame.init() # Инициализируем pygame sсreen = pygame.display.set_mode((800, 600)) # Создаем окно с разрешением 800x600 clock = pygame.time.Clock() # Создаем объект для работы со временем внутри игры player = Player() while True: for event in pygame.event.get(): # Перебираем все события которые произошли с программой if event.type == pygame.QUIT: # Проверяем на выход из игры exit() sсreen.fill((0, 0, 0)) # Заполняем экран черным sсreen.blit(player.image, player.rect) # Рисуем игрока pygame.display.update() # Обновляем дисплей clock.tick(60) # Ограничиваем частоту кадров игры до 60
Переходим к созданию класса игрока. В нем мы пропишем только отрисовку, и загрузим изображение спрайта, пока что мы сделаем ему координаты по центру экрана, чтобы мы увидели этого человечка.
import pygame # Создаем класс, который взаимствован от класса Sprite внутри pygame class Player(pygame.sprite.Sprite): # Инициализация def __init__(self): pygame.sprite.Sprite.__init__(self) # Загружаем спрайт игрока self.image = pygame.image.load("player.png").convert_alpha() # (400, 300) размеры экрана / 2, не стал делать в переменной self.rect = self.image.get_rect(center=(400, 300))
В итоге у нас получается окно с человечком по центру. Пока что он один, но скоро мы найдем ему друзей, и напишем сервер для обработки его желаний.

Сервер
Напишем сервер для того чтобы игроки (клиенты) могли взаимодействовать друг с другом через него, данные будут приниматься и отправлятся в формате JSON.
Весь код будет полностью, строчки почти все расписаны мной комментариями.
import socket from threading import Thread import json HOST, PORT = 'localhost', 8080 # Адрес сервера MAX_PLAYERS = 2 # Максимальное кол-во подключений class Server: def __init__(self, addr, max_conn): self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.sock.bind(addr) # запускаем сервер от заданного адреса self.max_players = max_conn self.players = [] # создаем массив из игроков на сервере self.sock.listen(self.max_players) # устанавливаем максимальное кол-во прослушиваний на сервере self.listen() # вызываем цикл, который отслеживает подключения к серверу def listen(self): while True: if not len(self.players) >= self.max_players: # проверяем не превышен ли лимит # одобряем подключение, получаем взамен адрес и другую информацию о клиенте conn, addr = self.sock.accept() print("New connection", addr) Thread(target=self.handle_client, args=(conn,)).start() # Запускаем в новом потоке проверку действий игрока def handle_client(self, conn): # Настраиваем стандартные данные для игрока self.player = { "id": len(self.players), "x": 400, "y": 300 } self.players.append(self.player) # добавляем его в массив игроков while True: try: data = conn.recv(1024) # ждем запросов от клиента if not data: # если запросы перестали поступать, то отключаем игрока от сервера print("Disconnect") break # загружаем данные в json формате data = json.loads(data.decode('utf-8')) # запрос на получение игроков на сервере if data["request"] == "get_players": conn.sendall(bytes(json.dumps({ "response": self.players }), 'UTF-8')) # движение if data["request"] == "move": if data["move"] == "left": self.player["x"] -= 1 if data["move"] == "right": self.player["x"] += 1 if data["move"] == "up": self.player["y"] -= 1 if data["move"] == "down": self.player["y"] += 1 except Exception as e: print(e) break self.players.remove(self.player) # если вышел или выкинуло с сервера - удалить персонажа if __name__ == "__main__": server = Server((HOST, PORT), MAX_PLAYERS)
В данном коде мы прослушиваем заданный айпи адрес, и ищем новых подключений. Если найден игрок для подключения, то запускаем функцию в новом потоке которая проверяет запросы игрока и отвечает на них.
Клиент. Отображение всех игроков
Так, мы написали сервер. Но с одним сервером игрок не сможет ничего сделать, для сервера нужно написать клиент. Чтобы подключиться к нему и получать результаты наших запросов, надо написать клиент, который будет получать информацию о других игроках, и перемещаться в мире.
import socket import json from threading import Thread class Client: def __init__(self, addr): self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.sock.connect(addr) # подключаемся к айпи адресу сервера self.players = [] # Создаем массив для хранения данных об игроках Thread(target=self.get_players).start() # Делаем новый поток с циклом, в которым берем данные об игроках def get_players(self): while True: self.sock.sendall(bytes(json.dumps({ "request": "get_players" }), 'UTF-8')) # Отправляем серверу запрос для получения игроков received = json.loads(self.sock.recv(1024).decode('UTF-8')) self.players = received["response"] # сохраняем результат запроса в переменную def move(self, side): self.sock.sendall(bytes(json.dumps({ "request": "move", "move": side }), 'UTF-8')) # Отправляем серверу запрос для получения игроков
В этом коде мы прописали подключение к серверу и функции для обращения к нему. В нашем случае это get_players, который отвечает за получение всех игроков в мире, и move, отвечающая за перемещение в этом же мире.
Добавляем в класс Player возможность задать координаты.
# Создаем класс, который взаимствован от класса Sprite внутри pygame class Player(pygame.sprite.Sprite): # Инициализация def __init__(self, pos): pygame.sprite.Sprite.__init__(self) # Загружаем спрайт игрока self.image = pygame.image.load("player.png").convert_alpha() self.rect = self.image.get_rect(center=pos)
Немного изменим главный файл отображения мира, добавив в него возможность инициализировать клиент, отображать других игроков, и заставлять себя двигаться, используя функции класса Client. Для отображения мы создаем новый спрайт класса Player и перемещаем его в нужную нам позицию.
import pygame from player import Player from client import Client pygame.init() # Инициализируем pygame HOST, PORT = "localhost", 8080 # Адрес сервера client = Client((HOST, PORT)) # Создаем объект клиента sсreen = pygame.display.set_mode((800, 600)) # Создаем окно с разрешением 800x600 clock = pygame.time.Clock() # Создаем объект для работы со временем внутри игры while True: for event in pygame.event.get(): # Перебираем все события которые произошли с программой if event.type == pygame.KEYDOWN: if event.key == ord('a'): client.move("left") if event.key == ord('d'): client.move("right") if event.key == ord('w'): client.move("up") if event.key == ord('s'): client.move("down") if event.type == pygame.QUIT: # Проверяем на выход из игры client.sock.close() exit() sсreen.fill((0, 0, 0)) # Заполняем экран черным for i in client.players: print(i) player = Player((i["x"], i["y"])) sсreen.blit(player.image, player.rect) # Рисуем игрока pygame.display.update() # Обновляем дисплей clock.tick(60) # Ограничиваем частоту кадров игры до 60
Вот и все, в итоге мы получаем мультиплеерную игру с возможностью двигать персонажем, и наблюдать за действиями других.

Знаю коммьюнити Хабра, надеюсь, первая в жизни статья обойдется без хейта в мою сторону. Благодарю тех, кто укажет на мои ошибки, в следующий раз попытаюсь их избежать.
