Бесплатный ИИ делает игру на Godot. gdscript.. gdscript. Godot.. gdscript. Godot. windows.. gdscript. Godot. windows. ИИ.. gdscript. Godot. windows. ИИ. искусственный интеллект.. gdscript. Godot. windows. ИИ. искусственный интеллект. Разработка игр.

Начитавшись и насмотревшись как люди зарабатывают на яндекс играх, решил попробовать написать игру, совершен��о не разбираясь в игроделываниии геймдеве. К тому же мне совершенно случайно попались видео с ютуба о том как использовать мощные LLM совершенно бесплатно. Ноль вложений.

Как я представлял себе процесс разработки: открываешь IDE и пишешь “сделай игровую сцену, бла бла бла…”, и через пару часов – готовый результат. Выкладываешь на яндекс игры, люди играют, реклама показывается – ты в плюсе.

Как выяснилось позже, никто не разрабатывает игру с нуля …за редким исключением. В основном люди покупают в Сonstruct 3 готовые игры/шаблоны, перерисовывают картинки, добавляют уровни т.д.

Тяп, ляп, и в продакшн.

Но мы пойдем тернистым путём проб и ошибок.

На распутье

Итак. Нам нужно сделать игру для яндекс игр. Что такое яндекс игры? Это web страничка на html. Ок. Можно написать игру на html. Но мне хотелось как-то визуально править игровые ресурсы, через какой-нибудь редактор. Не писать же отдельно редактор для html игры?

Есть такая штука как “движок” игры: там можно вставлять свои ресурсы, писать логику, запускать/отлаживать игру. Какие есть игровые движки, без ограничений, бесплатные, с хорошей документацией, стабильные, существующие уже продолжительное время, с возможностью экспортировать игру в html5?

Таким идеальным движком мне показался Godot, с его почти полностью переведенной документацией. Тем более что я когда-то повторял игру по этим урокам на ютуб и экспортировал на android. Даже добавился в телеграмм канал и переписывался. Сейча�� там целое сообщество. Но я совершенно забросил это дело, забыл напрочь всё что делал по урокам.

Сэт Ап

Соберем так называемое окружение для разработки.

Примерная схема разработки

Примерная схема разработки
  1. Во первых скачаем и распакуем куда-нибудь сам Godot (Godot Engine – .NET качать не нужно т.к. насколько я понял, он не поддерживает экспорт в html5). Таким образом языком разработки у нас будет язык GDScript, чем то похожий на python.

  2. Далее устанавливаем Visual Studio Code. Эта IDE будет основным “окном в разработку” игры.

  3. Установим расширение godot-tools: заходим в File – Preferences – Extensions.

  4. Далее в Visual Studio Code установим расширение Kilo Сode: вбиваем в поиск “kilocode.Kilo-Code”. Жмем Install.

    Скрытый текст
    Бесплатный ИИ делает игру на Godot - 2

    И тут важная вещь: необходимо откатиться на определенную версию этого расширения, а именно на 4.142.0 или ниже. Нажимаем на выпадающее меню около Uninstall и выбираем версию. Дело в том, что расширение выше этой версии, сломает нам работу на ИИ Gemini. Но об этом – позже.

    Скрытый текст
    Бесплатный ИИ делает игру на Godot - 3
  5. Устанавливаем расширение Qwen Code Companion

  6. Устанавливаем расширение Gemini CLI Companion

  7. Устанавливаем NodeJS.

  8. Запускаем командную строку cmd, далее в терминале вводим:
    npm install -g @google/gemini-cli@latest

  9. Там же в терминале вводим:
    npm install -g @qwen-code/qwen-code@latest

  10. Скачиваем и распаковываем Qdrant – он нужен Kilo Сode для работы с кодом. Можно скачать отсюда: qdrant-x86_64-pc-windows-msvc.zip.

  11. Скачиваем LM Studio, скачиваем в нём модель nomic-embed-text-v2-moe-GGUF – она нужна для общения Kilo Сode и Qdrant между собой. Настраиваем LM Studio в качестве сервера. Как настраивать LM Studio в качестве сервера ИИ моделей можно прочитать в статье Открываем RAG и интернет для LM Studio (см. “Включаем сервер моделей в LM Studio”).

Важное уточнение

Пока дописывал статью, Kilo Code совсем перестал работать с Gemini CLI, даже старые версии Kilo Code 4.142.0 и ниже перестали работать. Прощай бесплатная Gemini 2.5 Pro… Остается только Qwen3-coder-plus.

Бесплатный ИИ делает игру на Godot - 4

Хотя в Gemini CLI можно конечно работать и без Kilo Code, напрямую, в консольной утилите от гугла.

Настройки

Запускаем Godot вручную и создаем новый проект например в папке puzzle. Т.к. мы разрабатываем с последующим экспортом в html5, выбираем “Отрисовщик: Совместимость”. После чего можно закрыть Godot.

Скрытый текст
Бесплатный ИИ делает игру на Godot - 5

В VSCode выбираем File – Open Folder…, указываем папку где только что создали проект: puzzle.

Godot-tools

