Developer Manual

Руководство разработчика

Step-by-step guide to creating your first PLUTO-8 game.

Пошаговое руководство по созданию вашей первой игры на PLUTO-8.

1. Getting Started

1. Начало работы

PLUTO-8 is a fantasy console that runs entirely inside Roblox. You do not need to install anything — just join the Roblox game and you are ready to create.

PLUTO-8 — это фэнтезийная консоль, которая работает полностью внутри Roblox. Ничего устанавливать не нужно — просто войдите в игру Roblox и начинайте создавать.

What is a Fantasy Console?

Что такое фэнтезийная консоль?

A fantasy console is a simulated retro gaming machine with deliberately limited specifications. Those constraints make it easy to learn game development and to finish small games quickly. PLUTO-8 is inspired by PICO-8 but lives natively inside Roblox, so your games are instantly playable by millions of people.

Фэнтезийная консоль — это симулятор ретро-игровой машины с намеренно ограниченными характеристиками. Эти ограничения упрощают изучение разработки игр и позволяют быстро заканчивать небольшие проекты. PLUTO-8 вдохновлён PICO-8, но живёт внутри Roblox, поэтому ваши игры мгновенно становятся доступны миллионам игроков.

Specifications at a Glance

Краткие характеристики

  • Display: 256×256 pixels, 256-color palette
  • Frame rate: 30 FPS
  • Sprites: 256 sprites, each 8×8 pixels
  • Tilemap: 128×128 tiles
  • Scripting: Lua (sandbox, PICO-8-style API)
  • RAM: 128 KB addressable memory
  • ROM: 512 KB cartridge code (separate from RAM)
  • VRAM: 64 KB video buffer (256×256 × 1 byte)
  • SFX: 8 sound effect slots
  • Дисплей: 256×256 пикселей, 256-цветная палитра
  • Частота кадров: 30 FPS
  • Спрайты: 256 спрайтов, каждый 8×8 пикселей
  • Тайлмап: 128×128 тайлов
  • Скриптинг: Lua (песочница, API в стиле PICO-8)
  • ОЗУ: 128 КБ адресуемой памяти
  • ROM: 512 КБ код картриджа (отдельно от RAM)
  • VRAM: 64 КБ видеобуфер (256×256 × 1 байт)
  • SFX: 8 слотов звуковых эффектов

Opening the Code Editor

Открытие редактора кода

  1. Join the PLUTO-8 Roblox game.
  2. Press Tab (or tap the pencil icon on mobile) to enter Developer Mode.
  3. The code editor opens on the right side of the screen.
  4. Type your Lua code and press Ctrl+Enter (or the Run button) to load it.
  1. Войдите в игру PLUTO-8 на Roblox.
  2. Нажмите Tab (или иконку карандаша на мобильном) для входа в режим разработчика.
  3. Редактор кода откроется справа на экране.
  4. Введите ваш код на Lua и нажмите Ctrl+Enter (или кнопку Run) для запуска.
Note: Developer mode is available to everyone. There is no separate account or permission needed.
Примечание: Режим разработчика доступен всем. Отдельный аккаунт или разрешение не требуются.

2. Hello World

The smallest possible PLUTO-8 game just needs a _draw function:

Самая простая игра на PLUTO-8 требует только функции _draw:

lua
function _draw()
  cls(0)
  print("HELLO, WORLD!", 60, 120, 7)
end

Copy this into the code editor and press Run. You should see white text on a black background.

Скопируйте это в редактор кода и нажмите Run. Вы увидите белый текст на чёрном фоне.

Let's break it down:

  • cls(0) — clear the screen to color 0 (black). Always call this first in _draw or you will see the previous frame.
  • print("HELLO, WORLD!", 60, 120, 7) — draw text at position (60, 120) in color 7 (white). The screen is 256×256, so (60, 120) is near the left-center.

Разберём по частям:

  • cls(0) — очистить экран цветом 0 (чёрный). Всегда вызывайте это первым в _draw, иначе будет виден предыдущий кадр.
  • print("HELLO, WORLD!", 60, 120, 7) — нарисовать текст в позиции (60, 120) цветом 7 (белый). Экран 256×256, поэтому (60, 120) находится слева по центру.
Tip: Color 7 is always white in the default PLUTO-8 palette. Color 0 is black. See the color reference in the API docs.
Совет: Цвет 7 — всегда белый в стандартной палитре PLUTO-8. Цвет 0 — чёрный. Смотрите справочник цветов в документации API.

3. The Game Loop

3. Игровой цикл

PLUTO-8 runs three functions in a fixed order, 30 times per second:

PLUTO-8 запускает три функции в фиксированном порядке, 30 раз в секунду:

lua
function _init()
  -- Runs ONCE when the cartridge starts.
  -- Initialise your variables here.
  score = 0
  player = { x = 128, y = 200 }
end

function _update(dt)
  -- Runs 30 times per second. dt = real delta time (~0.033s).
  -- Move objects, check collisions, update state.
  if btn(0) then player.x -= 2 end
  if btn(1) then player.x += 2 end
end

function _draw()
  -- Runs after _update.
  -- Issue all draw commands here.
  cls(1)
  spr(1, player.x, player.y)
  print("SCORE: "..score, 2, 2, 7)
end

Execution Order

