16 KiB
MyGamePlayable — контекст проєкту
Playable ad на three.js + пропрієтарний SDK HitPlay. Цей файл вантажиться в кожну сесію — тримай його інструкцією, а не щоденником.
ПРАВИЛА ВЕДЕННЯ ЦЬОГО ФАЙЛУ
Стеля — 300 рядків. Перевищив — перенеси найстаріше й уже усталене в
docs/worklog/. Історія розробки живе там, не тут.
Що ДОДАВАТИ сюди
- правило або конвенцію, що змінює майбутні рішення («використовуй утиліту X, не пиши формулу заново»);
- граблі, які вкусять ще раз (поведінка SDK/бібліотеки/тулчейна, а не разовий баг у власному коді);
- зміну структури: новий контролер, нова утиліта, новий конфіг-параметр;
- нову команду або публічний API SDK, який не очевидний з коду.
Що НЕ додавати
- «сьогодні зробив…», «Day N», хронологію, цитати з чату;
- «перевірено в браузері / білд пройшов» — це не знання, це подія;
- підібрані числа (
CAMERA_OFFSET, тривалості, відсотки) — вони живуть у коді й там же змінюються; сюди — тільки де важіль, не його поточне значення; - переказ того, що й так видно з коду за 10 секунд.
Куди що писати
| Знання | Місце |
|---|---|
| Пояснює один файл/рядок | коментар у коді |
| Перетинає кілька файлів або є політикою проєкту | CLAUDE.md |
| «Чому ми дійшли саме до цього» | docs/worklog/ |
Коли граблі усунені в корені
Якщо гочтя закрита утилітою — заміни розповідь одним рядком з посиланням на
утиліту. Приклад: .chain()-баг → src/utils/tweenChain.ts. Довга версія
лишається в docs/worklog/02-day6-tween-animations.md.
Новий запис у worklog
docs/worklog/NN-коротка-англійська-назва.md, NN — наступний номер.
Додай рядок у docs/worklog/README.md. Один H1 на файл.
Стек
- three.js
0.185(НЕ Babylon), обгорнутий у SDK@hitplay/playable_template@hitplay/ads_common(приватні пакети HitPlay).
- Фізика:
cannon-es. SDK сам кличеworld.fixedStep()щокадру (Physics_internal.update) — свійstep()/fixedStep()НЕ викликати. Ініт —Physics_internal.init(...)уbeforeResourcesLoadedCb.ts. - Твіни:
@tweenjs/tween.jsчерез SDK-обгорткуTweenC.TweenC.init()треба покликати один раз (є вbeforeResourcesLoadedCb.ts), інакшеTweenC.add()тихо нічого не анімує. - VFX:
three.quarks— дизайнерські префаби з quarks.art (.json). - Звук
howler, ввідnipplejs, дебаг-панельlil-gui, аналітикаbytebrew-web-sdk. - Тулчейн: Vite 6 + TypeScript 7.
UpdateController.Instance.onUpdate — єдиний update-івент, delta в секундах.
ThreeC.removeFromScene(obj) — видалення спавнених мешів.
Команди
| Команда | Що робить |
|---|---|
npm start |
vite dev-сервер |
npm run export |
single-file HTML → export/output_export_browser_en.html |
npm run build |
zip для платформи 24ADS → zip/ |
npm run update |
оновити @hitplay/ads_common + playable_template |
Перевірка змін — npx vite build (у проєкті прийнято верифікувати саме білдом).
stats.html — rollup-visualizer, дивитись коли розпухає бандл.
Структура
src/index.ts точка входу, Template.initConfig({...})
src/controllers/*C.ts одна система = один файл, суфікс C, плоский список
src/configUIParams/ категорії конфігу, які рендерить платформа 24ADS
src/templateConfig/*Cb.ts лайфсайкл-колбеки SDK
src/utils/ спільні формули (див. нижче)
src/resources/ meshes/images/fonts/vfx/sounds
src/css/ main.css, ui.css, health-bar.css
docs/worklog/ архів історії розробки
Підпапки в controllers/ свідомо не зроблені — SDK сам плаский, вигоди нуль.
Утиліти — використовувати, не переписувати
perspectiveScale.ts— компенсація перспективи при польоті до/від камери.screenWorldProjection.ts— world↔screen в обидва боки.tweenChain.ts— коректна реєстрація ланцюжка твінів.flashOverlayMaterial.ts— адитивний флеш-оверлей.
DI-патерн
Контролери не імпортують одне одного напряму, коли це створить цикл — інжектиться
геттер/колбек, а зв'язує TestSceneC:
ResourceC.setFlyTarget(), HudC.setBalanceGetter(),
CameraFollowC.setFacingTargetGetter(), BreakablePropC.onHit/onDestroyed.
Конфіг платформи ↔ код
Платформа 24ADS рендерить ліву панель із категорій. Частина — наші
(src/configUIParams/), частина — стандартні від SDK.
| Вкладка на платформі | Джерело |
|---|---|
| Analytics params | analytics.ts |
| Install Banner Settings | installBanner.ts |
| Scene settings | globalSettings.ts (id категорії — global) |
| Gameplay tuning | gameplay.ts |
| Audio Settings | sounds.ts |
| Screen Orientation | orientation.ts |
| Filter scene, Redirect Settings, Loader Settings, Font Settings | SDK, не наш код |
Читати значення в коді — Template.getCategory("<id категорії>")["<id параметра>"]
(приклад — DebugConfigC.init()).
Scene settings (globalSettings.ts): light_intensity, light_color,
camera_fov_p/l, camera_position_p/l, camera_rotation_p/l.
Формат configUIParams ≠ формат рантайму. У визначенні кожна вісь масивного
параметра — це [min, max, default]; у грі Template.getCategory() віддає
готовий [x, y, z]. Не переплутати: [-20, 70, -6] — це не координати.
Камера: рівно один писар на camera.rotation — CameraFollowC.
camera_position_* — це офсет відносно гравця (не абсолютна позиція, камера
слідкує), camera_rotation_* — фіксований кут у градусах, camera_fov_* — FOV.
Розподіл власності: FOV застосовує CameraC.setCamera(), риг —
CameraFollowC.applyRigFromConfig(). Не повертати position/rotation у
CameraC.setCamera(): він перезапускається на кожну зміну будь-якого camera_*
поля, і два писарі на кут ламали камеру назавжди до кінця сесії.
CAMERA_OFFSET у TestSceneC лишився тільки як фолбек.
Gameplay tuning (gameplay.ts) дублює те, що дев-панель DebugConfigC
(клавіша C, тільки lil-gui, не потрапляє в білд) показує локально. Панель
сідить свої дефолти з цієї категорії при старті — один раз, без live-sync.
Живі граблі
Кожна вже кусала. Довгі розбори — в docs/worklog/.
glTF-завантажувач стирає крапки з імен нодів. У сирому GLB UI_Wood.001, у
живій сцені — UI_Wood001. getObjectByName() тихо повертає undefined, гард
if (node) завжди хибний, код «виглядає правильним». Перевіряти реальне ім'я
через scene.traverse, не через GLB JSON.
Матеріали шаряться між усіма нодами з GLB. Хочеш змінити матеріал одного об'єкта — клонуй його один раз при білді, інакше зачепиш усі 18 крейтів.
material.color на MeshBasicMaterial не дає флешу. Матеріал unlit, колір
множиться на текстуру, дефолт уже білий — лерп «білий → білий» невидимий.
Для hit-flash — окремий адитивний оверлей-меш (flashOverlayMaterial.ts).
«Чи гравець у зоні X» — тільки через Box3().setFromObject(node). Радіус на
око зі скріншота не збігається з реальним футпринтом (і центр може бути
несиметричний відносно пивота).
Перспектива сама змінює видимий розмір. Об'єкт, що летить до камери,
«росте» без жодного дотику до .scale. Треба явно компенсувати —
perspectiveScale.ts.
Ціль польоту пере-семплювати щокадру. Камера рухається разом з гравцем; взята один раз позиція HUD-анкера застигає й політ промахується.
tween.onComplete зрідка не спрацьовує (причина всередині tween.js не
знайдена) — для критичного клінапу дублювати setTimeout на тривалість + запас.
removeFromScene двічі — нешкідливо.
screen↔world рахувати від renderer.domElement.getBoundingClientRect(), не
від window.innerWidth/Height — SDK рендерить у letterboxed 9:16 бокс.
nipplejs force не обмежений радіусом зони — за краєм кільця росте далі,
хоч точка вже запінена. Клампити один раз при записі, не в кожному calc().
Math.atan2 і CSS conic-gradient мають різний нуль (схід vs північ) —
різниця рівно 90°.
Дві CSS-анімації на одному елементі не живуть разом — перемикання класу,
що чіпає animation-name, перезапускає анімацію. Розділяти на вкладені div.
Анімувати RGB, не альфу, якщо треба «згасання до базового кольору» — інакше на нулі отримаєш прозору дірку замість кольору.
У CSS — --lvh/--lvw, не сирі vh/vw/vmin (ротація/адаптивність).
Експорт — це gzip-блоб, а не текст. НЕ грепати export/*.html напряму.
npm run export пакує весь бандл у base64+gzip (decompressString("H4sIA..."),
розпаковується pako в рантаймі). Будь-який пошук по цьому файлу дасть 0
збігів — для всього, включно з класами самого SDK. Прочитати цей нуль як
«purgecss вирізав наш CSS» — легка й дуже дорога помилка (перевірено: після
розпаковки всі кастомні класи й @font-face на місці, кількість збігається з
vite build). Перевіряти тільки після gunzip.
purgecss справді вирізає .bannerButton — але саме його, точково (клас
призначається всередині мінифікованого SDK-бандла). У розпакованому експорті:
.bannerButton{ — 0, .bannerLogo{ — 1, .bannerButtonText{ — 1. Не писати
CSS-правила під .bannerButton, це мертвий код. На решту класів не поширюється.
Template.setUpFont() працює тільки коли !release — тобто шрифт через
customFont.ts/formFontFamily реєструється для прев'ю платформи, а не для
експорту. У реальному білді шрифт малює наш власний @font-face з main.css
(інлайн base64). Це два незалежні механізми на дві різні поверхні — потрібні
обидва, і поломка одного не видна з іншого.
Інлайн ассетів: картинки — ConvertToBase64WhenRelease("./file.webp"), і
викликати обов'язково з файлу в тій самій директорії, що й ассет. Шрифти —
нічого не треба, assetsInlineLimit у конфігу SDK інлайнить url() у CSS сам.
Ігрові дані
ZombiePunk_Map.glb, група Lootable → ~18 × Wooden_Box_XXX:
Wooden_Box_XXX
BoxCollider.NNN obstacle-бокс
Wooden_Box_States_XXX
Wooden_Box_XXX_S1..S3 стейджі пошкодження, не в усіх є всі три
Крейти можуть стартувати вже пошкодженими (тільки S2/S3) — BreakablePropC
рахує стартовий HP з реальних номерів _SN, не з припущення про 3 стейджі.
UI_Wood — реальна іконка ресурсу (текстура Icon_Wood), клонується на кожен
спавн. UI_Interactive_Zone_02 — Pay Zone. UI_Tool_Zone, UI_Wood001, група
UI — приховані як зайва декорація.
Поточний стан
- Working tree брудний, багато незакомічених змін.
DebugConfigC.tsіconfigUIParams/gameplay.ts— нові, ще не в git.- Відомі борги (відкладені свідомо, за рішенням користувача):
ui.css/health-bar.css— досі наvh, без landscape-медіазапиту;- VFX-емітер
StretchSmokeуVFX_Lootable_Hitвилітає далеко від крейта (дизайнерський ассет тюнили під інший масштаб сцени) — рішення за користувачем: лишити / притлумити / прибрати емітер; - Destroy-VFX підключений, але візуально не підтверджений.