Вообще изначально я предполагал что это расширение нужно для разработки, оно позволяет правильно разрисовывать код GDScript, подсвечивает ошибки и прочее в VSCode. Но если вам не нужно вручную ковыряться – можно и не настраивать.

Настройки

Заходим в File – Preferencesd – Settings, вводим godot в поиске, вводим путь до exe файла godot в Godot tools > Editor Path: Godot 4

Бесплатный ИИ делает игру на Godot - 6

Затем в VSCode нажимаем F1, вводим View: Show Run and Debug, далее create a launch.json file, выбираем GDScript Godot Debug.

Бесплатный ИИ делает игру на Godot - 7

Создается файл puzzle.vscodelaunch.json:

{
    "version": "0.2.0",
    "configurations": [
        {
            "name": "GDScript: Launch Project",
            "type": "godot",
            "request": "launch",
            "project": "${workspaceFolder}",
            "debug_collisions": false,
            "debug_paths": false,
            "debug_navigation": false,
            "additional_options": ""      
        }
    ]
}
Бесплатный ИИ делает игру на Godot - 8

Теперь можно нажать в правом нижнем углу Open workspace with Godot Editor, откроется Godot, и надпись с Disconnedted сменится на Connected

Бесплатный ИИ делает игру на Godot - 9

Qwen CLI

Для работы с бесплатной моделью нам необходимо зарегистрироваться https://chat.qwen.ai/auth?mode=register. Далее запускаем cmd, а в нем команда qwen. Выбираем 1 вариант, вас перекинет в окно где нужно войти под своей учеткой и вуаля. При этом в папке пользователя появляется файл C:Usersuser.qwenoauth_creds.json через который будет производится аутентификация при последующих запусках.

Скрытый текст
Бесплатный ИИ делает игру на Godot - 10

Gemini CLI

Необходимо зайти https://console.cloud.google.com, скопировать оттуда Project ID, и добавить его в системные или пользовательские переменные среды.

Скрытый текст
Бесплатный ИИ делает игру на Godot - 11

Далее запускаем в командной строке gemini, нас перекидывает на страницу авторизации, подтверждаем.

Скрытый текст
Бесплатный ИИ делает игру на Godot - 12

При этом в папке пользователя появляется файл C:Usersuser.geminioauth_creds.json.

Kilo Code

Ну а теперь можно подключить наши модели к агенту с которым и будем далее работать.

Скрытый текст

Указываем русский язык.

Бесплатный ИИ делает игру на Godot - 13

Добавляем профиль для Qwen CLI

Бесплатный ИИ делает игру на Godot - 14

Указываем провайдер Qwen Code и файл аутентификации:

Бесплатный ИИ делает игру на Godot - 15

Для Gemini CLI так же создаем новое подключение и указываем настройки:

Бесплатный ИИ делает игру на Godot - 16

Так же для gemini я бы установил “Лимит скорости” около 7 секунд.

Бесплатный ИИ делает игру на Godot - 17

Зачем это нужно? Gemini при слишком частых обращениях может выдавать ошибку timeout (слишком частые запросы). Лимит скорости между запросами в 7 секунд – устраняет эту проблему.

Затем добавляем MCP серверы для того что бы ИИ агент мог обращаться к свежей документации по GDScript.

Бесплатный ИИ делает игру на Godot - 18

Нажимаем на “Редактировать глобальный MCP” и вставляем в файл такое содержимое:

{
   "mcpServers":{
      "context7":{
         "type":"streamable-http",
         "url":"https://mcp.context7.com/mcp",
         "headers":{
            "CONTEXT7_API_KEY":"xxx"
         },
         "alwaysAllow":[
            "query-docs",
            "resolve-library-id"
         ],
         "disabled":false,
         "timeout":15
      },
      "godot-docs":{
         "type":"streamable-http",
         "url":"https://godot-docs-mcp.j2d.workers.dev/mcp",
         "alwaysAllow":[
            "search_docs",
            "get_docs_page_for_term",
            ""
         ],
         "timeout":15,
         "disabled":false
      }
   }
}
Бесплатный ИИ делает игру на Godot - 19

Для context7 нужно генерировать свой токен (CONTEXT7_API_KEY) у них на сайте. Хотя можно и без него.

Затем переключаемся на главное окно Kilo Code, нажимаем на значок индексации:

Бесплатный ИИ делает игру на Godot - 20

Указываем настройки подключения к Qdrant и к embedding модели обитающей на нашем LM Studio, жмем “Начать” – статус станет зелёным.

Бесплатный ИИ делает игру на Godot - 21

Создаем игру

Итак, язык GDScript не такой уж и распространенный в мире, что бы кодовая база попала в gemini и qwen когда их обучали. Поэтому нам желательно установить некие правила, по которым эти модели будут создавать игру. Для этого у Kilo Code есть настройка – мы можем дать моделям правила в markdown разметке с которыми они должны всегда сверятся.

Скрытый текст
Бесплатный ИИ делает игру на Godot - 22

Добавим файл rules.md:

Бесплатный ИИ делает игру на Godot - 23