Порядок выполнения

  1. On first run: _init() is called once.
  2. Every frame (30 fps): _update(dt) is called with the real delta time.
  3. Immediately after: _draw() is called.
  4. The resulting frame is sent to all connected clients.
  1. При первом запуске: _init() вызывается один раз.
  2. Каждый кадр (30 fps): вызывается _update(dt) с реальным дельта-временем.
  3. Сразу после: вызывается _draw().
  4. Полученный кадр отправляется всем подключённым клиентам.
Note: All three functions are optional. If _init is missing, the game still works. If _draw is missing, nothing is displayed.
Примечание: Все три функции необязательны. Если _init отсутствует, игра всё равно работает. Если _draw отсутствует, ничего не отображается.

Delta Time (dt)

Дельта-время (dt)

The global variable dt contains the real time between frames in seconds (~0.033 at stable 30 FPS). It is also passed as an argument to _update(dt). Use it for frame-rate independent movement and timers:

Глобальная переменная dt содержит реальное время между кадрами в секундах (~0.033 при стабильных 30 FPS). Также передаётся как аргумент в _update(dt). Используйте для движения и таймеров, независимых от частоты кадров:

lua
local speed = 120  -- pixels per second
local timer = 3    -- 3 second countdown

function _update(dt)
  x += speed * dt
  timer -= dt
  if timer <= 0 then
    -- time's up!
  end
end

Note: Old cartridges without dt continue to work. You can also use a fixed timestep (2 pixels per frame = 60 pixels per second) if you prefer.

Примечание: Старые картриджи без dt продолжают работать. Вы также можете использовать фиксированный шаг (2 пикселя за кадр = 60 пикселей в секунду), если предпочитаете.

4. Drawing

4. Рисование

Colors

Цвета

PLUTO-8 uses a 256-color palette. Colors 0–15 follow the classic PICO-8 palette. Colors 16–255 are extended. The most common colors:

PLUTO-8 использует 256-цветную палитру. Цвета 0–15 соответствуют классической палитре PICO-8. Цвета 16–255 — расширенные. Наиболее часто используемые цвета:

  • 0 — Black
  • 1 — Dark Blue
  • 2 — Dark Purple
  • 3 — Dark Green
  • 5 — Dark Grey
  • 6 — Light Grey
  • 7 — White
  • 8 — Red
  • 9 — Orange
  • 10 — Yellow
  • 11 — Green
  • 12 — Blue
  • 14 — Pink
  • 15 — Peach
  • 0 — Чёрный
  • 1 — Тёмно-синий
  • 2 — Тёмно-фиолетовый
  • 3 — Тёмно-зелёный
  • 5 — Тёмно-серый
  • 6 — Светло-серый
  • 7 — Белый
  • 8 — Красный
  • 9 — Оранжевый
  • 10 — Жёлтый
  • 11 — Зелёный
  • 12 — Синий
  • 14 — Розовый
  • 15 — Персиковый

Shapes

Фигуры

lua
-- Pixel
pset(50, 50, 8)

-- Line
line(0, 0, 100, 100, 7)

-- Rectangle (outline / filled)
rect(10, 10, 60, 40, 6)
rectfill(10, 10, 60, 40, 1)

-- Circle (outline / filled)
circ(128, 128, 30, 11)
circfill(128, 128, 30, 3)

Text

Текст

Use print(text, x, y, color) for simple text. The built-in font is 4×6 pixels. Each character is 4 pixels wide with 1 pixel spacing, so a line of text is about 5 pixels per character.

Используйте print(text, x, y, color) для простого текста. Встроенный шрифт 4×6 пикселей. Каждый символ 4 пикселя шириной с интервалом 1 пиксель, то есть одна строка текста занимает около 5 пикселей на символ.

lua
print("PLAYER 1", 80, 10, 7)
print("SCORE: "..tostr(score), 2, 2, 10)

Camera

Камера

Use camera(x, y) to scroll the view. All draw calls after this are offset so that world position (x, y) appears at screen position (0, 0).

Используйте camera(x, y) для прокрутки вида. Все последующие вызовы рисования смещаются так, что мировая позиция (x, y) отображается в экранной позиции (0, 0).

lua
function _draw()
  cls(1)
  camera(cam_x, cam_y)   -- follow the player
  map(0, 0, 0, 0, 32, 32)
  spr(1, player.x, player.y)
  camera(0, 0)           -- reset for HUD
  print("HP:"..hp, 2, 2, 8)
end

5. Input

5. Управление

PLUTO-8 supports up to 7 buttons. On desktop they map to keyboard keys; on mobile a D-pad and A/B buttons are shown on screen.

PLUTO-8 поддерживает до 7 кнопок. На компьютере они соответствуют клавишам клавиатуры; на мобильных устройствах на экране отображается D-pad и кнопки A/B.

IDActionДействиеKeyboardКлавиатураMobileМобильный
0LeftВлево← or AD-pad leftD-pad влево
1RightВправо→ or DD-pad rightD-pad вправо
2UpВверх↑ or WD-pad upD-pad вверх
3DownВниз↓ or SD-pad downD-pad вниз
4A (confirm)A (подтвердить)ZA buttonКнопка A
5B (cancel)B (отмена)XB buttonКнопка B
6StartEnterStart buttonКнопка Start

btn vs btnp

btn и btnp

btn(id) returns true every frame the button is held. Use it for continuous movement.

