Череда проблем длиною в 16 лет

    Не так давно, на заре этого тысячелетия, холодным ноябрьским днем 2004го я сел писать эмулятор сервера для некой онлайн игры. Писалось мне весьма хорошо, на радующем глаз C# и .Net Framework версии 1.1. Особых целей перед собой я не ставил, а опыт имел сравнительно небольшой. По какой-то причине сообщество оценило эту поделку (может, потому что она появилась раньше официального старта основной игры?) и через несколько месяцев я столкнулся с взрывным ростом онлайна, а заодно и серьезными проблемами производительности. Проект прожил 6+ лет, достиг заметных высот (2500 онлайна в пике, порядка 20000 MAU), а затем и почил в бозе. И вот спустя полтора десятилетия я решил сделать собственную ММО игру на тех же “проверенных временем” наработках и столкнулся с аналогичными же проблемами, не смотря на то, что они были уже единожды мною решены.

    P.S. При написании этой статьи не пострадала ни одна IP, изначальный проект хоть и был пропитан духом пиратства (бесплатный сервер платной игры!), никаких прав не нарушал, код правообладателя там не использовался, а сервер базировался целиком на исследованиях честно купленного клиента игры и здравом смысле разработчика. Данный опус рассказывает только о вызовах, с которыми столкнулся автор и оригинальными методами их решения, как в старом, так и в современном проекте. Я заранее прошу прощения за повествовательный стиль рассказа, в противовес простому перечислению фактов.

    Вступление


    Можно сколько угодно спорить о том, что .Net не для серверов, но мне тогда (да и сейчас) показалось весьма здравой идеей, что можно писать логику в виде скриптов, компилировать и подгружать её на ходу, не сильно думая о выделении памяти, сборке мусора, указателях и прочем. По-сути, это позволяет делегировать скриптование бизнес-логики менее квалифицированным разработчикам, ограничиваясь только Code Review. Но для этого надо убедиться, что само ядро работает без сбоев, а оно начинало сбоить уже при 10-15 онлайна, как в 2004-м, так и в 2020-м.
    В 2004-м все крутилось на Windows Server 2003, .Net 1.1, MSSQL 2000. Сервер и хостинг предоставил провайдер Wnet, а затем и был собран новый сервер на пожертвования игроков. Проект был сугубо не коммерческий, а некоторый минимальный доход от баннеров и премиум аккаунтов был пущен на апгрейды.
    Современный сервер работает на Mono под Debian в режиме совместимости с .Net 4.7, с MariaDB для данных, размещается в облаке Hetzner. Давно уже нет того идеалиста с горящими глазами, который верил, что игры должны быть бесплатными, а донат и продажа игровых вещей убивают весь интерес. Сейчас этот персонаж изрядно поседел, сменил задор на опыт и убеждён, что стартап должен приносить как удовольствие, так и доход.

    