Как написать такое содержимое – вопрос. Я попросил GPT 5 на https://arena.ai/ сформировать этот файл. Включил туда общие правила по языку GDScript и по сценам, плюс место откуда модель может запустить игру напрямую (если сказать что-то типа “запусти проект и проанализируй логи”). Правильно это или нет – я не знаю, но вроде получилось.

rules.md
You are an expert game developer specializing in **Godot Engine 4.4** (2D) on **Windows**, with strong knowledge of **typed GDScript**, scalable game architecture, debugging, and performance optimization.

## 0) Version Lock + Documentation Sources (Godot 4.4)
- Target **Godot 4.4** APIs and behavior. Do **not** use Godot 3.x patterns.
- Primary source of truth: official Godot **4.4** manual + class reference: `https://docs.godotengine.org/en/4.4/`
- Always verify:
  - Node/class names,
  - property names,
  - signal names,
  - method signatures,
  - and lifecycle callbacks
  against the **4.4** docs.
- If you must use **stable/latest** docs as a fallback, explicitly say so and re-check compatibility with 4.4.

---

## 1) Core Mission
- Give **correct, production-ready** guidance for Godot 4.4 **2D** development.
- Prefer **Godot built-ins** (Nodes/Scenes, Signals, InputMap, Resources, Animation, Physics, UI) over custom frameworks.
- For each solution provide:
  - A short **plan**
  - **Implementation steps** (Editor + code)
  - **Complete runnable code** (or a clear patch when asked)
  - **Pitfalls** and **performance notes**

---

## 2) Writing Style (LLM-friendly)
- Be **clear, technical, and concrete**.
- Use short paragraphs, lists, and fenced code blocks.
- Avoid vague advice; show **exact node names**, **file paths**, and **Godot 4.4 APIs**.
- If multiple approaches exist: explain trade-offs and recommend one.

---

## 3) Godot 4 Architecture Rules (2D)
- Use **Scenes** as reusable prefabs (`PackedScene`) for Player, Enemy, UI widgets, projectiles, pickups.
- Prefer **composition over inheritance** (node/component-style).
- Keep scene trees shallow and readable; name nodes clearly.
- Avoid fragile parent-chains like `get_parent().get_parent()`; prefer signals, groups, or explicit references.

---

## 4) Node Selection (2D defaults)
- `CharacterBody2D` — character movement (player/enemies).
- `RigidBody2D` — physics-driven objects.
- `Area2D` — triggers, hurtboxes/hitboxes, pickups.
- `Control` — UI (don’t build UI on `Node2D`).

---

## 5) GDScript 2.0 (Godot 4.4) — MUST-KNOW LANGUAGE RULES

### 5.1 Indentation is semantic (Python-like)
- Indentation defines blocks. Wrong indentation = wrong program.
- Use **tabs** for indentation (Godot style guide).
- Prefer readable multiline formatting; avoid overly dense one-liners.

### 5.2 Naming conventions (Godot style)
- `PascalCase` — classes/types/nodes.
- `snake_case` — variables, functions, signals.
- `ALL_CAPS` — constants.
- Prefer trailing commas in multiline arrays/dicts/enums for cleaner diffs.

### 5.3 Script lifecycle (Node callbacks — use correctly)
- `_enter_tree()` — node enters the SceneTree (can happen multiple times).
- `_ready()` — node + its children are in the SceneTree; children’s `_ready()` run **before** the parent; usually called only once per node lifetime.
- `_process(delta)` — every rendered frame (variable timestep).
- `_physics_process(delta)` — fixed timestep (physics loop); use for movement/collisions.
- `_exit_tree()` — node leaves the SceneTree.

### 5.4 Input callbacks (priority matters)
- Prefer `_unhandled_input(event)` for gameplay input so UI can consume events first.
- Use `_shortcut_input(event)` for shortcuts; it runs before `_unhandled_key_input()` and `_unhandled_input()`.
- Use polling (`Input.is_action_pressed`, `Input.get_vector`) for continuous movement in `_physics_process()`.

### 5.5 Initialization order (common bug source)
Understand this order for Node-derived scripts:
1) member vars default init,
2) member var assignments top-to-bottom,
3) `_init()` runs (if defined),
4) exported values are assigned (when instancing scenes/resources),
5) `@onready` vars initialize,
6) `_ready()` runs.

### 5.6 `@onready` (defer node lookups safely)
- `@onready` defers member initialization until `_ready()`.
- Do NOT combine `@onready` with `@export` on the same variable (it causes confusing overrides and is treated as an error by default).

Good:
- `@export var speed: float = 300.0`
- `@onready var sprite: Sprite2D = $Sprite2D`

### 5.7 Typed GDScript (required)
- Use typed GDScript by default:
  - `var hp: int = 10`
  - `func take_damage(amount: int) -> void:`
- Always specify return types, including `-> void`.
- Prefer typed arrays/dicts where practical:
  - `var points: Array[Vector2] = []`
  - `var costs: Dictionary[String, int] = {"apple": 5}`
- Use `:=` for type inference only when it’s truly obvious and improves readability.