btnp(id) returns true only on the first frame the button is pressed. Use it for actions that should fire once per press — like jumping, firing, or selecting a menu item.

btn(id) возвращает true каждый кадр, пока кнопка зажата. Используйте для непрерывного движения.

btnp(id) возвращает true только в первый кадр нажатия. Используйте для действий, которые должны сработать один раз — например, прыжок, выстрел или выбор пункта меню.

lua
function _update()
  -- continuous movement
  if btn(0) then x -= 2 end
  if btn(1) then x += 2 end

  -- single press: jump only once
  if btnp(4) and on_ground then
    vy = -6
  end
end

Mouse and Touch

Мышь и касания

PLUTO-8 also tracks mouse position and clicks. On mobile, touching the game screen directly works as mouse input — separate from the D-pad and buttons.

  • mx() / my() — cursor X/Y in screen coordinates (0–255). Returns -1 if the cursor is outside the screen.
  • mbtn(0)true while left mouse button or finger is held on screen.
  • mbtnp(0)true only on the first frame of a click/tap.

Coordinates are in screen space (same as drawing commands), independent of the camera offset.

PLUTO-8 также отслеживает положение мыши и клики. На мобильных устройствах прямое касание экрана игры работает как ввод мыши — отдельно от D-pad и кнопок.

  • mx() / my() — X/Y курсора в экранных координатах (0–255). Возвращает -1, если курсор за пределами экрана.
  • mbtn(0)true, пока левая кнопка мыши или палец удерживается на экране.
  • mbtnp(0)true только в первый кадр клика/касания.

Координаты в экранном пространстве (как команды рисования), независимо от смещения камеры.

lua
function _draw()
  cls(0)
  if mx() >= 0 then          -- cursor is on screen
    local col = mbtn(0) and 7 or 6
    circfill(mx(), my(), 4, col)
  end
end

6. Sprites

6. Спрайты

The sprite sheet holds 256 sprites, each 8×8 pixels. Sprite 0 is the top-left; sprite 1 is to its right, and so on across 16 columns.

Лист спрайтов содержит 256 спрайтов, каждый 8×8 пикселей. Спрайт 0 — верхний левый; спрайт 1 — правее, и так далее по 16 колонкам.

Creating Sprites

Создание спрайтов

  1. Open the sprite editor tab (palette icon) in Developer Mode.
  2. Click a sprite slot to select it.
  3. Draw with the pencil tool — left-click to place pixels, right-click to erase.
  4. Use the palette to pick colors.
  5. Zoom in with the zoom slider for precision.
  1. Откройте вкладку редактора спрайтов (иконка палитры) в режиме разработчика.
  2. Кликните на слот спрайта, чтобы выбрать его.
  3. Рисуйте карандашом — левая кнопка мыши ставит пиксели, правая — стирает.
  4. Используйте палитру для выбора цветов.
  5. Увеличьте масштаб ползунком для точного рисования.

Game Icon (Sprite 255)

Иконка игры (спрайт 255)

Sprite 255 is reserved as the game icon. It is displayed in the in-console game catalog (32–36 px) and on the website pluto-8.com/games.html (64×64 px).

Спрайт 255 зарезервирован как иконка игры. Он отображается в каталоге игр в приставке (32–36 px) и на сайте pluto-8.com/games.html (64×64 px).

  1. Open the SPRITE tab in the IDE.
  2. Select or draw the sprite you want as your icon (8×8 pixels).
  3. Click the SET ICON button — the sprite is copied to slot 255.
  4. Click SAVE — the icon is saved with the cartridge.
  5. When you publish, the icon automatically appears in the game catalog.
  1. Откройте вкладку SPRITE в IDE.
  2. Выберите или нарисуйте нужный спрайт (8×8 пикселей).
  3. Нажмите кнопку SET ICON — спрайт скопируется в слот 255.
  4. Нажмите SAVE — иконка сохранится вместе с картриджем.
  5. При публикации иконка автоматически отобразится в каталоге игр.
Tip: Do not use sprite 255 for in-game graphics — it is reserved for the game icon.
Совет: Не используйте спрайт 255 для игровой графики — он зарезервирован под иконку игры.

Drawing Sprites in Code

Рисование спрайтов в коде

lua
-- Draw sprite 1 at position (x, y)
spr(1, x, y)

-- Draw a 2x2 block of sprites starting at sprite 4
-- This draws sprites 4, 5, 20, 21 as a 16x16 image
spr(4, x, y, 2, 2)

-- Flip horizontally (for left-facing walk)
spr(1, x, y, 1, 1, facing_left)

Sprite Animation

Анимация спрайтов

lua
function _init()
  frame = 0
  anim_timer = 0
end

function _update()
  anim_timer += 1
  if anim_timer >= 8 then  -- change frame every 8 ticks (~4 fps)
    anim_timer = 0
    frame = (frame + 1) % 4   -- cycle frames 0-3
  end
end

function _draw()
  cls(0)
  spr(1 + frame, player.x, player.y)
end
Tip: Color 0 is transparent by default when drawing sprites. Use palt() to change which colors are transparent.
Совет: Цвет 0 прозрачен по умолчанию при рисовании спрайтов. Используйте palt(), чтобы изменить, какие цвета прозрачны.

7. Tilemap

7. Тайлмап

The tilemap is a 128×64 grid of tile indices. Each tile refers to a sprite. Draw a section of the map with map().