Но сказ не об этом, а о самописных серверах и их проблемах.

    Глава 1. Мор



    На локальном компьютере и с одним игроком сервер всегда работал идеально. На выделенном же компьютере и живых игроках, иногда процесс сервера банально закрывался, особенно тяготея к ночным падениям, когда админы спят. Забавно было, когда пользователи звонили в службу поддержки провайдера, те передёргивали питание сервера и на утро причину установить уже было невозможно. Сервер работал как сервис, поэтому перезапускался без проблем, а в логах явных причин падения видно не было. Я несколько раз пробовал запускать процесс из Visual Studio, но дебаггер отваливался через какое-то время, да и производительность сильно страдала. Системный же EventLog оказался удивительно не информативным.

    В первую очередь я решил существенно улучшить систему логирования — она стал перехватывать все, что попадало в Console.Out и Console.Error. Затем был добавлен обработчик UnhandledExceptionHandler, который тоже писал в лог информацию об ошибках. Лог умел работать в режиме AutoFlush = true, что добавляло тормозов, но защищало от оборванных на середине фраз.

    Следующим шагом была переделка сервиса в режим консольной программы и запуск из отдельного окна cmd — так можно было найти это окно и попытаться в нем прочитать последнее, что пытался нам сообщить сервер. Это, кстати, привело к потрясающей проблеме, когда сервер останавливался из-за клика мышью в консольное окно — включался режим выбора текста, останавливавший процесс. На каком-то этапе я писал сразу два лога — через .Net и перенаправлением в >> log.txt.

    К сожалению UnhandledExceptionHandler не помогает с тремя видами исключений: OutOfMemoryException (любое действие в обработчике может потребовать выделения памяти и не будет выполнено), StackOverflowException и исключениям в Unmanaged коде. И как оказалось, у меня были как минимум два из них — Access Violation где-то в нативном коде и OOM.
    Для борьбы с Access Violation я собрался с силами и полностью избавился от нативного кода — никакого больше ZLib (заменен на ICSharpCode.SharpZipLib), никакого OpenSSL (написана собственная реализация SRP-6), никакого нативного MySQL коннектора (выполнен переход на System.Data и MSSQL для БД).

    Отдельно обнаружилось, что выброс исключения в коллбеке от Socket.BeginReceive может заморить весь процесс целиком. Этот коллбек выполняется в .Net Thread Pool (в той части потоков, что зарезервированы для IO Threads) и в случае выброса исключения рушит всю программу, не смотря на UnhandledExceptionHandler. Ну и чуть позже оказалось, что цепочка вызовов BeginReceive->EndReceive->BeginReceive тоже вызывает проблемы и ее надо дорабатывать, чтобы BeginReceive не вызывался из колбека.
    Все это существенно улучшило картину и сервер стал падать гораздо реже, в основном лишь когда заканчивалась память.
    В 2020-м приложение сервера было в принципе только консольным, запущенным в отдельном screen в Linux. Вариантов запуска под Visual Studio более не было, зато логгер с годами стал весьма продвинутым, UnhandledExceptions попадались как зайчики в сети, а нативного кода не было в принципе. Что, правда, не спасло от падений с OOM и StackOverflowException. Глубина стека в случае StackOverflowException выросла в десятки раз, забивая сотни килобайт лога однотипными сообщениями и отказываясь писать нормальный стек трейс. Но в любом случае перенаправление в >> log.txt быстро позволило понять, кто и где виноват. Отдельно помог Телеграм бот, который сигнализировал, что процесс сервера умер.

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

    Глава 2. Глад



    Сервера в начале века могли иметь невероятное количество памяти — на домашнем компьютере я довольствовался 256 Мб ОЗУ, а на сервере был целый гигабайт! И все-равно этой памяти не хватало, она медленно, но неуклонно съедалась, она утекала как вода и с повышением онлайна счет до падения уже шел не на дни, а на часы — каждый раз запуская сервер я с ужасом ждал, когда нехватка ресурсов вызовет OOM или что-то еще похлеще. Конечно, я не сидел сложа руки — неоднократно пытался подключиться к работающему серверу с Visual Studio (при нехватке памяти это превращалось в пытку, а затем в падение), пытался делать дамп памяти через утилиты WinDbg (неуспешно), один раз умудрился запустить сервер из-под dotTrace (ненадолго). Хорошим решением стала установка еще одной планки памяти на гигабайт, а затем и вынос БД на отдельный компьютер. Но проблема оставалась — память сервера медленно, но уверенно подходила к планке 1.7Гб, после чего происходил краш. Настройки файла подкачки не помогали. Отдельно все чаще я стал замечать загрузку процессора в 100%. Сервер откровенно голодал, ему не хватало ресурсов, но я не мог понять, почему — даже на моем домашнем компьютере за несколько часов работы он не выбивался из разумных ~100 мегабайт. Настоящим откровением стали статьи Maoni Stephens и Rico Mariani про GC, дурацкое слово LOH (Large Object Heap) и организацию памяти в .Net. Как казалось, все сетевые операции фиксировали (pin) буферы в памяти, что автоматически отправляло их в Gen 2, а в случае крупных размеров — прямиком в LOH, особый круг преисподней для самых толстых больших объектов. Я боролся с ними как молодой Давид — писал собственные пулы для буферов, переиспользовал массивы как мог, реализовал собственные коллекции, которые для работы брали массивы из пула и потом возвращали их туда (напомню, это в .Net 1.1 без Generics!). Но Голиаф крепко стоял на ногах — память хоть и чуть медленнее, но все-таки утекала, голод не отступал. Тогда я собрался с силами и стал выделять буферы через Marshal.AllocHGlobal в куче и сам следил за их удалением (которое происходило почему-то заметно реже, чем создание). Уже было ясно, что утекают именно эти буферы, используемые в отсылке и получении данных по сети. Медленно, но уверенно я искал причину, постепенно начиная подозревать, что загрузка в 100% CPU тоже как-то к этому причастна. В очередной отчаянной попытке я сделал Interop вызовы напрямую к WSASend/WSAReceive (работа с сетью в Windows напрямую, минуя .Net) и в этот момент утечки памяти нормализовались. Это было очень-очень странно, я несколько раз возвращался к родной .Net версии сокетов и каждый раз ситуация повторялась: вызовы BeginSend/BeginReceive не всегда корректно завершались соответствующими коллбеками, не всегда потоки отдавались назад в пул, а это иногда сопровождалось еще и 100% загрузкой CPU.

    Память больше не утекала, но даже это не исправило ситуацию, когда загрузка процессора неожиданно росла, но при этом сервер продолжал работать, принимать соединения и обрабатывать клиентов, только медленно. Вывод списка потоков показывал, что один из них намертво висел с выполнением какого-то кода и потреблял 100% процессора, но это не был мой код!

    И вот, в начале лета 2005го года я прочитал статью про Workstation GC и Server GC и ради них запустил сервер на .Net 2.0 Preview. И это был долгожданный прорыв — все вызовы асинхронных методов стали корректно завершаться, а посторонняя нагрузка сконцентрировалась в отдельном GC потоке, который потреблял 5-10% CPU.

    У меня было время и настроение, поэтому я провел десяток экспериментов и точно мог сказать, что вызов сборщика мусора в потоке от Thread Pool в Net 1.1 и Workstation GC мог привести к глухому зависанию этого потока, еще даже до выполнения самого кода вызываемого метода (привет не возврат буфера в пул или кучу!) или иногда после него (привет 100% загрузки).
    Со временем я окончательно отказался от BeginSend/BeginReceive и использовал исключительно Windows IOCP для всех операций с сетью. Сервер уже был полностью многопоточным и соединения обрабатывались параллельно, что было весьма хорошо и прогрессивно, не смотря на большое количество блокировок, которые раньше были толком не видны на фоне OOM или 100% жора процессора.
    Современный сервер с объемами менее 4Гб памяти вызывает ухмылку, а добавить для облачного решения лишние 8-16 гигабайт можно в пару кликов и один рестарт. Тем не менее, когда начала утекать память и загрузка процессора подскочила до 100-150% (из расчета 800% на 8 ядер), я снова почувствовал себя 20-летним студентом, сжигающим гигабайты и гигафлопсы в топке прожорливой машины. Это было странно, не нормально и глупо. Особенно неприятно было, что как и раньше игра продолжала идти нормально (пусть и с лагами), но ничего не прерывалось. Ну, пока не заканчивалась память, конечно.

    За эти годы успели появиться и исчезнуть Lightweight Threads (они же Fibers) из-за которых мы больше не имеем доступа к системным потокам в .Net, только к т.н. Managed Threads, а на Mono еще и нет доступа к ProcessThread — внутри одни только заглушки. Диагностика потоков существенно усложнилась, но зато я теперь использовал собственный Thread Pool, все потоки были просчитаны и именованы, для каждого велась точная статистика, какой из них что сейчас выполняет, сколько времени занимает конкретная задача. За счёт этого довольно быстро получилось отследить, что сейчас неполадки именно в моем коде, а не в системном, а статистика потоков показала, что жор связан с выполнением бизнес логики, просто некоторые действия выполняются в 100 раз чаще, чем должны. Сейчас я не был ограничен в ресурсах, поэтому совершенно спокойно снабдил вызов каждого скрипта и таймера дополнительным логированием, замерил время выполнения каждого события и за неделю экспериментов смог уверенно сказать в чем проблема. Оказалось, что некий NPC пытался атаковать другого NPC и оба были застрявшими в камнях, поэтому не могли двигаться и их попытки стрельбы друг в друга моментально прерывались из-за отсутствия Line Of Sight. Но при этом каждый такт расчета поведения (15мс) они пытались просчитывать путь, начинали стрелять, но из-за невозможности выстрела у орудий не шла перезарядка и на следующий такт все повторялось. За несколько дней игры таких NPC набирались сотни и они в итоге съедали все ресурсы сервера. Решением стала правка поведения и уменьшение ситуаций с застреванием, а заодно небольшое время перезарядки даже для неудачных выстрелов.

    А затем сервер стал зависать.

    Глава 3. Хлад



    Осень 2005 года выдалась не простой — у меня была неопределенная ситуация с работой, аренда квартиры неожиданно удвоилась. Радовал только игровой сервер — онлайн уже исчислялся сотнями, но и там началась проблема — весь мир начал замирать. В лучшем случае продолжали ходить пинги или работать некоторые таймеры. А иногда и замерзало вообще все, прекращался трафик и приходилось убивать приложение сервера и запускать снова. Как и прежде, невозможно было подключиться отладчиком к работающему серверу из-за существенного потребления и тормозов. По каким-то причинам Visual Studio от этого просто падал, либо зависал.

    С каждым днем проблема становилась все более серьезной — сотни человек онлайна невероятно расстраивались, что не могут поиграть и грозились уйти. Админов все так же тяжело было убедить, чтобы они оставляли зависший сервер и давали мне возможность что-то с ним сделать. Но проблема была в том, что и сделать-то я ничего не мог. Неопределенное время было потрачено на борьбу с SOS.dll. Эта замечательная библиотека с именем Son Of Strike должна работать как модуль к WinDbg и может показать .Net потоки, задачи, место выброса исключения и другие полезные вещи. Все бы хорошо, но библиотека очень чувствительна к версии .Net и типу GC. И по какой-то дурацкой причине именно конкретно для моей версии подсоединение sos.dll срабатывало один раз из 50. То версия не та, то подключились, но работать не можем, то Марс ретроградный. Буквально лишь один раз у меня получилось увидеть стену текста и цифр, из которой я вынес одно — у нас deadlock!

    Честно говоря, я был не готов к этому. Диагноз звучал как приговор — наличие перекрестных блокировок это явное следствие плохой архитектуры. И дальше самокопание и депрессия по накатанной — опыта было мало, литературу не читал и ввязался в непонятно что, нет чтобы сайтики рисовать, а в выходные пиво пить, он в сервера полез! С трудом скинув наваждение я начал думать, гуглить и искать варианты. В первую очередь я нашел варианты использовать самописные SpinLock и try/finally для их вызова. Это не защищает напрямую от перекрестных блокировок, но оригинально позволяет определить, кто виноват — потоки, ждущие SpinLock не останавливаются, а явно потребляют процессор, поэтому взгляд на список потоков и последнюю вызванную задачу должен, по идее, показать, кто на чем застрял. В моем случае застревало сразу штук 8 потоков, поэтому пользы от этого не было. Однако, я попробовал решить вопрос административным путем: раз я уже перешел на собственные классы для блокировок, то я их расширил и для каждого потока вел отдельный счетчик, чтобы знать глубину “кроличьей норы”. В итоге вообще все места, где использовалось две и более блокировок я переписал начисто. Это заняло около полугода, такие проблемы встречались повсеместно, но результат того стоил — до самого закрытия сервера дедлоков больше не было.

    А дальше изменилась и ситуация с жильем, работой, а еще силами игроков мы купили новый сервер с Xeon 5130x2 и 8Гб памяти. Онлайн рос до 2000, а затем кратковременно поднялся и до 2500, но потом стал спадать. Игроков уже не устраивало, что сервер работает стабильно, им нужны были возможности, скрипты, таланты и прочая бизнес-логика, с которой я не успевал справляться с должной скоростью и с надлежащим качеством. Но это уже совсем другая история.
    В один из холодных октябрьских дней 2020-го запланированный приход лайв стримеров сорвался, потому что сервер неожиданно завис. Работала авторизация, но нельзя было зайти в мир, Телеграм-бот молчал. Быстрый поиск проблем не показал ничего в логах, не было проблем с памятью, ни один из потоков не голодал. Просто все остановилось. Произнеся вслух несколько раз что-то там про кота из матрицы и женщину непристойного поведения я полез искать дедлок. После покупки Майкрософтом Мигеля де Икаса и Xamarin документация по Mono представляет собой жалкое зрелище — она как бы есть, но не актуальна, либо никуда не ведет. Например, 3/4 данных со страницы про отладку в mono с gdb не применимы и не работают. Я смог подключиться к замерзшему серверу через gdb, но команды call mono_pmip и другие давали невразумительные ответы, в основном про синтаксические ошибки. Каким-то чудом я понял, что gdb хочет, чтобы я приводил параметры и результат команд mono_* к определенным типам и так я в итоге смог получить список потоков, замерших в перекрестной блокировке. Но номера в списке не соответствовали ни результатам команды ps, ни ManagedThreadId из сервера. 