### 5.8 Properties (setters/getters) — Godot 4 behavior
- Use property syntax:

  var _ms: int = 0
  var seconds: int:
      get:
          return _ms / 1000
      set(value):
          _ms = value * 1000

- In Godot 4, `set`/`get` are called consistently even from inside the same class (with exceptions described in docs).
- Avoid accidental infinite recursion when calling helper methods inside setters/getters.

### 5.9 Signals (decoupling rule)
- Prefer signals for communication between systems (UI ↔ gameplay).
- Signals are first-class values in Godot 4 (like `Callable`).
- Prefer the recommended connection style using `Signal.connect()`:

  button.button_down.connect(_on_button_down)
  player.hit.connect(_on_player_hit.bind("sword", 100))

- Declare custom signals with `signal`, emit with `.emit(...)`.

### 5.10 `await` (coroutines by awaiting signals)
- `await` is used to wait for signals (or other awaitables).
- Canonical delay:

  await get_tree().create_timer(1.0).timeout

- `SceneTree.create_timer()` returns a `SceneTreeTimer` that emits `timeout` and is auto-freed.
- Use the `create_timer(..., process_in_physics=..., ignore_time_scale=...)` flags when you need precise timing behavior.

### 5.11 Tool scripts
- Use `@tool` at the top to run script code in the editor.
- Be careful with `queue_free()`/`free()` in tool scripts (can crash the editor).

### 5.12 Memory management (must be correct)
- `RefCounted`-based objects (including `Resource`) free automatically when unreferenced.
- `Node` is not ref-counted: free with `queue_free()` (preferred) or `free()`.

---

## 6) Exported Properties & Data
- Use `@export` for tunables in Inspector.
- Use export grouping when helpful:
  - `@export_group("Movement")`
  - `@export_subgroup("Air")`
- Prefer **Resources** for data assets, not hard-coded dictionaries.
- Use Autoload singletons sparingly and keep them thin:
  - `GameState`, `AudioManager`, `SceneLoader`, `SaveSystem`

---

## 7) Input (Godot Way)
- Use **InputMap actions** (Project Settings → Input Map).
- Prefer `StringName` literals for action names:
  - `Input.is_action_pressed(&"move_left")`
  - `Input.is_action_just_pressed(&"jump")`
- Always list required InputMap actions in setup instructions.

---

## 8) UI Rules
- Use `Control` + Containers (`VBoxContainer`, `HBoxContainer`, `MarginContainer`) for layout.
- Avoid hard-coded pixel positioning when containers can solve it.
- Prefer Themes for consistent UI styling.

---

## 9) Animation Rules
- Use `AnimationPlayer` for timelines and simple animation control.
- Use `AnimationTree` (state machine) for complex character animation logic.
- Keep animation state changes explicit and debuggable.

---

## 10) Audio Rules
- Use `AudioStreamPlayer`, `AudioStreamPlayer2D`
- Use buses for volume groups (SFX/Music/UI)
- Don’t recreate streams every time; reuse players or pool when needed.

---

## 11) Error Handling & Debugging (Windows)
- Logging: `print()`, `push_warning()`, `push_error()`, `assert()`
- Use Godot tools: Debugger, Remote Inspector, Profiler, Monitors
- When errors are likely, include:
  - symptom
  - reproduction steps
  - fix
  - how to verify

---

## 12) Performance Rules (Godot 4.x)
- Avoid per-frame allocations in hot paths.
- Pool frequently spawned nodes (bullets/VFX).
- Use collision layers/masks to reduce physics checks.
- Use `queue_free()` responsibly; avoid mass churn every frame.
- Profile first (Profiler + Monitors), then optimize.

---

## 13) Code Organization
- Keep code well-organized with meaningful names.
- Use doc comments `##` for class/module documentation and Inspector tooltips.
- Keep functions small; comment only tricky logic.

---

## 14) Output Format (Hard Requirements)
When you provide code, always include:
- **File path**, e.g. `res://player/player.gd`
- **Node tree expectations**
- **Inspector settings** (`@export` values to set)
- **Signal connections** (who emits, who listens)
- **InputMap actions** needed
- A short **"how to test"** checklist

---

## 15) Programming / Environment Specific (Windows)
- Godot executable path (current): `C:UsersuserDownloadsgodotGodot_v4.5.1-stable_win64_console.exe`
  - This ruleset targets **Godot 4.4** docs/APIs. Avoid using features introduced after 4.4 unless you verify compatibility.
- Create project with Godot.
- Do not close Godot during debugging.
- Environment is Windows 11; cmd tools available for file/folder operations.
- For basic puzzle mechanics (image slicing + snapping), you can use the project structure at:
  `C:UsersuserDesktopProjectspuzzle`
Бесплатный ИИ делает игру на Godot - 24

Режимы ИИ агента

У Kilo Code есть несколько режимов работы.

Скрытый текст
Бесплатный ИИ делает игру на Godot - 25
  1. Архитектор – создает план по которому будет производить написание кода. Сам может переключиться в режим “Код”.

  2. Код – здесь у модели есть права на написание кода, чем собственно она и занимается.

  3. Вопросы – тут можно задавать вопросы без изменения кода.

  4. Отладка – в этом режиме можно почти бесконечно исправлять то, что отказывается работать.

  5. Оркестратор – менеджер проекта, разбивает сложный запрос на подзадачи и распределяет их между предыдущими режимами.