Тайлмап — это сетка 128×64 тайловых индексов. Каждый тайл ссылается на спрайт. Рисуйте секцию карты с помощью map().

Designing Maps

Создание карт

  1. Open the Map Editor tab (grid icon) in Developer Mode.
  2. Select a tile from the sprite sheet at the bottom.
  3. Click and drag on the map canvas to paint tiles.
  4. Use the fill tool (bucket) for large areas.
  1. Откройте вкладку редактора карт (иконка сетки) в режиме разработчика.
  2. Выберите тайл из листа спрайтов внизу.
  3. Кликайте и перетаскивайте по холсту карты для рисования тайлов.
  4. Используйте инструмент заливки (ведро) для больших областей.

Drawing the Map

Рисование карты

lua
function _draw()
  cls(1)
  -- draw tiles 0,0 to 31,31 of the map at screen (0,0)
  map(0, 0,   -- map top-left tile
      0, 0,   -- screen destination
      32, 32) -- tile count (256x256 pixels)
end

Tile-based Collision

Тайловые коллизии

lua
function is_solid(wx, wy)
  local tx = flr(wx / 8)
  local ty = flr(wy / 8)
  local tile = mget(tx, ty)
  -- sprite flag 0 = solid tile
  return (tile ~= 0) and (tile & 1) ~= 0
end

8. Sound

8. Звук

PLUTO-8 supports 8 SFX slots (0–7). You define sounds in the SFX editor inside Developer Mode, then play them in code.

PLUTO-8 поддерживает 8 слотов SFX (0–7). Звуки задаются в редакторе SFX в режиме разработчика, а затем воспроизводятся в коде.

Playing SFX

Воспроизведение SFX

lua
sfx(0)          -- play sfx 0 at full volume
sfx(1, 0.5)     -- play sfx 1 at 50% volume

function _update()
  if btnp(4) then
    sfx(0)     -- play "jump" sound
    vy = -5
  end
end

Built-in SFX Presets

Встроенные пресеты SFX

SlotCharacterWhen to use
0Low hitDeath, damage, lose
1Soft bumpPick up item, touch
2Medium stepInteraction, footstep
3Neutral beepMenu navigation, select
4Rising toneCoin, score point
5High pitchJump, launch, start
6ExplosionDestruction, crash
7VictoryBonus, level complete
СлотХарактерКогда использовать
0Низкий ударСмерть, урон, проигрыш
1Мягкий бампПодбор предмета, касание
2Средний шагВзаимодействие, шаг
3Нейтральный бипНавигация, выбор
4ПодъёмМонета, очко
5ВысокийПрыжок, запуск
6ВзрывРазрушение, столкновение
7ПобедаБонус, финал уровня

Typical Usage

Типичное использование

lua
function _update()
  if btnp(4) and on_ground then
    vy = -6
    sfx(5)     -- jump sound
  end
  if got_coin then
    sfx(4)     -- coin sound
  end
  if hp <= 0 then
    sfx(0)     -- death sound
  end
end

9. Tips & Tricks

9. Советы

Performance

Производительность

  • Keep _draw lean. Minimize draw calls per frame — avoid drawing thousands of individual pixels with pset.
  • Use sprite sheets and spr() instead of per-pixel drawing.
  • Avoid allocating new tables every frame — reuse objects instead.
  • Clearing only dirty rectangles with rectfill instead of cls can help in static scenes.
  • Держите _draw лёгким. Минимизируйте количество вызовов рисования за кадр — не рисуйте тысячи отдельных пикселей через pset.
  • Используйте листы спрайтов и spr() вместо попиксельного рисования.
  • Не создавайте новые таблицы каждый кадр — переиспользуйте объекты.
  • В статичных сценах помогает очистка только изменившихся прямоугольников через rectfill вместо cls.

Common Patterns

Распространённые паттерны

Entity list:

Список сущностей:

lua
bullets = {}

function spawn_bullet(x, y, vx, vy)
  add(bullets, {x=x, y=y, vx=vx, vy=vy, alive=true})
end

function _update()
  for b in all(bullets) do
    b.x += b.vx; b.y += b.vy
    if b.x < 0 or b.x > 256 then del(bullets, b) end
  end
end

State machine:

Конечный автомат:

lua
state = "title"

function _update()
  if state == "title" then
    if btnp(4) then state = "game" end
  elseif state == "game" then
    update_game()
  elseif state == "gameover" then
    if btnp(4) then state = "title" end
  end
end

AABB collision check:

Проверка AABB-коллизии:

lua
function overlaps(a, b)
  return a.x < b.x+b.w and a.x+a.w > b.x
     and a.y < b.y+b.h and a.y+a.h > b.y
end

10. Publishing

10. Публикация

Once your game is complete, you can publish it to the PLUTO-8 community browser.

Когда игра готова, вы можете опубликовать её в браузере сообщества PLUTO-8.

How to Publish

Как опубликовать

  1. Open the code editor and make sure your game runs without errors.
  2. Click the Publish button (upload icon) in the Developer Mode toolbar.
  3. Enter a name and optional description for your game.
  4. Click Confirm.
  5. Your game is now listed in the Community Games browser and playable by everyone.
  1. Откройте редактор кода и убедитесь, что игра запускается без ошибок.
  2. Нажмите кнопку Publish (иконка загрузки) на панели инструментов режима разработчика.
  3. Введите название и необязательное описание игры.
  4. Нажмите Confirm.
  5. Ваша игра теперь отображается в браузере Community Games и доступна всем.

