Эффекты для спрайтов и текста
Тень, обводка, свечение и размытие для 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. Обе долгое время были объявлены в каталоге без
реализации, и их вызов вызывал ошибку; теперь они работают.