Для любого режима можно выбрать модель. Обычно для Архитектора я выбираю Gemini, а для кода – Qwen.

Каждый режим можно отредактировать: поправить промпт, указать модель. По кнопке “Предпросмотр системного промпта” можно увидеть как Kilo Code внедряет в результативный промпт текст из rules.md.

Полезные вещи

Есть такая очень полезная вещь как “улучшение промпта” с учетом текущего проекта. Например вы пишете “добавь сцену с меню: уровень сложности, выход”. Эта кнопка обогатит ваш запрос в более конкретный промпт с учетом текущей реализации.

Скрытый текст
Бесплатный ИИ делает игру на Godot - 26

Однако иногда надо вчитываться в то, как вам “улучшили” ваш запрос. Бывает что он может не правильно вас понять и вписать ненужную вам логику.

Очень удобная вещь в Kilo Code – это возможность восстановить произведенные изменения в проекте. Эта штука будет спасать вас не раз.

Скрытый текст
Бесплатный ИИ делает игру на Godot - 27

В начале было Слово

Как вы уже догадались, решил я “разработать” мозгами ИИ игру “Пазл”. Вот прямо так ему и написал: “создай игру пазл. есть главное меню, и игровое поле с картинкой”.

…спустя несколько минут часов это уже было похоже на:

Стартовый промпт

Создайте с нуля новую 2D-игру на Godot 4 под названием “Собрание Паззлов”, предназначенную для Windows 11. Игра должна динамически загружать изображения из внешних источников, с первоначальной реализацией, использующей публичный API музея Метрополитен: https://collectionapi.metmuseum.org/public/collection/v1, выбирая случайный объект, имеющий доступное поле primaryImage. Стартовое меню игры должно предоставлять выбор источника изображений (по умолчанию Метрополитен) и три уровня сложности: Легкий (например, 3×3 кусочка), Средний (например, 5×5 кусочков) и Сложный (например, 7×7 кусочков), где количество кусочков увеличивается с возрастанием сложности. После нажатия кнопки “Старт” открывается основное игровое поле: в левом верхнем углу отображается полноразмерное превью выбранного изображения с его описанием (например, название, автор, дата), полученным из API; в центральной части экрана находится область, где пользователь собирает разбросанные кусочки паззла, которые должны иметь функцию перетаскивания и автоматического притягивания (снаппинга) к соседним правильным позициям; в правом верхнем углу отображается счетчик “Собрано: X” (количество корректно соединенных кусочков), и кнопка “Обновить”, которая загружает новое изображение и генерирует новый паззл. Во время загрузки нового изображения должна отображаться анимированная индикация загрузки, а кнопка “Обновить” должна быть временно недоступна. Предусмотрите кнопку “Выйти” или возможность выхода по нажатию клавиши ESC; если паззл не завершен, перед выходом должно появиться диалоговое окно подтверждения “Действительно выйти?”. Для реализации базовых механик паззла, таких как нарезка изображений и логика снаппинга, можно ориентироваться на структуру проекта по пути C:UsersuserDocumentsJigsaw-Puzzle-2D-masterJigsaw-Puzzle-2D-master.

Да пришлось найти где то пример пазла, который брал картинки из бесплатного публичного API музея “Метрополитен” из Нью-Йорка.

В этом примере не хватало главного – не было реализации фигурных вырезов каждого кусочка пазла. На реализацию которого у меня ушло примерно …четыре дня. Тут уже пришлось напрячь извилины и придумать моему помощнику идею: на основе шаблона с прозрачными линиями – вырезать эти самые кусочки. Шаблон генерировал с помощью chat-gpt5 и других моделей на lmarena.ai. С генерацией тоже было много проблем. Пришлось самому править шаблоны вручную.

Шаблон
Бесплатный ИИ делает игру на Godot - 28

Какая же эта игра без фоновой музыки? ИИ справился с добавлением фоновой музыки достаточно быстро.

Фальшстарт

И тут я решил запилить этот шедевр на яндекс игры. Зашел в консоль, на добавлял скриншотов с описанием игры. Ну и собственно загрузил игру экспортированную из godot в html5.

Игра запустилась, но ничего не загружается. Оказывается для доступа игры к серверу музея, необходимо добавлять url в консоль и ждать когда его одобрят. Проходит день, доступ к сайту дают. Игра не может загрузить картинку для создания пазла. Смотрю в консоль – ошибки CORS.

Сделал некий CORS ретранслятор на https://dash.cloudflare.com/, так же добавил его.

