Главная » Хабрахабр » [recovery mode] Как я свою онлайн игру создавал. Часть 1: Работа с сетью

[recovery mode] Как я свою онлайн игру создавал. Часть 1: Работа с сетью

У меня недавно был отпуск, и появилось время спокойно попрограммировать свои домашние проекты. Привет всем! Точнее, простенькую 2D стрелялку. Захотел я, значит, свою простенькую онлайн игру сделать на Rust. Так как жанр предполагает экшен во все поля, поэтому решил использовать протокол UDP. Решил сначала сделать сетевую часть, а там уже видно будет, что да как. Понял что можно это все вынести в отдельную библиотеку. Начал проектировать архитектуру сетевой части. б) Может она еще кому-то пригодится и принесет пользу. Получившуюся библиотеку я еще и на crates.io залил, под лицензией MIT, потому, что: а) Мне самому будет ее потом удобнее оттуда в свои проекты подключать. За подробностями добро пожаловать под кат. Назвал библиотеку Victorem что в переводе с латыни значит победоносный, приносящий победу.

Ссылки

-> Исходники
-> Библиотека на crates.io
-> Документация

Пример использования

Клиент

//Подключаем нашу библиотеку
use victorem; fn main() -> Result<(), victorem::Exception> {
//Создаем сокет, который слушает порт 11111 и отправляет данные на адрес 127.0.0.1:22222 let mut socket = victorem::ClientSocket::new("11111", "127.0.0.1:22222")?; loop {
//Отправляем байты на сервер socket.send(b"Client!".to_vec());
//Пытаемся прочитать данные от сервера. В случаем успеха преобразуем байты в строку и выводим результат в консоль socket.recv().map(|v| String::from_utf8(v).map(|s| println!("",s))); }
}

Сервер

//Подключаем нашу библиотеку
use victorem;
use std::time::Duration;
use std::net::SocketAddr; //Собственно, наша игра. В ней будут храниться все данные нашей игры и вся ее логика.
struct ClientServerGame; //Реализуем для нашей игры протокол Game, чтобы ее можно было запустить на нашем сервере
impl victorem::Game for ClientServerGame {
//Вызывается, когда от клиента приходит команда. Возвращает булево значение и если возвращает false, то сервер останавливается. fn handle_command(&mut self, delta_time: Duration, commands: Vec<Vec<u8>>, from: SocketAddr) -> bool { for command in commands { String::from_utf8(command).map(|s| println!("{}",s)); } true }
//Вызывается сервером автоматически раз в 30 миллисекунд. Если вернуть пустой массив байт, то он не будет отправлен. Если же в векторе есть данные, то отправляем их на сервер. fn draw(&mut self, delta_time: Duration) -> Vec<u8> { b"Server!".to_vec() }
} fn main() -> Result<(), victorem::Exception> {
//Создаем сервер, который будет передавать данные нашей ClientServerGame и будет слушать порт 22222 let mut server = victorem::GameServer::new(ClientServerGame, "22222")?;
//Запускает бесконечный цикл игры и блокирует текущий поток. server.run(); Ok(())
}

Внутренне устройство

Вообще, если бы я использовал для сетевой части Laminar а не сырые UDP сокеты, то код можно было раз в 100 сократить, а так я использую алгоритм описанный в этой серии статей — Сетевое программирование для разработчиков игр.
Архитектура сервера предполагает получение от клиентов команд (например, нажатие клавиши мыши или какой-нибудь кнопки на клавиатуре) и отправка им состояния (например, текущую позицию юнитов и направления куда они смотрят) с помощью которого клиент сможет отобразить картинку игроку.

На сервере

//Возвращает айди последнего полученного пакета и кодирует в битах u32 числа последовательность пакетов до него, где 0 - пакет получен, 1 - пакет не получен, и клиент должен отправить его повторно. pub fn get_lost(&self) -> (u32, u32) { let mut sequence: u32 = 0; let mut x = 0; let mut y = self.last_received_packet_id; while x < 32 && y > 1 { y -= 1; if !self.received.contains(&y) { let mask = 1u32 << x; sequence |= mask; } x += 1; } (sequence, self.last_received_packet_id) }

На клиенте

//Декодирует из айди последнего полученного пакета (max_id) и закодированной в битах последовательности пакетов после него (sequence) потерянные пакеты. Извлекает их из кеша и возвращает в качестве своего результата
fn get_lost(&mut self, max_id: u32, sequence: u32) -> Vec<CommandPacket> { let mut x = max_id; let mut y = 0; let mut ids = Vec::<u32>::new(); //Если сервер не получил последний отправленный и, следовательно, последний добавленный в кеш пакет, то его тоже нужно повторно отправить. let max_cached = self.cache.get_max_id(); if max_cached != max_id { ids.push(max_cached); } while x > 0 && y < 32 { x -= 1; let mask = 1u32 << y; y += 1; let res = sequence & mask; if res > 0 { ids.push(x); } } self.cache.get_range(&ids) }

Эпилог

Вообще-то, можно было алгоритм доставки команд проще сделать. На сервере принимать только тот пакет у которого айди больше айди последнего полученного пакета на +1, а остальные отбрасывать. Отсылать клиенту айди последнего полученного пакета. На клиенте держать кеш всех команд, которые пользователь пытался оправить серверу. Каждый раз, когда от сервера приходит новое состояние с айди, последнего полученного сервером пакета, удалять из кеша его и все пакеты с айди меньше, чем у него. Все оставшиеся пакеты снова отправляем на сервер.
Далее, когда буду делать уже саму игру, в процессе использования буду дальше улучшать и оптимизировать либу. Возможно, найду еще какие-то баги.

Только там разработка в процессе. Нашел тут проект игрового сервера на C# — Networker + на Rust есть leaf вроде, как аналог гейм сервера на Go — leaf.

S. P. То вот тебе мой совет — не делай так как я. Дорогой друг, если ты новичок и решил почитать мой код к этому проекту и увидишь там тесты, которые я написал. Так делать не надо в продакшене. Я там все в кучу в тестах намешал и не соблюдал шаблон «ААА» (погугли что это). Нормальный тест должен проверять что-то одно, а не несколько условий сразу и должен состоять из этапов:

  1. Ты устанавливаешь свои переменные;
  2. Ты выполняешь действие, которое хочешь протестировать;
  3. Ты сравниваешь получившийся результат с ожидаемым.

Например,

fn add_one(x:usize) -> usize { x+1 } #[test] fn add_one_fn_should_add_one_to_it_argument(){ let x = 2; let expected = x+1; ///////////////////////// let result = add_one(x); ////////////////////////////////// assert_eq!(expected,result); }


Оставить комментарий

Ваш email нигде не будет показан
Обязательные для заполнения поля помечены *

*

x

Ещё Hi-Tech Интересное!

Киберпреступники пять месяцев контролировали ASUS Live Update

Злоумышленники разместили на сервере вредоносный файл с бэкдором, подписанный валидным сертификатом ASUS. Как сообщает «Лаборатория Касперского», хакеры из APT-группировки ShadowHammer 5 месяцев контролировали сервис обновлений ASUS Live Update и заразили более полумиллиона компьютеров по всему миру.Исследователи из «Лаборатории Касперского» обнаружили, ...

Kubernetes 1.14: обзор основных новшеств

14. Этой ночью состоится очередной релиз Kubernetes — 1. По сложившейся для нашего блога традиции, рассказываем о ключевых изменениях в новой версии этого замечательного Open Source-продукта. 14 и сооветствующих issues, pull requests, Kubernetes Enhancement Proposals (KEP). Информация, использованная для подготовки ...