After Publishing

После публикации

  • Players can like your game and leave it in their favourites.
  • You can update your game at any time — the cartridge is replaced for all future plays.
  • Play count and like count are shown on your game card.
  • Игроки могут лайкнуть игру и добавить её в избранное.
  • Вы можете обновить игру в любое время — картридж заменяется для всех будущих запусков.
  • Количество запусков и лайков отображается на карточке игры.
Note: Published games are moderated. Games that violate Roblox Community Guidelines will be removed.
Примечание: Опубликованные игры проходят модерацию. Игры, нарушающие правила сообщества Roblox, будут удалены.
Tip: Give your game a clear title and test it on mobile before publishing — many players use touch controls.
Совет: Дайте игре понятное название и протестируйте её на мобильном перед публикацией — многие игроки используют сенсорное управление.

11. Modules & Libraries

11. Модули и библиотеки

PLUTO-8 uses a single require() function to load both cartridge modules and platform libraries.

PLUTO-8 использует единую функцию require() для подключения как модулей картриджа, так и платформенных библиотек.

Loading Priority

Приоритет загрузки

require("name") searches in this order:

  1. Cartridge modules — files inside your project
  2. Platform libraries — built-in PLUTO-8 libraries

require("name") ищет в следующем порядке:

  1. Модули картриджа — файлы внутри вашего проекта
  2. Платформенные библиотеки — встроенные библиотеки PLUTO-8

Cartridge Modules

Модули картриджа

The code editor supports up to 256 modules per cartridge.

  • Main — the entry point module. Always present, cannot be deleted.
  • Additional modules — helper code loaded via require("name").
  • Switch between modules via the dropdown menu at the top of the editor.
  • Modules can be renamed and deleted (except Main).
  • Each module must return a value (typically a table).

Редактор кода поддерживает до 256 модулей в одном картридже.

  • Main — главный модуль, точка входа. Всегда присутствует, удалить нельзя.
  • Дополнительные модули — вспомогательный код, подключаемый через require("name").
  • Переключение модулей — через dropdown-меню в верхней части редактора.
  • Модули можно переименовывать и удалять (кроме Main).
  • Каждый модуль должен возвращать значение через return (обычно таблицу).

Platform Libraries

Платформенные библиотеки

Built-in libraries available in any cartridge:

LibraryDescription
collisionAABB, circle-circle, point-in-rect, circle-rect checks
tweenEasing functions (linear, ease in/out, cubic, bounce) and lerp
particlesSimple particle system (emit, update, draw)
timerTimer system: delayed and repeating actions
geom2D geometry utilities
sceneScene/state manager
gameobjectEntity management with tagging
spriteAnimated sprites with rotation

Встроенные библиотеки, доступные в любом картридже:

БиблиотекаОписание
collisionAABB, circle-circle, point-in-rect, circle-rect коллизии
tweenEasing-функции (linear, ease in/out, cubic, bounce) и lerp
particlesПростая система частиц (emit, update, draw)
timerТаймеры: отложенные и повторяющиеся действия
geom2D-геометрия
sceneМенеджер сцен
gameobjectСистема сущностей с тегами
spriteАнимированные спрайты с rotation

Example

Пример

lua
-- Module "utils":
local utils = {}
function utils.lerp(a, b, t) return a + (b - a) * t end
function utils.clamp(v, lo, hi) return max(lo, min(hi, v)) end
return utils

-- Module "Main":
local utils = require("utils")        -- cartridge module
local col = require("collision")      -- platform library

function _init()
  x = 128
end

function _update()
  x = utils.lerp(x, mx(), 0.2)
end

function _draw()
  cls(0)
  circfill(utils.clamp(x, 10, 246), 128, 10, 10)
end
Tip: Use platform libraries (collision, tween, particles, timer, geom, scene, gameobject, sprite) instead of writing your own. See section 13 for PlutoFL library details. Good cartridge module candidates: Utils, UI (menus, HUD), Enemies (AI, spawning), Levels (data tables).
Совет: Используйте платформенные библиотеки (collision, tween, particles, timer, geom, scene, gameobject, sprite) вместо написания своих. Подробнее о библиотеках PlutoFL — в разделе 13. Хорошие кандидаты для модулей картриджа: Utils, UI (меню, HUD), Enemies (ИИ, спавн), Levels (таблицы данных).

12. Saving Progress

12. Сохранение прогресса

PLUTO-8 supports saving player scores between sessions. Scores are tied to the player and game name.

PLUTO-8 поддерживает сохранение результатов игроков между сессиями. Результаты привязаны к игроку и названию игры.

Available Functions

Доступные функции

FunctionDescription
save_score(score)Save score (only if higher than previous best)
load_score()Load best score (0 if none)
get_leaderboard(n)Top-N players (max 10), returns {name, score}
load_game_score(name)Best score in another game by name
ФункцияОписание
save_score(score)Сохранить результат (только если лучше предыдущего)
load_score()Загрузить лучший результат (0 если нет)
get_leaderboard(n)Топ-N игроков (макс. 10), возвращает {name, score}
load_game_score(name)Лучший результат в другой игре по имени

Full Example: Game with Leaderboard

