Это моя первая статья и серия из статьей о написании мультиплеерной игры на 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
Вот и все, в итоге мы получаем мультиплеерную игру с возможностью двигать персонажем, и наблюдать за действиями других.
Знаю коммьюнити Хабра, надеюсь, первая в жизни статья обойдется без хейта в мою сторону. Благодарю тех, кто укажет на мои ошибки, в следующий раз попытаюсь их избежать.
Комментарии (6)
cher11
22.08.2023 15:40+3Для примеров общего концепта - неплохо.
Но явно стоило бы отметить, что в реальных и клиентах и серверах и while true кода поменьше, и данные, скорее всего, гоняются и не в виде JSON, а в бинарном формате (трафик), и не в виде полного дампа (предположу, что летят изменения + есть какой-то механизм синхронизации, детекта её сбоев и доотправки потерянных пакетов).
Было бы интересно про такие оптимизации почитать. А то сейчас, на первый взгляд, сама отрисовка честно выдает 60 кадров, а вот класс клиентов от души терзает сервер настолько быстро, насколько вообще может :)ivankudryavtsev
22.08.2023 15:40Дело не только (и возможно не столько) в трафике (JSON), но и в том, что кодирование, декодирование JSON обычно медленнее, чем имеющиеся форматы типа Protobuf. Что важно для игр с низкой задержкой.
ivankudryavtsev
Не надо заискивать перед сообществом Хабра, тут и хорошую статью заговнить могут, а могут и бездарную залайкать… Статья нормальная, особенно для первой.