Код ретранслятора, если кому интересно
export default {
  async fetch(request, env) {
    // Обработка OPTIONS запросов (CORS preflight)
    if (request.method === 'OPTIONS') {
      return new Response(null, {
        headers: {
          'Access-Control-Allow-Origin': '*',
          'Access-Control-Allow-Methods': 'GET, OPTIONS',
          'Access-Control-Allow-Headers': '*',
          'Access-Control-Max-Age': '86400',
          'Accept-Ranges': 'bytes'
        }
      });
    }

    try {
      const url = new URL(request.url);
      
      // ПРАВИЛЬНО извлекаем параметр url с помощью URLSearchParams
      const rawUrl = request.url.split('url=')[1]; 
      var targetUrlParam = decodeURIComponent(rawUrl);      

      /* test
      return new Response(targetUrlParam, { 
        status: 200,
        headers: { 'Content-Type': 'text/plain' }
      });
      */
      
      // АЛЬТЕРНАТИВНЫЙ СПОСОБ (если URLSearchParams не сработал):
      if (!targetUrlParam) {
        // Ручной парсинг для случаев, когда параметры сложные
        const queryString = url.search.substring(1); // Убираем '?'
        const params = queryString.split('&');
        let extractedUrl = null;
        
        for (const param of params) {
          if (param.startsWith('url=')) {
            extractedUrl = decodeURIComponent(param.substring(4));
            break;
          }
        }
        
        if (!extractedUrl) {
          return new Response('No URL parameter provided', { 
            status: 400,
            headers: { 'Content-Type': 'text/plain' }
          });
        }
        
        // Если нашли URL ручным способом, используем его
        targetUrlParam = extractedUrl;
      }

      if (!targetUrlParam) {
        return new Response('No URL parameter provided', { 
          status: 400,
          headers: { 'Content-Type': 'text/plain' }
        });
      }

      // Исправляем URL с пробелами и неправильным форматированием
      let cleanedUrl = targetUrlParam.trim()
        .replace(/s*:s*/s*//g, '://')  // Исправляем "https  ://", "http  :  //", и т.д.
        .replace(/s+/g, '%20');            // Заменяем пробелы на %20

      // Дополнительная очистка от лишних параметров proxy
      // Убираем все после первого &, если это не часть целевого URL
      // Но сначала проверяем, есть ли в URL уже параметры (?)
      let finalUrl;
      
      try {
        // Пытаемся создать URL объект для валидации
        finalUrl = new URL(cleanedUrl);
      } catch (e) {
        // Если не удалось, пробуем дополнительные методы очистки
        if (cleanedUrl.includes('?') && cleanedUrl.includes('&')) {
          // Для URL с параметрами оставляем всё как есть
          finalUrl = cleanedUrl;
        } else {
          // Пытаемся найти начало реального URL
          const possibleProtocols = ['http://', 'https://'];
          let startIndex = -1;
          
          for (const protocol of possibleProtocols) {
            const index = cleanedUrl.toLowerCase().indexOf(protocol);
            if (index !== -1) {
              startIndex = index;
              break;
            }
          }
          
          if (startIndex !== -1) {
            finalUrl = cleanedUrl.substring(startIndex);
          } else {
            finalUrl = cleanedUrl;
          }
        }
        
        try {
          finalUrl = new URL(finalUrl);
        } catch (e2) {
          return new Response('Invalid URL format after cleaning: ' + e2.message, { 
            status: 400,
            headers: { 'Content-Type': 'text/plain' }
          });
        }
      }

      // Создаем заголовки для запроса
      const headers = new Headers();
      
      // Передаем Range заголовок если есть
      const rangeHeader = request.headers.get('Range');
      if (rangeHeader) {
        headers.set('Range', rangeHeader);
      }
      
      // Копируем важные заголовки
      const forwardedHeaders = ['User-Agent', 'Accept', 'Accept-Language', 'Referer'];
      forwardedHeaders.forEach(header => {
        const value = request.headers.get(header);
        if (value) headers.set(header, value);
      });

      // Выполняем запрос к целевому URL
      const upstreamResponse = await fetch(finalUrl.toString(), {
        headers: headers,
        redirect: 'follow'
      });

      // Создаем заголовки ответа
      const responseHeaders = new Headers(upstreamResponse.headers);
      
      // Сохраняем критические заголовки
      const preserveHeaders = ['Content-Range', 'Accept-Ranges', 'Content-Length', 'Content-Type', 'Cache-Control'];
      preserveHeaders.forEach(header => {
        const value = upstreamResponse.headers.get(header);
        if (value) responseHeaders.set(header, value);
      });

      // Добавляем CORS заголовки
      responseHeaders.set('Access-Control-Allow-Origin', '*');
      responseHeaders.set('Access-Control-Expose-Headers', '*');
      responseHeaders.set('Accept-Ranges', 'bytes');
      responseHeaders.set('Cross-Origin-Opener-Policy', 'same-origin');
      responseHeaders.set('Cross-Origin-Embedder-Policy', 'require-corp');
   

      // Обработка 206 Partial Content
      if (upstreamResponse.status === 206) {
        return new Response(upstreamResponse.body, {
          status: 206,
          statusText: 'Partial Content',
          headers: responseHeaders
        });
      }

      // Для бинарных данных используем потоковую передачу
      const contentType = (responseHeaders.get('content-type') || '').toLowerCase();
      /*
      const isBinary = contentType.startsWith('image/') || 
                       contentType.startsWith('video/') || 
                       contentType.startsWith('audio/') || 
                       contentType.includes('application/octet-stream');
      */
      return new Response(upstreamResponse.body, {
        status: upstreamResponse.status,
        headers: responseHeaders
      });

    } catch (error) {
      console.error('Proxy error:', error);
      
      if (error.name === 'TypeError' && error.message.includes('fetch')) {
        return new Response('Failed to fetch target URL. Check if the URL is valid and accessible.', {
          status: 502,
          headers: { 
            'Content-Type': 'text/plain',
            'Access-Control-Allow-Origin': '*'
          }
        });
      }
      
      return new Response('Proxy Error: ' + error.message, {
        status: 500,
        headers: { 
          'Content-Type': 'text/plain',
          'Access-Control-Allow-Origin': '*'
        }
      });
    }
  }
};
Бесплатный ИИ делает игру на Godot - 29

Проблему это не решило: были страшные тормоза при загрузке из игры. Хотя браузер прекрасно загружал картинки. ИИ гугла сказал что это может быть проблема с тем как godot скачивает данные: не умеет он качать сжатые куски как это делает браузер.

Появилась еще одна проблема: фоновая музыка прерывалась при работе с сетью.

Локализация

Ладно, если Магомет не идет к горе… то сделаем проще. Картинки будем хранить на яндекс диске.

Идем на https://oauth.yandex.ru/ добавляем новое “приложение” MuzeumPuzzle, запрашиваемые права – Яндекс.Диск REST API • Доступ к папке приложения на Диске.

Получаем токен: заходим через https://oauth.yandex.ru/authorize?response_type=token&client_id=xxx и сохраняем токен, этот токен нам нужен будет для работы с яндекс диском. Токен работает ровно 1 год с момента получения.

У нас есть целый полигон от яндекса для тестирования api. Можно создать например папку, и она появится по пути: https://disk.yandex.ru/client/disk/Приложения/MuzeumPuzzle.

Теперь легким движением руки закидываем в папку игры MuzeumPuzzle фото и их описание.

Сначала думал поискать какой нибудь клиент яндекс диска на js, но в итоге написал свой плагин для godot:

yandex_disk_service.gd
extends Node

const BASE_URL = "https://cloud-api.yandex.net"

# готовый токен на 1 год с доступом к папке приложения
var token: String = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"

var headers = ["User-Agent: Godot-Puzzle-Game/1.0", "Accept: */*"]

# регистрация связана с появлением окна
# при этом должно быть не больше 30 токенов
# поэтому обойдемся готовым токеном на 1 год
#func auth(client_id: String) -> void:
#	var response = await _make_request("https://oauth.yandex.ru/authorize?response_type=token&client_id=" + client_id)
#	push_error("YandexDisk: Ошибка получения ссылки (Code %d)" % response.code)

func check_auth() -> bool:
	var response = await _yandex_make_request(BASE_URL + "/v1/disk/resources?path=app:/")
	if response.code == 200:
		return true
	else:
		return false

## Получение списка файлов и папок
func get_files(path: String = "app:/") -> Array:
	var url = BASE_URL + "/v1/disk/resources?path=" + path.uri_encode()
	var response = await _yandex_make_request(url)
	
	if response.code == 200:
		var data = response.data
		if data is Dictionary and data.has("_embedded"):
			return data["_embedded"].get("items", [])
	
	push_error("YandexDisk: Ошибка получения списка файлов (Code %d)" % response.code)
	return []

## Получение временной ссылки на скачивание файла
func get_download_link(path: String = "app:/") -> String:
	var url = BASE_URL + "/v1/disk/resources/download?path=" + path.uri_encode()
	var response = await _yandex_make_request(url)
	
	if response.code == 200:
		return response.data.get("href", "")
	
	push_error("YandexDisk: Ошибка получения ссылки (Code %d)" % response.code)
	return ""
	
## Получение изображения
func get_image(path: String = "app:/") -> Image:
	# получаем ссылку
	var link = await get_download_link(path)
	
	# скачиваем по ссылке
	var image_result = await _make_request(link, headers)
	
	# преобразуем в картинку
	if image_result.result == HTTPRequest.RESULT_SUCCESS:
		var image = Image.new()
		var err = image.load_jpg_from_buffer(image_result.body)
		if err != OK:
			err = image.load_png_from_buffer(image_result.body)
		
		if err == OK:
			return image
			
	print("WARNING: Не удалось загрузить файл: %s" % path)
	return null
	
## Получение изображения
func get_text(path: String = "app:/") -> String:
	# получаем ссылку
	var link = await get_download_link(path)
	
	# скачиваем по ссылке
	var result = await _make_request(link, headers)
	
	# преобразуем в картинку
	if result.result == HTTPRequest.RESULT_SUCCESS:
		return result.body.get_string_from_utf8()
			
	print("WARNING: Не удалось загрузить или декодировать изображение: %s" % path)
	return ""

## Внутренний метод для выполнения HTTP-запросов
func _yandex_make_request(url: String) -> Dictionary:
	var http = HTTPRequest.new()
	http.max_redirects = 5
	add_child(http)
	
	var headers = [
		"Authorization: OAuth " + token,
		"Accept: application/json"
	]
	
	var err = http.request(url, headers, HTTPClient.METHOD_GET)
	if err != OK:
		http.queue_free()
		return {"code": 0, "data": {}}

	var result = await http.request_completed
	# result[0] - result (int), result[1] - response_code (int), 
	# result[2] - headers (PackedStringArray), result[3] - body (PackedByteArray)
	
	var response_code = result[1]
	var body = result[3].get_string_from_utf8()
	
	var json = JSON.new()
	var parse_err = json.parse(body)
	var data = json.get_data() if parse_err == OK else {}
	
	http.queue_free()
	return {"code": response_code, "data": data}

## обычный запрос
func _make_request(url: String, headers: PackedStringArray) -> Dictionary:
	# Вместо использования одного глобального http_request, 
	# создаем временный для этого конкретного вызова. 
	# Это исключит ошибку 5 (ERR_ALREADY_IN_USE).
	var http = HTTPRequest.new()
	add_child(http)
	
	# Настраиваем TLS ДО вызова request
	#var tls_settings = TLSOptions.client_unsafe()
	#http.set_tls_options(tls_settings)

	http.timeout = 15  # Установим таймаут 15 секунд для предотвращения зависания
	
	http.max_redirects = 5
	
	# Увеличиваем лимиты для бинарных данных
	http.body_size_limit = 26214400 # 25 МБ (чтобы точно влезли любые фото)
	
	var error = http.request(url, headers, HTTPClient.METHOD_GET, "")
	if error != OK:
		print("ERROR: %s" % error)
		http.queue_free()
		return {"result": -1}

	var result = await http.request_completed
	
	# Удаляем временный узел
	http.queue_free()
	
	return {
		"result": result[0],
		"code": result[1],
		"headers": result[2],
		"body": result[3]
	}
Бесплатный ИИ делает игру на Godot - 30

Звук

Для звука так же пытался найти а затем сделать отдельный worker, но так ничего не получилось. Сейчас точно не помню, но проблема была решена изменениями в двух местах: в экспорте в html5 нужно было убрать галку “Поддержка потоков”. А в коде нужно было указать такое:

Фрагмент MusicService.gd
audio_player = AudioStreamPlayer.new()
if OS.has_feature("web"):
	audio_player.playback_type = AudioServer.PLAYBACK_TYPE_SAMPLE
else:
	audio_player.playback_type = AudioServer.PLAYBACK_TYPE_STREAM
Бесплатный ИИ делает игру на Godot - 31

Фоновая музыка для игры – с бесплатной лицензией для использования в коммерческих целях.

Допиливание

Прежде чем выкладывать игру, надо в неё поиграть.

Ни у кого не получится очень точно, пиксель в пиксель разместить кусок пазла в нужное место на шаблоне с первого раза. Поэтому добавил авто-притягивание куска пазла если его положили близко к своему месту.

Новая проблема: все картинки разные, а шаблон – квадратный, поэтому при формировании пазла, картинка растягивается, что выглядит не очень. Нашел единственный вариант – дорисовывать по краям белый фон.

Белый фон по краям картинки
Бесплатный ИИ делает игру на Godot - 32

Было не удобно когда сгенерированные кусочки раскидывались по всему игровому полю. Поэтому решил сделать некий “конвейер” с кусочками, откуда можно их доставать, не захламляя игровое поле.

При запуске игры на компьютере все идеально, но при запуске на телефоне – всё мелкое и шаблон очень мелкий. После долгих препираний с ИИ, удалось таки сделать динамическое масштабирование при изменении размеров игрового поля.

Разное масштабирование
Бесплатный ИИ делает игру на Godot - 33

Пиши пропало

И тут случилось то, чего я не ожидал. Kilo code перестал работать с gemini. Возврат агента на старую версию не намного отсрочил проблему. Через какое то время gemini перестала работать даже со старой версией агента.

Попробовал последние правки игры делать сразу в Qwen CLI, без VSCode. Общие впечатления: отвечает намного шустрее. Но работа через Kilo code намного нагляднее и проще, особенно с возвратом кода.

Итого

Это увлекательный был аттракцион…

Вайб прости господи кодинг по началу вызывает ощущение безграничных возможностей. Но эти возможности кардинально ограничены тем, кто решил нанять ИИ в качестве джуна. После многочисленных “да вы правы, сейчас исправим”, понимаешь, что без собственных знаний в предмете, такое “программирование” начинает очень сильно утомлять.

История…
История в Kilo Code

История в Kilo Code
История в файловой системе

История в файловой системе

Какова ценность того, что сделал ИИ для вас, если вы сами в этом ничего не понимаете? Как оценить то, что написал для вас ИИ? Что с этим делать? Как это дальше использовать?

Вопрос – открытый.

Раньше все ругали «индусский код». Теперь на смену ему пришел куда более опасный ИИ-слоп.

Бесплатный ИИ делает игру на Godot - 36

А на сегодня всё…

Автор: virex

Источник