Полный пример: игра с лидербордом

lua
function _init()
  score = 0
  best  = load_score()
  state = "title"
  top   = {}
end

function _update()
  if state == "title" then
    if mbtnp(0) then
      sfx(3)
      state = "play"
    end
  elseif state == "play" then
    score += 1
    if score > 100 then
      save_score(score)
      top = get_leaderboard(5)
      state = "over"
    end
  elseif state == "over" then
    if mbtnp(0) then
      score = 0
      state = "title"
    end
  end
end

function _draw()
  cls(1)
  if state == "title" then
    print("TAP TO PLAY", 78, 120, 7)
    print("BEST: "..tostr(best), 90, 134, 10)
  elseif state == "play" then
    print("SCORE: "..tostr(score), 4, 4, 7)
  elseif state == "over" then
    print("GAME OVER", 90, 40, 8)
    print("YOUR SCORE: "..tostr(score), 70, 60, 7)
    print("TOP PLAYERS:", 70, 85, 10)
    for i, e in pairs(top) do
      print(i..". "..e.name.."  "..tostr(e.score), 55, 95+i*12, 6)
    end
    print("TAP TO RETRY", 78, 170, 11)
  end
end
Note: The game name is set when the cartridge is loaded. For built-in cartridges and editor cartridges without a name, save functions return 0 and do not store data.
Примечание: Название игры задаётся при загрузке картриджа. Для встроенных картриджей и картриджей без названия функции сохранения возвращают 0 и не сохраняют данные.

13. PlutoFL Game Libraries

13. Игровые библиотеки PlutoFL

PlutoFL is a set of high-level libraries for rapid game development. They save hundreds of characters of code (cartridge limit is 9000 characters).

PlutoFL — набор высокоуровневых библиотек для быстрой разработки игр. Экономят сотни символов кода (лимит картриджа — 9000 символов).

timer

timer

Schedule delayed and repeating actions without manual frame counting.

Планируйте отложенные и повторяющиеся действия без ручного подсчёта кадров.

FunctionDescription
timer.after(sec, fn)Call fn once after sec seconds
timer.every(sec, fn)Call fn every sec seconds
timer.cancel(id)Cancel a scheduled timer by its ID
timer.update()Advance all timers (call in _update)
timer.clear()Remove all timers
ФункцияОписание
timer.after(sec, fn)Вызвать fn один раз через sec секунд
timer.every(sec, fn)Вызывать fn каждые sec секунд
timer.cancel(id)Отменить таймер по его ID
timer.update()Обновить все таймеры (вызывать в _update)
timer.clear()Удалить все таймеры
lua
local T = require("timer")

function _init()
  -- spawn an enemy every 2 seconds
  T.every(2, function()
    add(enemies, {x=rnd(240), y=0})
  end)
  -- show "GO!" after 1 second
  T.after(1, function() msg = "GO!" end)
end

function _update()
  T.update()
end

geom

geom

2D geometry helpers: distance, angles, vectors, and intersections.

Помощники 2D-геометрии: расстояние, углы, векторы, пересечения.

FunctionDescription
geom.dist(x1,y1,x2,y2)Distance between two points
geom.angle(x1,y1,x2,y2)Angle from point 1 to point 2 (0–1, PICO-8 style)
geom.normalize(x,y)Returns unit vector (nx, ny)
geom.lerp2d(x1,y1,x2,y2,t)Linear interpolation between two points
geom.rot(x,y,a)Rotate point around origin by angle a (0–1)
ФункцияОписание
geom.dist(x1,y1,x2,y2)Расстояние между двумя точками
geom.angle(x1,y1,x2,y2)Угол от точки 1 к точке 2 (0–1, стиль PICO-8)
geom.normalize(x,y)Возвращает единичный вектор (nx, ny)
geom.lerp2d(x1,y1,x2,y2,t)Линейная интерполяция между двумя точками
geom.rot(x,y,a)Повернуть точку вокруг начала координат на угол a (0–1)
lua
local G = require("geom")

function _update()
  -- enemy chases player
  local d = G.dist(enemy.x, enemy.y, px, py)
  if d < 80 then
    local nx, ny = G.normalize(px - enemy.x, py - enemy.y)
    enemy.x += nx * 1.5
    enemy.y += ny * 1.5
  end
end

scene

scene

Manage game states (title, play, gameover) cleanly. Each scene has its own init, update, and draw callbacks.

Чистое управление состояниями игры (title, play, gameover). Каждая сцена имеет свои колбэки init, update и draw.

FunctionDescription
scene.add(name, tbl)Register a scene with {init, update, draw}
scene.go(name)Switch to scene (calls init)
scene.current()Returns current scene name
scene.update()Call current scene's update
scene.draw()Call current scene's draw
ФункцияОписание
scene.add(name, tbl)Зарегистрировать сцену с {init, update, draw}
scene.go(name)Переключиться на сцену (вызывает init)
scene.current()Возвращает имя текущей сцены
scene.update()Вызвать update текущей сцены
scene.draw()Вызвать draw текущей сцены
lua
local S = require("scene")

S.add("title", {
  init = function() t = 0 end,
  update = function()
    t += 1
    if btnp(4) then S.go("play") end
  end,
  draw = function()
    cls(0)
    print("PRESS Z", 100, 120, 7)
  end
})