Существенно помогло расширенное логирование, которое я делал для поиска жора процессора — по нему я смог понять, какие пакеты и таймеры выполнялись последними и понемногу стал сужать круг подозреваемых. Как на зло, перекрестная блокировка была не с двумя потоками, а с тремя, поэтому более детальной картины получить не удавалось. Тогда я вспомнил о старых граблях и стал просматривать код на использование блокировок. Как оказалось, за эти годы прошло несколько рефакторингов и SpinLock понемногу заменились на Monitor.Enter/Monitor.Exit, а часто и на простой lock. И тут неожиданно на глаза мне попалась статья Eric Gunnerson, в которой говорится, что можно же делать в разы проще: использовать везде Monitor.TryEnter с таймаутом и если блокировку сделать не удалось, то бросать исключение. Это невероятно простой и очень действенный метод — если где-то вызов TryEnter прождал более 30 секунд и выпал (а логике такие задержки не свойственны), то это место обязательно надо исследовать и проверить, кто мог так надолго забрать себе и не отдать объект блокировки. Посыпая голову пеплом я осознал, что мог и 15 лет назад таким образом все вычистить, не обязательно было изобретать велосипед с подсчетом “глубины норы”. Но может тогда это было и к лучшему.

    Ну а потом к новому проекту, как когда-то и к эмулятору, пришел 4й всадник. Только популярным он стать не успел. Все-таки наличие аж трех критичных проблем прямо на старте проекта его быстро подкосили. Да и игра вышла совсем не мейнстримовой. Но это тоже тема не для этой статьи.

    P.P.S. В статье использованы иллюстрации неизвестного художника Parsakoira с подписью “ChoW#227 :: VOTING :: 4 Horsemen of the Apocalypse”, предположительно с умершего уже сайта conceptart.com:
    https://www.pinterest.com/pin/460141286926583086/
    https://www.pinterest.com/pin/490681321879914768/
    Ads
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More

    Comments 27

      +6
      В 2020 же есть уже net core, который инструментарием и документацией не обижен, по сравнению с mono.

      Третья проблема с локами понравилась — думаю почти каждый сталкивался с ними, когда писал многопоточность =)
        +2
        Чтобы сохранить большую часть старого кода, разумно было мигрировать только на .Net 5.0. А он вышел уж слишком поздно. Я на тестовом сервере перенешел на него, порадовался появлению давно забытых на Mono номеров строк в Exceptions релизной версии. Да и все на этом.
        Но это было уже весной, и он отнес ёлку обратно
          0
          У меня возникло два вопроса?
          1. Почему код сокетов не был переписан на асинхронные сокеты (это не те что async/await, а те что SocketAsyncEventArgs)?
          2. Почему не был осуществлен переход на Core 2.x. Я в одном из проектов с Mono под ARM благополучно перешел на core под ARM, а уж 3.1 там поддерживал любые изыски.
            0
            1. Внутренность BeginSend и SendAsync существенно не отличается — оба вызывают тот же самый WSASend, разница только в том, как именно выстреливается колбек. Ну и производительность обоих методов заметно ниже, чем у IOCP.
            2. Для какой цели? Core 2.x вышел в 2018м году, его производительность уступала Mono на Linux и особенно .Net 4.7 на Windows, а совместимость с старым кодом была не очень хорошей. Отрефакторить же 200000 строк кода бизнес логики ради перехода на него было бы откровенной ошибкой. Уже в 3.0 производительность была получше, но миграция все еще вызывала вопросы. Только .Net 5.0 позволил мигрировать без изменения бизнес логики, только с небольшим допиливанием ядра. Его производительность все еще чуть меньше, чем у Mono, но порядок тот же и этим уже можно пользоваться без сильной головной боли. Следующий проект будет уже на нем.
              0
              1. Там же важнее переиспользование структур SocketAsyncEventArgs — что дает меньший расход памяти и уменьшает нагрузку на GC.
              2. На уровне .NET Standard 2.0 код нормально шарился между платформами. В крайнем случае можно было настроить multitargeting на netcoreapp2.1, netstandard2.0 и net47. А на .NET Core 3 и 3.1 уже даже WPF заработал.

              P.S. Вообще интересно даже что вы там такое использовали, что только .NET 5 нормально стал поддерживать?
                0
                1. Если напрямую сравнивать сравнивать, то вообще BeginReceive/BeginSend в 80% случаев выполняется синхронно, тогда внутренняя структура OverlappedAsyncResult сразу попадает в Gen 0 (или Gen 1, если в отдельном потоке) и удаляется условно очень дешево, по сравнению с буферами. Но, как я писал, использование IOCP или прямой доступ к сокетам еще быстрее и удобнее. А сейчас мы вообще перешли на GameNetworkingSockets от Valve.
                2. Тут накопилось столько всего, что сработал эффект домино — DynamicMethod и ILGenerator.Emit, динамическая кодогенерация через Microsoft.CSharp, объектная БД (DataTable/DataSet), Performance Counters, да и просто множество мест, где порушилась сериализация, is для структур и т.д. При том, что .Net Core 2 работал медленнее, это переделывать вообще не было смысла. Сейчас уже много чего подчищено и код компилируется c TargetFrameworks=net461;net5.0;netstandard2.0, но все-равно местами вылазят потрясающие вещи вроде разного поведения Directory.GetCurrentDirectory (не спрашивайте).
        0
        так а про что статья?
          +1
          Там чуть ниже в тегах написано.
            +3
            я сужу обычно по содержанию. так вот, если отбросить весь сумбур, как я понимаю, проблема была только в том, что автор просто попытался поднять какой-то легаси код (напиленый еще для 1.1 (серьезно?)) без особого желания все это добро пересмотреть/отрефакторить, наткнулся на проблемы с дедлоками…… и все?
            про это статья?
              +3
              Тут две разные истории. Легаси код начинался с версии .Net 1.1, дожил до полноценного перехода 4.0, был хорошо вычищен и отрефакторен, достиг неких высот.
              Вторая история про то, как через много лет, уже далекий наследник этого кода (опять же, переписанный и вычищенный не один раз) неожиданно повторил тот же самый путь. В основном, потому что при рефакторинге вместе с водой выплеснули и ребенка — старые проблемы забылись, их методы решения тоже.
              Я соглашусь, что вышло в целом сумбурно — много рассуждений, мало технических деталей, мало описаний инструментов, только основные подходы к решению проблем.
          +1
          Забавное дежавю.
          Чтобы один раз пройтись «по тем же граблям» — было наверное у каждого, но чтобы так последовательно — у себя не припомню.

          ПС
          По третьей проблеме — на счастье дедлоки чуть ли не единственная проблема многопоточности, которая имеет формально корректные решения (разумеется чем раньше эти решения появились — тем проще корректировать).
          Начиная от «берём блокировки в том порядке, который никогда не приведёт к дэдлоку» и заканчивая «если дэдлок появился (в графе блокировок цикл, или как у вас по внешнему поведению) — убиваем вовлечённые в него процессы).
            +1
            Супер интересно, спасибо за статью. Даже не предполагал, что на дотнете в такие проблемы зарулить можно.
              +2

              Чем дальше читал статью, тем сильнее поступала перед глазами строчка "мыши плакали, кололись, но продолжали жрать кактус". Не в обиду, это во мне низкоуровневый прогер, вдоволь покатавшийся по граблям синхронизации, говорит. Вот уж не думал, что в высокоуровневом коде есть такие же проблемы, да ещё уходящие корнями в библиотеки.
              Автору — респект однозначно.

                +2
                В Camelot Unchained разрабы для тестирования производительности серверов (и клиента) активно подключает ботов, которые по сути являются полноценными клиентами — отправляют и принимают пакеты, имеют заданное поведение.

                Такое тестирование позволило вывести производительность на очень хороший уровень. 2000-3000 активно воюющих «игроков» в пределах одного взгляда игрока. Без лагов и тормозов сервера/клиента. Так наверное эффективнее искать проблемные места, чем на живых игроках.
                  0
                  Да, это очень хороший вариант. Не раз о нем думал, но так и не дошли руки его реализовать.
                    0
                    Да, это очень хороший вариант. Не раз о нем думал, но так и не дошли руки его реализовать.

                    Заодно потом можно бота для игры продавать, дорого. И самому с ним отчаянно, денно и нощно бороться! :)).
                      0
                      Знаете, я и так сделал весьма мощный АИ (не нейросеть, скриптовый), с ним интересно играть, потому что у него есть полная информация. Конечно, это не полная имитация игроков — он не может прокачиваться и брать квесты. Но чтобы сделать именно подключаемых по сети ботов, надо потратить вагон времени на игру с неполной информацией — без знания всей карты, положения врагов, точек куда лететь для захвата флага и т.д.
                        0
                        Вы знаете — играть против АИ, у которого есть полная информация как раз очень НЕинтересно.
                        Сразу убивает весь метагейм, нестандартные ходы и т.п.
                        Остаётся только поиск «узких дырок» в логике бота (ну или пере-«фармить \ строить \ дамажить \… (подставьте что именно у вас в игре важно)»)
                          0
                          Верю. Но игра в целом соревновательная (шутер) и в обычном ПВП режиме у игрока преимущество за счет правильного использования навыков, бонусов и т.д. Поэтому это хороший соревновательный элемент. А уже именно для интересного геймплея нужны живые соперники, но их не оказалось.
                  +1
                  Это, наверное, лучшая статья которую я читал на хабре. Благодарю!
                    0
                    Интересно, а ты делал игру целеком один?
                      0
                      Нет, если самому все делать, то можно с ума сойти :) В случае с эмулятором за все время человек 15 поучаствовало, кто базы наполнял, кто скрипты писал. В новом же проекте я собрал маленькую инди фирму. Пусть и кодеров было только двое, но это тоже немало.
                      0
                      Помню, в те времена ты говорил, что планируешь использовать движок в целом для онлайн игр, а не для конкретного эмулятора.

                      Судя по истории, применить больше нигде не удалось?
                        0
                        За это время было целых три проекта, которые использовали этот движок. И все закрылись до релиза или сразу после него. До боевого тестирования по-сути дожил только последний, четвертый.
                          0
                          Ну и отлично, главное попытки =).

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

                          Спасибо за проект! =)

                          ПС Игра реверсилась своими силами, кстати?
                            0
                            ПС Игра реверсилась своими силами, кстати?

                            На самом деле у нас было большое, хоть и не очень дружное сообщество разработчиков и многие исследования делались совместно. У меня хорошо выходило копаться в протоколе, кто-то был гуру в шифровании, один коллега мастерски расшифровывал БД клиента. Ну а когда кто-то играл на официальном сервере и выкладывал дампы, то это было праздником для всех :)
                        0
                        Следующим шагом была переделка сервиса в режим консольной программы и запуск из отдельного окна cmd — так можно было найти это окно и попытаться в нем прочитать последнее, что пытался нам сообщить сервер. Это, кстати, привело к потрясающей проблеме, когда сервер останавливался из-за клика мышью в консольное окно — включался режим выбора текста, останавливавший процесс.

                        Прослезился.

                        С одной стороны жалко, что не попал в разработку в начале нулевых (хотя мог, но судьба сложилась иначе), с другой стороны, как приятно, что работаю уже с linux, и таких проблем не знал.

                        Only users with full accounts can post comments. Log in, please.