Заваривай чай, как обычно. Разговор будет неудобный, потому что я в нём выгляжу не очень умным.
В прошлый раз я рассказывал, как перекраиваю движок под живыми игроками и не роняю прод. Технически. А сегодня история про то, как я месяц гордился красивым графиком, а потом посмотрел на второй график и захотел закопаться в свою же пустошь.
Коротко про игру, если ты тут впервые. Wild World это текстовая MMORPG в Telegram. Мрачный остров, по нему ходят выжившие, добывают ресурсы, крафтят, строят базы, дерутся с тварями и иногда друг с другом. Пилю в одиночку по вечерам уже два года. Не стартап, хобби. Никакого маркетингового бюджета, только я и гаечный ключ.
Как это выглядит вживую. Ты высаживаешься на остров с пустыми руками и сухим горлом. Находишь воду, обдираешь кору, из подобранного хлама сооружаешь первый инструмент. Ставишь лагерь, а к ночи на огонёк выходит тварь, и надо решать, встретить её ржавым ножом или отсидеться за наспех сбитой стеной. Дальше мир только растёт. Склады, дроны-сборщики, походы через полкарты на час реального времени, фракции, стычки с другими выжившими. И всё это обычной перепиской в чате, без графики, одним текстом и кнопками.
Часть первая, приятная. Мёртвый проект ожил
Полтора года игра стояла заброшенной. Я к ней не прикасался, а люди всё равно капали. Один-два новых выживших в неделю, будто радиосигнал из пустоши, который никто не выключил.
Потом я вернулся и решил попробовать простую вещь. Просто рассказать про игру. Написал сюда, на Хабр, несколько честных статей про кухню разработки. Адаптировал их на другие площадки. Ничего не покупал, просто писал текст.
И вот тут цифры, ради которых Хабр вообще стоит читать. Смотри на помесячную регистрацию запусков бота.
Реальные данные из прода, SQL под капотом
Один запрос, никакой магии:
SELECT DATE_FORMAT(created_at,'%Y-%m') AS month, COUNT(*) AS nFROM telegram_usersGROUP BY month ORDER BY month;
Живая выжимка (усечённая до интересного):
2025-01 | 113 ← старый пик, ещё на энтузиазме запуска2025-08 | 172025-11 | 12 ← «мёртвый» период, радиосигнал в пустоту2026-03 | 102026-05 | 92026-06 | 183 ← контент-месяц2026-07 | 5 ← только начался
Весь мёртвый год по десять-двенадцать регистраций в месяц. В июне сто восемьдесят три. Я честно порадовался как ребёнок.
Из года, где капало по десять-двенадцать регистраций в месяц, я прыгнул в сто восемьдесят три за один июнь. Пятнадцатикратно. Это тот самый момент, когда ты откидываешься на стуле, делаешь глоток чая и думаешь: ну наконец-то, я нащупал ручку роста.
А потом от нечего делать открываешь вторую вкладку с аналитикой. И чай встаёт поперёк горла.
Часть вторая, неприятная. Куда они все делись
Я собрал простенькую воронку новичка прямо в своей админке. Без сторонней аналитики, просто считаю по своим же таблицам, кто на каком шаге отвалился. И вот что она показала на всей истории проекта.
Персонажей создано за всё время: пятьсот пятьдесят шесть. Из них сделали хотя бы один шаг по карте четыре из пяти. Дошли до пятого уровня меньше десяти процентов. До десятого, где открывается выбор фракции, около шести. Основная масса живых персонажей сидит на первом уровне и никогда с него не двигается.
Теперь самое обидное. Я взял свежую когорту, тех, кто пришёл на волне контента, и посмотрел первые пятнадцать минут их жизни в игре.
Что показала воронка первой сессии (честно, без прикрас)
Считаю по шагам обучающей цепочки (у меня она хранится как обычные квесты с флажком «шаг пройден»), поэтому цифры чистые, это не мутный updated_at.
-
Больше половины новичков создавали персонажа и не делали ни одного действия. Пустой инвентарь, ноль шагов, ушли.
-
Из тех, кто дошёл до крафта, застревал каждый третий.
-
Медиана времени до первого действия ползла вверх, не вниз.
Отдельно скажу честно, чтобы не ловить за руку в комментариях: чистой ретеншн-аналитики на длинной дистанции у меня нет. Поле updated_at у персонажа дёргается фоновым тиком голода и жажды, так что «вернулся на седьмой день» по нему считать нельзя, это будет враньё. Плюс выборка свежей когорты крошечная. Так что я не строю красивых кривых удержания. Я смотрю на грубые, но честные сигналы первой сессии. И они говорят прямо: ведро течёт.
Вот тут до меня и дошло. Я целый месяц лил в ведро воду и любовался тем, как быстро она прибывает. А ведро всё это время текло по шву. Каждый новый человек, которого приводил мой текст, заходил в игру, утыкался в пустоту первых минут и уходил. Сто восемьдесят три регистрации, из которых до нормальной игры добрались единицы.
Приводить людей в дырявую игру хуже, чем не приводить вообще. Потому что человек приходит один раз. Ты сжигаешь первое впечатление, и второго не будет.
Часть третья. Латаю швы
Я отложил рост в сторону и залез чинить первую сессию. Расскажу про три дырки, потому что каждая учит чему-то поверх кода.
Дырка первая. Пустые руки
Новичок появлялся на острове с абсолютно пустым рюкзаком. Игра говорила «иди добывай», но первая добыча небыстрая, а до неё скучно и непонятно. Пол-экрана людей отваливалось именно здесь.
Решение простое до стыда. Стартовый набор. Немного воды, дерева, пары материалов прямо в рюкзак на старте, чтобы человеку было с чем поиграть в первую же минуту.
Как я сделал стартовый набор безопасным для баланса
Я не хардкожу количества в код. Любая балансная циферка у меня живёт в настройках, которые можно крутить из админки без релиза, у каждой есть обоснование и убиваемый рубильник.
// набор выдаётся один раз, идемпотентно, за флажком в логе действийprivate const KIT = [ 'onboarding.starter_kit.water' => [17, '💧', 'Вода'], 'onboarding.starter_kit.wood' => [2, '🪵', 'Древесина'], // ...материалы];public function grant(int $charId, int $tgUserId, int $chatId): ?string{ if ($charId <= 0 || ! $this->enabled()) { return null; // рубильник выключен → байт-в-байт как было } if ($this->alreadyGranted($charId)) { return null; // второй раз не выдаём } // ...выдать и записать флаг}
Весь смысл в том, что фичу можно погасить одним переключателем и откатить поведение к прежнему, не трогая код. Для соло-разработчика это спасение: боишься меньше, экспериментируешь смелее.
Дырка вторая. Обещание, которое игра не могла сдержать
А вот эта дырка мне особенно дорога, потому что она чисто моя, авторская глупость.
Обучение бодро говорило новичку: «скрафти бинт». Отличная первая цель. Игрок открывает крафт бинта, а там нужны водоросли. А водорослей в стартовом наборе нет. И добыть их с ходу нельзя. Игра сама послала человека в тупик, который сама же и построила.
Мало того, во втором варианте подсказки я предлагал скрафтить «факел». Полез проверять и обнаружил, что рецепта факела в базе вообще нет. Я годами вёл людей к предмету, которого не существует.
Мораль про стартовый контент, стоит одной строки
Стартовый набор обязан содержать реальные ингредиенты того первого крафта, к которому ты гонишь новичка. Иначе набор наполняет рюкзак, а обещание остаётся мёртвым. И не обещай предмет, которого нет в игре. Звучит как очевидность. Пока сам не наступишь, очевидностью не станет.
Проверять теперь буду тупо: рецепт обещанного предмета целиком лежит в стартовом наборе, и уровень для ингредиентов не выше, чем у новичка.
Починил так: докинул в набор недостающие ингредиенты бинта, выкинул несуществующий факел из всех подсказок. Прогнал через реального тестового персонажа в живом Telegram, убедился, что бинт теперь реально крафтится, а не показывает «не хватает ресурсов». Мелочь, а именно из таких мелочей и течёт ведро.
Дырка третья. Человек терялся в первые секунды
Даже с полным рюкзаком новичок часто просто не понимал, куда нажимать. Экран, кнопки, а зацепки нет. Я добавил то, что называю холодным открытием. С самого старта у человека всегда горит ровно одна подсвеченная цель, следующий маленький шаг, а не десять кнопок одинаковой яркости. У точки высадки ловится слабый радиосигнал, и первое, куда тянется новичок, это на него. Не стена текста, а одна ниточка, за которую потянул, и она сама выводит на следующую.
Мораль в тон первым двум. Свобода выбора в первую минуту это не подарок, а паралич. Одна ясная стрелка бьёт десять равнозначных возможностей. Внимание новичку надо оркестровать, а не расширять. Всех ли это спасло, судить рано, когорта свежая. Но отваливаться на ровном месте, не понимая, куда ткнуть, люди перестали.
Часть четвёртая. Разворот, от которого мне до сих пор стыдно
Я чинил-чинил воронку, а потом остановился и задал себе вопрос, с которого стоило начать. А откуда я вообще знаю, что моя проблема в удержании, а не в притоке?
Полез в аналитику источников. И вот тут поймал главный урок этой статьи, увесистый как кирпич.
Из пятисот пятидесяти шести регистраций у меня было размечено по источнику ровно две. Две. Всё остальное, включая весь июньский всплеск на сто восемьдесят три человека, лежало безымянной кучей «откуда-то пришли».
Как я месяц оптимизировал воронку, не видя её вершины
Механизм разметки у меня был. Бот при старте умеет принять метку из ссылки и записать, откуда пришёл человек:
// ссылка вида t.me/mybot?start=src_site_home// значение метки залетает в /start первым касанием$source = $this->extractAcquisitionSource($message); // санитайз [a-zA-Z0-9_-]$user->acquisition_source = $source; // пишем один раз при создании
Приёмник построен, колонка есть, дашборд есть. А кормить его я забыл. Все кнопки «играть» на моём собственном сайте вели на голую ссылку бота, без всякой метки:
// былоpublic string $botLink = 'https://t.me/wildworldrpg_bot';
То есть весь трафик с сайта, самой верхушки воронки, приходил безымянным. Я построил счётчик посетителей и повесил его в чулане, куда никто не заходит.
Починка на один вечер: маленький помощник, который лепит к ссылке метку источника, и по метке на каждую поверхность сайта.
public function botStart(string $source = ''): string{ $source = preg_replace('/[^a-zA-Z0-9_-]/', '', $source) ?? ''; return $source === '' ? $this->botLink : $this->botLink . '?start=' . $source;}// шапка → src_site_header, подвал → src_site_footer, статья → src_site_post ...
Понимаешь иронию? Я неделями крутил воронку удержания, спорил сам с собой про стартовые наборы и подсказки, а на входе стояла табличка, которую я не удосужился прочитать, потому что сам же её и не подписал. Нельзя чинить то, чего не измеряешь. И самая коварная слепота это когда инструмент измерения у тебя есть, но ты забыл его включить.
Теперь метки стоят. Со следующей волны контента я наконец увижу, какая площадка реально приводит людей, а не буду гадать на кофейной гуще из пустоши.
Где всё это сейчас
Голые цифры проекта на сегодня, чтобы было ощущение масштаба этого хобби. Тысяча восемьдесят один человек хоть раз запускал бота. Пятьсот пятьдесят шесть создали выжившего. Прямо в эту секунду в мире крутится больше двух тысяч фоновых задач, стоит под семьдесят баз, а самый упорный игрок добрался до двести двенадцатого уровня. Под капотом больше двухсот тысяч строк на PHP и CodeIgniter, четыре с лишним сотни миграций и пара тысяч тестов на строгом уровне статанализа. Один человек, вечера, гаечный ключ.
Вывод, к которому я приполз на четвереньках. Рост и удержание это не «сначала одно, потом другое». Это одна система с обратной связью, и слепое место в любой её части обесценивает работу в остальных. Можно месяц лить воду и не заметить дыру. Можно месяц латать дыру и не заметить, что кран ты так и не открыл толком. Единственное лекарство скучное: сначала научись видеть всю трубу целиком, потом трогай вентили.
Пойду открывать кран. С уже подписанными табличками.
И честно, чтобы не оставлять тебя с картинкой одних руин. Три дырки из этой статьи уже заштопаны и стоят на проде. В рюкзаке новичка лежит стартовый паёк, обещанный бинт крафтится с первой минуты, а холодное открытие ведёт за руку по первым шагам. Так что первые пятнадцать минут встретят тебя не пустотой, а живым островом. Дальше уже сам.
Если хочешь посмотреть на этот живой мир изнутри, он весь помещается в чат.
👉 Запустить Wild World в Telegram — бесплатно, прямо в переписке, скачивать нечего.
А в комментариях расскажите, ловили ли вы себя на том, что оптимизируете метрику, которую забыли начать мерить. Хочу знать, что я не один такой.
ссылка на оригинал статьи https://habr.com/ru/articles/1056412/