Егор Урванов

Архитектура игры «Житьё»: C4 и критические пути

Схемы C4 до уровня компонентов и четыре критических пути через sequence: запуск, тик симуляции, действие игрока и кадр.

Житьё — симулятор жизни в браузере: район из десяти семей, потребности, карьера, отношения, режимы «Покупка» и «Стройка». Без сборки и сервера: страница, набор ES-модулей и three.js с CDN. Ниже схемы C4 до уровня компонентов и четыре пути, на которых всё держится.

C4: контекст и контейнеры

Уровень 1. Система и её окружение

Игрокмышь, клавиатура«Житьё»игра в браузереCDN jsDelivrthree.js 0.169localStorageсохранение, настройкииграетгрузит библиотекупишет и читает
Своего сервера нет: подойдёт любой статический хостинг.

Уровень 2. Контейнеры

main.jsсборка, цикл, автосохранениеМозгjs/simМирjs/worldЯдрошина, состояниеlocalStoragezhitie.saveUIjs/uiЗвукjs/audioДанныеdata/Рендерjs/renderCDNthree.jsМоделиassets/*.glbtickloadStartLotsave / loadframeframeшина, состояниешиназвуккаталогimport maploadManifest
main.js создаёт шину и состояние, собирает контексты и крутит цикл. Мозг, Мир и UI читают каталог из data/.

C4 уровня 3: компоненты контекстов

Контексты общаются через шину событий и один объект состояния. У каждого факта один хозяин: время и потребности ведёт Мозг, клетки и стены знает Мир, картинку строит Рендер.

Мозг

sim/index.jstick, enqueueautonomy.jsвыбор действияwalk.jsшаги по путиqueue.jsочередь с приоритетамиactions.jsидти, вход, цикл, выходmotives.jsпотребности, настроениеПодсистемыcareer, social, familyhazards, npc, hoodМирfindPath, useSpotsthinkpushItemпервый в очередиstartActionstepWalkmotiveTickHOOKS, EVENTSпуть
Мозг не знает ни DOM, ни three.js: его можно запускать в Node, так и устроены тесты.
  • Автономия оценивает предметы вокруг по кривым потребностей и делит выигрыш на расстояние.
  • Команда игрока встаёт в очередь выше автономной и вытесняет её.
  • Подсистемы подключаются к действиям через хуки завершения шагов, ядро цикла их не знает.

Мир

world/index.jsпубличный APIobjects, wallsfloors, roofcache.jsпроизводные сеткиrooms.jsroomAt, roomScorenav.jsпоиск пути A*spots.jsточки использованияhood.jsрайон, участки, переездhousegen, furnishcommunitylot.jsloadStartLotdata/шаблоны, каталогправкиinvalidateкомнатыпроходимостьfindPathцелиloadLot, moveInстроит домастартшаблоны
Мир хранит участок в состоянии и отвечает на вопросы: можно ли поставить, где пройти, что за комната.
  • Сетки проходимости и комнат считаются один раз и сбрасываются при правке участка.
  • Активный участок живёт в state.lot, остальные хранятся снимками внутри района.

Рендер

render/index.jsinitRender, framecamera.js4 поворота, 3 масштабаenv.jsсвет, небо, тениassets, manifestмодели glblot.jsстены и полыroof, fireghostsobjects.jsпредметыsims.jsжители, анимацииhood.jsвид районаrigupdateloadManifestrebuildupdatesyncsyncshowHood
Рендер только читает состояние и слушает шину; сам ничего в нём не меняет.
  • Объекты и жители пересинхронизируются по событиям шины, а стены ещё и по хэшу раз в полсекунды.
  • Модель не нашлась: рисуется процедурная замена.
  • Шейдеры прогреваются при старте, чтобы не было рывков позже.

UI и звук

ui/index.jsввод, режимы, циклpanel.jsпотребности, времяpie.jsкруговое менюqueue, bubblesочередь, пузыриcamera.jsмышь, клавишиbuy.js, build.jsПокупка, Стройкаhood.js, cas.jsрайон, семьяdialogs, eventsвопросы, событияaudio/index.jsзвук и музыкаupdateopenupdateupdateenter, clickopenupdatesfx, frame
UI получает от main.js мир, мозг и рендер одним объектом и вызывает их напрямую.
  • Звук стартует по первому жесту игрока: браузер раньше не даёт.
  • Громкости и настройки хранятся в localStorage.

Четыре критических пути

1. Запуск и загрузка мира

main.jsХранилищеМозгМирРендерUIload(), если ?continueсохранение или пустоcreateState()seed, addSim ×2loadStartLotточка появленияcreateHoodawait initRenderмодели, прогрев шейдероврендер готовinitUIrequestAnimationFrame
Новая игра сеет генератор случайных чисел, ставит стартовый дом и двух жителей; с сохранением пропускает это.

2. Тик симуляции

main.jssim.tickautonomyactionsМирШинаtick(dt)скорость × минуты; все спят: ускорениепод-шаги по 0,5 минthink, если нет действияpushItem в очередьstartActionfindPathпутьsim:anim, object:changedпотребности раз в 2 минmoney:changed, notify
За кадр проходит несколько под-шагов: потребности тикают редко, действия и ходьба каждый под-шаг.

3. Действие игрока и его эффект

ИгрокUIРендерМозгШинаклик по предметуpick(x, y)object, idinteractionsForпункты и причины отказакруговое менювыбор пунктаenqueueочередь выше автономииobject:changed, sim:animsync, анимация
Дальше действие выполняет тик: идёт, входит, делает, выходит; Рендер реагирует на события.

4. Кадр: рендер и звук

main.jssim.tickUIЗвукРендерХранилищеtick(dt), если режим «Жизнь»frame(dt)frame(dt)петли: огонь, пищалкаframe(dt)sync, вырез стен, крышасвет по часам игрырисование кадраsave() раз в 30 с
Один requestAnimationFrame ведёт всё: симуляция, интерфейс, картинка, звук и автосохранение.

Правила, на которых всё держится

  • Состояние одно. Всё, что нужно сохранить, лежит в одном объекте, который целиком превращается в JSON.
  • Шина событий связывает контексты. Ошибка в обработчике одного слушателя не роняет остальных.
  • Мозг и Мир без DOM и three.js. Их проверяют в Node без браузера.
  • Рендер ничего не решает. Он рисует то, что лежит в состоянии.
  • Игрок и автономия в одной очереди. Приказ игрока всегда важнее.

Слабые места

Где Что не так
Хуки в одном файле Все обработчики шагов действий собраны в sim/index.js на 450 строк.
UI знает про Рендер Создание семьи берёт списки одежды прямо из файла манифеста рендера.
Мир через глобальную ссылку Мозг получает мир параметром и ещё хранит его в общей переменной модуля.
Версия сохранения Сохранение другой версии молча отбрасывается, миграций нет.
Вес загрузки three.js приходит с внешнего CDN, а набор моделей glb грузится при старте.

Об игре

Страница игрыЖитьёСимулятор жизни в духе The Sims 1: изометрический low-poly район из десяти семей
© 2026 Егор Урванов