Эффекты для спрайтов и текста

Тень, обводка, свечение и размытие для 2D-спрайтов и текста: что стоит каждый из них, что происходит с движущимся спрайтом, и какой самодельный рецепт они заменяют.

Для чего это нужно#

До сих пор 2D-спрайт мог нести только тон и изображение, и больше ничего. Чтобы отделить персонажа от фона, эффект приходилось строить вручную: создать второй спрайт, покрасить его в чёрный, сместить, поставить позади и держать в шаге с первым на каждом кадре. Это работает. Это также четыре лишние строки учёта на каждого персонажа, и в тот день, когда одну из них забудут, тень начинает тащиться за своим владельцем отдельно.

Четыре команды делают это за вас, для спрайтов и для текста:

hero = LoadSprite("hero.png")
SpritePosition(hero, 160, 120)

SpriteShadow(hero, 4, 4, 6, 128)        ' offset x, offset y, softness, opacity
SpriteOutline(hero, 2, 255, 255, 255)   ' thickness, colour
SpriteGlow(hero, 0, 240, 255, 8)        ' colour, size

DO
  Sync()
LOOP

Больше ничего поддерживать не нужно. Двигайте спрайт, масштабируйте его, отражайте, проигрывайте его анимацию: эффекты следуют за ним.

Четыре эффекта#

Тень#

SpriteShadow(spriteID, offsetX, offsetY, softness, opacity)
SpriteShadowColor(spriteID, red, green, blue)

offsetX и offsetY заданы в ваших собственных экранных единицах, отсчёт такой же, как у SpritePosition: положительный Y идёт вниз. softness растягивает тень; opacity изменяется от 0 до 255. Цвет чёрный, пока вы не укажете иное.

SpriteShadow(hero, 3, 5, 4, 110)
SpriteShadowColor(hero, 20, 0, 40)      ' a cold shadow, not a grey one

Тень с непрозрачностью 0, это отсутствие тени. Именно так её отключают.

Обводка#

SpriteOutline(spriteID, thickness, red, green, blue)

Обводка следует за силуэтом спрайта, а не за прямоугольником, в котором он находится: прозрачный PNG получает обводку вокруг рисунка, а не вокруг файла. Толщина задаётся в экранных единицах, и полезный диапазон, это от 1 до 3. Выше этого значения восемь направлений, из которых строится обводка, начинают проявляться как восемь лепестков.

SpriteOutline(hero, 2, 255, 255, 255)   ' the readable-on-anything outline
SpriteOutline(hero, 0, 0, 0, 0)         ' off

Свечение#

SpriteGlow(spriteID, red, green, blue, size)

Цветное сияние, расходящееся на size единиц за пределы силуэта. Оно находится позади спрайта и перед тенью, что и позволяет использовать оба эффекта сразу.

SpriteGlow(pickup, 255, 210, 0, 10)     ' this one is worth walking to

Размытие#

SpriteBlur(spriteID, radius)

Размытие, это исключение среди четырёх. Остальные три рисуются вокруг спрайта; размытие заменяет само изображение спрайта его размытой копией. Из этого следуют две вещи:

  • оно ничего не стоит за кадр (размытое изображение вычисляется один раз), и
  • оно обрезается по краю изображения. Рисунок, касающийся края своего PNG, теряет ту часть ореола, которая должна была бы выйти за пределы файла. Оставьте несколько прозрачных пикселей отступа в файле, если планируете его размывать.

SpriteColor продолжает работать на размытом спрайте.

Выключить всё и включить обратно#

SpriteEffects(spriteID, 0)   ' all four off
SpriteEffects(spriteID, 1)   ' all four back, exactly as they were

SpriteEffects не стирает ваши настройки. Это переключатель, а не сброс, поэтому персонаж может мигать без необходимости заново объявлять его тень после этого.

Те же четыре эффекта для текста#

score = CreateText("SCORE 0")
TextPositionSet(score, 10, 10)
TextSizeSet(score, 6)

TextOutline(score, 3, 0, 0, 0)          ' readable on any background
TextShadow(score, 2, 2, 4, 160)
TextGlow(score, 255, 80, 0, 6)
TextBlur(score, 0)
TextEffects(score, 1)