S.add("play", {
  init = function() score = 0 end,
  update = function() score += 1 end,
  draw = function()
    cls(1)
    print("SCORE: "..score, 4, 4, 7)
  end
})

function _init() S.go("title") end
function _update() S.update() end
function _draw() S.draw() end

gameobject

gameobject

Lightweight entity system with tagging. Create, query, update, and draw game objects without manual list management.

Лёгкая система сущностей с тегами. Создавайте, запрашивайте, обновляйте и рисуйте игровые объекты без ручного управления списками.

FunctionDescription
go.new(props)Create an object with {x, y, tag, update, draw, ...}
go.all(tag)Return all objects with a given tag (or all if nil)
go.remove(obj)Mark object for removal
go.update()Call update on every object, then sweep removed
go.draw()Call draw on every object
go.clear()Remove all objects
ФункцияОписание
go.new(props)Создать объект с {x, y, tag, update, draw, ...}
go.all(tag)Вернуть все объекты с тегом (или все, если nil)
go.remove(obj)Пометить объект на удаление
go.update()Вызвать update у каждого объекта, затем удалить помеченные
go.draw()Вызвать draw у каждого объекта
go.clear()Удалить все объекты
lua
local go = require("gameobject")

function spawn_coin(x, y)
  go.new({
    x = x, y = y, tag = "coin",
    draw = function(self)
      circfill(self.x, self.y, 3, 10)
    end
  })
end

function _update()
  go.update()
  -- check player vs all coins
  for c in all(go.all("coin")) do
    if abs(px - c.x) < 6 and abs(py - c.y) < 6 then
      score += 1
      sfx(4)
      go.remove(c)
    end
  end
end

sprite

sprite

Animated sprite helper with frame sequencing and rotation support.

Помощник анимированных спрайтов с последовательностями кадров и поддержкой rotation.

FunctionDescription
sprite.anim(name, {frames}, fps)Define a named animation
sprite.new(n, x, y)Create sprite instance at position
sprite.play(s, name)Play named animation
sprite.update(s)Advance animation frame
sprite.draw(s)Draw sprite (with flip/rotation support)
sprite.bbox(s)Get bounding box → x, y, w, h
sprite.tapped(s)True if sprite was tapped/clicked this frame
sprite.held(s)True if touch/mouse is held on sprite
sprite.hover(s)True if cursor is over sprite (no press needed)
ФункцияОписание
sprite.anim(name, {frames}, fps)Определить именованную анимацию
sprite.new(n, x, y)Создать экземпляр спрайта в позиции
sprite.play(s, name)Запустить анимацию
sprite.update(s)Обновить кадр анимации
sprite.draw(s)Нарисовать спрайт (с flip/rotation)
sprite.bbox(s)Получить bounding box → x, y, w, h
sprite.tapped(s)True если спрайт нажат/кликнут в этом кадре
sprite.held(s)True если касание/мышь удерживается на спрайте
sprite.hover(s)True если курсор над спрайтом (без нажатия)
lua
local sp = require("sprite")

sp.anim("walk", {1,2,3,4}, 8)
local hero

function _init()
  hero = sp.new(1, 128, 200)
  sp.play(hero, "walk")
end

function _update()
  sp.update(hero)
  if btn(0) then hero.x -= 2; hero.flipX = true end
  if btn(1) then hero.x += 2; hero.flipX = false end
  -- touch input helpers
  if sp.tapped(hero) then sfx(0) end
end

function _draw()
  cls(0)
  sp.draw(hero)
end

Complete Example: All PlutoFL Libraries Together

Полный пример: все библиотеки PlutoFL вместе

A coin-collecting mini-game using timer, geom, scene, gameobject, and sprite:

Мини-игра по сбору монет с использованием timer, geom, scene, gameobject и sprite:

lua
local T  = require("timer")
local G  = require("geom")
local S  = require("scene")
local go = require("gameobject")
local SP = require("sprite")

-- ── title scene ──
S.add("title", {
  init = function() blink = 0 end,
  update = function()
    blink = (blink + 1) % 40
    if btnp(4) then S.go("play") end
  end,
  draw = function()
    cls(1)
    print("COIN GRAB", 88, 100, 10)
    if blink < 25 then
      print("PRESS Z", 100, 130, 7)
    end
  end
})

-- ── play scene ──
S.add("play", {
  init = function()
    px, py, score = 128, 128, 0
    anim = SP.new({1,2,3,4}, 6)
    go.clear()
    T.clear()
    -- spawn a coin every 1.5 seconds
    T.every(1.5, function()
      go.new({
        x = 8 + rnd(240), y = 8 + rnd(240),
        tag = "coin", age = 0,
        update = function(self)
          self.age += 1
          if self.age > 150 then go.remove(self) end
        end,
        draw = function(self)
          circfill(self.x, self.y, 3, 10)
        end
      })
    end)
  end,
  update = function()
    T.update()
    if btn(0) then px -= 2; anim.flip_x = true end
    if btn(1) then px += 2; anim.flip_x = false end
    if btn(2) then py -= 2 end
    if btn(3) then py += 2 end
    anim:update()
    go.update()
    -- collect coins using geom.dist
    for c in all(go.all("coin")) do
      if G.dist(px, py, c.x, c.y) < 7 then
        score += 1
        sfx(4)
        go.remove(c)
      end
    end
    if score >= 10 then S.go("win") end
  end,
  draw = function()
    cls(1)
    go.draw()
    anim:draw(px, py)
    print("COINS: "..score.."/10", 4, 4, 7)
  end
})

