Efekty sprite'ów i tekstu

Cień, obrys, poświata i rozmycie na sprite'ach 2D i na tekście, ile kosztuje każdy z nich, co robi z poruszającym się sprite'em i jaką ręcznie sklejaną metodę zastępuje.

Do czego to służy#

Do tej pory sprite 2D mógł nosić tint i obraz, i nic więcej. Żeby oderwać postać od tła, trzeba było zbudować efekt ręcznie: stworzyć drugi sprite, zabarwić go na czarno, przesunąć, umieścić z tyłu i trzymać go w kroku z pierwszym na każdej pojedynczej klatce. To działa. To też cztery dodatkowe linie księgowości na postać, i w dniu, w którym zapomnisz o jednej z nich, cień zaczyna wlec się za swoim właścicielem.

Cztery polecenia robią to za ciebie, na sprite'ach i na tekście:

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

Nic więcej do utrzymywania. Przesuń sprite, przeskaluj go, odbij, odtwórz jego animację: efekty podążają za nim.

Cztery efekty#

Cień#

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

offsetX i offsetY są w twoich własnych jednostkach ekranu, liczonych tak samo jak w SpritePosition: dodatnie Y to w dół. softness rozprasza cień; opacity biegnie od 0 do 255. Kolor jest czarny, chyba że powiesz inaczej.

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

Cień o opacity 0 to brak cienia. Tak właśnie go wyłączasz.

Obrys#

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

Obrys podąża za sylwetką sprite'a, nie za prostokątem, w którym żyje: przezroczysty PNG dostaje obrys wokół rysunku, nie wokół pliku. Thickness jest w jednostkach ekranu, a użyteczny zakres to 1 do 3. Powyżej tego osiem kierunków, z których budowany jest obrys, zaczyna być widoczne jako osiem płatków.

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

Poświata#

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

Kolorowa poświata, rozprzestrzeniająca się o size jednostek poza sylwetkę. Siedzi za sprite'em i przed cieniem, co pozwala używać obu naraz.

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

Rozmycie#

SpriteBlur(spriteID, radius)

Rozmycie jest tym nietypowym. Pozostałe trzy są rysowane wokół sprite'a; rozmycie zastępuje własny obraz sprite'a jego rozmytą kopią. To oznacza dwie rzeczy warte poznania:

  • nic nie kosztuje na klatkę (rozmyty obraz jest liczony raz), oraz
  • rozmycie jest przycinane na krawędzi obrazu. Rysunek, który dotyka krawędzi swojego PNG, traci tę część poświaty, która wypadłaby poza plik. Zostaw kilka przezroczystych pikseli marginesu w pliku, jeśli planujesz go rozmyć.

SpriteColor nadal działa na rozmytym sprite'cie.

Wyłączanie wszystkiego i włączanie z powrotem#

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

SpriteEffects nie kasuje twoich ustawień. To przełącznik, nie reset, więc postać może błysnąć, a ty nie musisz potem na nowo deklarować jej cienia.

Te same cztery na tekście#

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)

Sygnatury zgadzają się dokładnie z tymi od sprite'ów, więc nie ma niczego nowego do nauczenia. Dwie różnice w zachowaniu:

  • Poświata wygrywa z miękkością (softness) cienia. Obie są rysowane tym samym mechanizmem, a poświata bez softness jest niewidoczna, podczas gdy cień bez softness wciąż jest cieniem. Gdy poprosisz o obie naraz, to poświata dostaje softness.
  • Działają na tekście wektorowym, nie na tekście z czcionki bitmapowej. Tekst narysowany za pomocą TextFontImage to kompozycja sprite'ów; polecenia są na nim po cichu ignorowane. W takim przypadku użyj efektów sprite'ów na samych sprite'ach.

Przykład: czytelne na wszystkim#

Problem, który te polecenia naprawdę rozwiązują, to kontrast, którego nie kontrolujesz. Biały wynik jest niewidoczny na śniegu; czarny jest niewidoczny w nocy. Obrys rozwiązuje to raz, na każde tło, jakie kiedykolwiek narysujesz.

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

Odczytywanie efektu z powrotem#

Każdy parametr, który zapisujesz, ma swój odpowiednik do odczytu, tak jak SpriteColor ma SpriteColorRed. To reguła tego dialektu, i te polecenia się jej trzymają.

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

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

Pełny zestaw, dla każdej rodziny: SpriteShadowOffsetX, SpriteShadowOffsetY, SpriteShadowBlur, SpriteShadowAlpha, SpriteShadowColorRed, SpriteShadowColorGreen, SpriteShadowColorBlue, SpriteGlowRed, SpriteGlowGreen, SpriteGlowBlue, SpriteGlowSize, SpriteOutlineThickness, SpriteOutlineRed, SpriteOutlineGreen, SpriteOutlineBlue, SpriteBlurRadius, SpriteEffectsEnabled, plus te same siedemnaście z przedrostkiem Text.

Dwie reguły warte poznania:

  • Sprite, któremu nigdy nie nadano efektu, zgłasza się jako ON. To stan domyślny; odpowiedź 0 mówiłaby programowi, że jego efekty są wyłączone, zanim jakikolwiek ustawił.
  • Pytanie o sprite'a, który nie istnieje, odpowiada 0, nie zatrzymuje programu. Tak dialekt traktuje pytania wszędzie: SpriteHit odpowiada 0, gdy nic nie zostało dotknięte, a programy czytają to 0 bez sprawdzania go wcześniej.

Ile to kosztuje#

Cień, obrys i poświata są rysowane, więc kosztują draw calls: jeden na cień (trzy, gdy jest miękki), cztery na poświatę, osiem na obrys. Ekran z dwustoma sprite'ami z obrysem to ekran rysujący tysiąc osiemset quadów, i to będzie widać. Są pomyślane na garstkę rzeczy, na które gracz ma patrzeć: postać, przedmiot do zebrania, zaznaczoną jednostkę.

Rozmycie nic nie kosztuje na klatkę.

Praktyczna zasada: efekty na tym, co się rusza i ma znaczenie, żadnych na tle.

Co przechodzi do wyeksportowanej gry#

CelCień, obrys, poświataRozmycieEfekty tekstu
Edytortaktaktak
Eksport HTML5taktaktak
Natywny i wasm (Bevy)taknienie

Cień, obrys i poświata są zbudowane ze zwykłych sprite'ów, więc podróżują wszędzie tam, gdzie sprite'y. Rozmycie i efekty tekstu potrzebują mostka, którego cele Bevy jeszcze nie mają: raport konwersji eksportu je nazywa, zamiast po cichu je porzucać, więc build mówi ci, czego nie zdołał przenieść.

Gdzie tych efektów nie ma#

Nie ma SpriteEffect(id, "glow", ...) przyjmującego nazwę efektu jako string. Każdy efekt to osobne polecenie, żeby jego parametry mogły być nazwane, udokumentowane i podpowiadane w trakcie pisania. Dla wszystkiego poza tymi czterema przejmują dwa inne polecenia: SpriteBlendModes(id, src, dst) dla mieszania addytywnego albo mnożonego (ogień, laser), oraz SpriteShader(id, shaderID), żeby nałożyć na sprite'a shader skompilowany przez LoadSpriteShader. Oba były zadeklarowane w katalogu od dawna, bez implementacji, a ich wywołanie zgłaszało błąd; teraz działają.