Сигнатуры в точности совпадают со спрайтовыми, учить нечего нового. Два отличия в поведении:

  • Свечение побеждает мягкость тени. Оба эффекта рисуются одним и тем же механизмом, и свечение без мягкости невидимо, тогда как тень без мягкости остаётся тенью. Когда запрошены оба, мягкость достаётся свечению.
  • Это применяется к векторному тексту, а не к тексту в растровом шрифте. Текст, нарисованный через TextFontImage, это композиция спрайтов; на нём эти команды молча игнорируются. В этом случае используйте эффекты спрайтов на самих спрайтах.

Разобранный пример: читаемо на чём угодно#

Настоящая проблема, которую решают эти команды, это контраст, который вы не контролируете. Белый счёт невидим на снегу; чёрный невидим ночью. Обводка решает это раз и навсегда, для любого фона, который вы когда-либо нарисуете.

CreateText(1, "SCORE 0")
TextPositionSet(1, 10, 10)
TextSizeSet(1, 8)
TextColorSet(1, 255, 255, 255, 255)
TextOutline(1, 3, 0, 0, 0)

hero = LoadSprite("hero.png")
SpriteName(hero, "hero")
SpriteShadow(hero, 4, 6, 5, 120)
SpriteOutline(hero, 2, 255, 255, 255)

points = 0
DO
  points = points + 1
  TextString(1, "SCORE " + Str(points))
  Sync()
LOOP

Считывание эффекта обратно#

У каждого записываемого параметра есть свой считыватель, так же как у SpriteColor есть SpriteColorRed. Это правило диалекта, и эти команды ему следуют.

IF SpriteEffectsEnabled(hero) = 1
  SpriteEffects(hero, 0)
ENDIF

alpha = SpriteShadowAlpha(hero)      ' 0 to 255
size  = SpriteGlowSize(hero)         ' 0 means no glow

Полный набор, по семействам: SpriteShadowOffsetX, SpriteShadowOffsetY, SpriteShadowBlur, SpriteShadowAlpha, SpriteShadowColorRed, SpriteShadowColorGreen, SpriteShadowColorBlue, SpriteGlowRed, SpriteGlowGreen, SpriteGlowBlue, SpriteGlowSize, SpriteOutlineThickness, SpriteOutlineRed, SpriteOutlineGreen, SpriteOutlineBlue, SpriteBlurRadius, SpriteEffectsEnabled, плюс те же семнадцать с префиксом Text.

Два правила, которые стоит знать:

  • Спрайт, которому никогда не назначали эффект, сообщает, что он ВКЛЮЧЁН. Это состояние по умолчанию; ответ 0 сказал бы программе, что её эффекты отключены ещё до того, как она хоть один задала.
  • Запрос к спрайту, которого не существует, возвращает 0 и не останавливает программу. Так диалект обходится с вопросами повсюду: SpriteHit возвращает 0, когда ничего не задето, и программы читают этот 0, не проверяя его отдельно.

Во что это обходится#

Тень, обводка и свечение, это то, что рисуется, поэтому они стоят вызовов отрисовки: один для тени (три, когда она мягкая), четыре для свечения, восемь для обводки. Экран с двумястами обведёнными спрайтами, это экран, рисующий тысячу восемьсот quad-полигонов, и это будет заметно. Эти эффекты предназначены для той горстки вещей, на которые игрок действительно должен смотреть: персонаж, подбираемый предмет, выделенный юнит.

Размытие не стоит ничего за кадр.

Практическое правило: эффекты на том, что движется и важно, и ничего на фоне.

Что переносится в экспортированную игру#

ЦельТень, обводка, свечениеРазмытиеТекстовые эффекты
Редактордадада
Экспорт HTML5дадада
Native и wasm (Bevy)данетнет

Тень, обводка и свечение построены из обычных спрайтов, поэтому они переносятся всюду, куда переносятся спрайты. Размытию и текстовым эффектам нужен мост, которого пока нет у целей Bevy: отчёт о конвертации при экспорте называет их прямо, а не отбрасывает молча, так что сборка сообщает, что именно она не смогла перенести.

Чего среди эффектов нет#

Не существует команды SpriteEffect(id, "glow", ...), принимающей имя эффекта строкой. Каждый эффект, это своя собственная команда, чтобы её параметры можно было называть, документировать и дополнять по мере ввода. Помимо этих четырёх, есть ещё две команды: SpriteBlendModes(id, src, dst) для аддитивного или умноженного смешивания (огонь, лазер), и SpriteShader(id, shaderID) для наложения на спрайт шейдера, скомпилированного через LoadSpriteShader. Обе долгое время были объявлены в каталоге без реализации, и их вызов вызывал ошибку; теперь они работают.