-- ── win scene ──
S.add("win", {
  init = function() sfx(7) end,
  update = function()
    if btnp(4) then S.go("title") end
  end,
  draw = function()
    cls(3)
    print("YOU WIN!", 96, 120, 10)
  end
})

function _init() S.go("title") end
function _update() S.update() end
function _draw() S.draw() end
Tip: PlutoFL libraries are designed to work together. Use scene for state management, gameobject for entities, timer for scheduling, geom for spatial math, and sprite for animations. Each library is small and does not count against your 9000-character cartridge limit.
Совет: Библиотеки PlutoFL спроектированы для совместной работы. Используйте scene для управления состояниями, gameobject для сущностей, timer для планирования, geom для пространственной математики, sprite для анимаций. Каждая библиотека мала и не считается в лимит 9000 символов картриджа.

14. Physics Engine

14. Физический движок

The physics library provides a lightweight 2D physics engine with gravity, collision detection and response, explosions, and spatial queries. It supports rectangular and circular bodies in three modes: dynamic (moves and collides), static (immovable), and sensor (detects overlap without physical response).

Библиотека physics предоставляет легковесный 2D-физический движок с гравитацией, детекцией и откликом столкновений, взрывами и пространственными запросами. Поддерживает прямоугольные и круглые тела в трёх режимах: dynamic (двигается и сталкивается), static (неподвижное), sensor (детектирует пересечение без физического отклика).

FunctionDescription
physics.world(gx, gy)Create a physics world with gravity
physics.step(w)Advance simulation one frame (call in _update). Auto sub-stepping at high velocities to prevent tunneling
physics.gravity(w, gx, gy)Change gravity vector
physics.body(w, kind, x, y, w, h)Create rectangular body
physics.circle(w, kind, x, y, r)Create circular body
physics.remove(w, b)Remove body from world
physics.push(b, fx, fy)Apply force (F/mass)
physics.impulse(b, ix, iy)Instant velocity change
physics.blast(w, x, y, r, f)Radial explosion
physics.at(w, px, py)Bodies at point
physics.overlaps(w, rx, ry, rw, rh)Bodies in rectangle
ФункцияОписание
physics.world(gx, gy)Создать физический мир с гравитацией
physics.step(w)Шаг симуляции (вызывать в _update). Авто sub-stepping при высоких скоростях для предотвращения туннелирования
physics.gravity(w, gx, gy)Изменить вектор гравитации
physics.body(w, kind, x, y, w, h)Создать прямоугольное тело
physics.circle(w, kind, x, y, r)Создать круглое тело
physics.remove(w, b)Удалить тело из мира
physics.push(b, fx, fy)Приложить силу (F/масса)
physics.impulse(b, ix, iy)Мгновенное изменение скорости
physics.blast(w, x, y, r, f)Радиальный взрыв
physics.at(w, px, py)Тела в точке
physics.overlaps(w, rx, ry, rw, rh)Тела в прямоугольнике

Body Properties

Свойства тела

PropertyDescriptionDefault
b.x, b.yPosition (top-left for rects, center for circles)
b.vx, b.vyVelocity0
b.bounceRestitution (0–1)0.2
b.frictionFriction (0–1)0.1
b.massMass1
b.tagString tag for identificationnil
СвойствоОписаниеПо умолчанию
b.x, b.yПозиция (верхний-левый для rect, центр для circle)
b.vx, b.vyСкорость0
b.bounceУпругость (0–1)0.2
b.frictionТрение (0–1)0.1
b.massМасса1
b.tagСтроковый тег для идентификацииnil

Collision Callback

Коллбэк столкновений

Set w.on_collide = function(a, b) to be notified of collisions. Return false to ignore a collision (useful for one-way platforms, triggers, etc.).

Установите w.on_collide = function(a, b) для оповещения о столкновениях. Верните false, чтобы игнорировать столкновение (полезно для односторонних платформ, триггеров и т.д.).

Example: Bouncing Balls

Пример: прыгающие мячи

local phys = require("physics")
local w = phys.world(0, 0.3)

function _init()
  phys.body(w, "static", 0, 240, 256, 16)
  phys.body(w, "static", -8, 0, 8, 256)
  phys.body(w, "static", 256, 0, 8, 256)
  for i = 1, 10 do
    local b = phys.circle(w, "dynamic", 64+rnd(128), rnd(100), 4+rnd(4))
    b.bounce = 0.6
  end
end

function _update()
  phys.step(w)
  if mbtnp(0) then phys.blast(w, mx(), my(), 40, 5) end
end

function _draw()
  cls(0)
  for _,b in ipairs(w.bodies) do
    if b.shape=="c" then circfill(b.x, b.y, b.r, 10)
    else rectfill(b.x, b.y, b.w, b.h, 5) end
  end
  print("TAP TO EXPLODE", 60, 2, 7)
end
Tip: The physics library handles up to ~50 bodies comfortably at 30 FPS. Use sensor bodies for triggers and pickups. Combine with gameobject and scene for complete game architecture.
Совет: Библиотека physics справляется с ~50 телами при 30 FPS. Используйте sensor-тела для триггеров и подбираемых предметов. Комбинируйте с gameobject и scene для полной архитектуры игры.