Effets sur les sprites et les textes

Ombre, contour, halo et flou sur les sprites 2D et sur le texte, avec ce que chacun coûte, ce qu'il devient sur un sprite qui bouge, et la recette à la main qu'il remplace.

À quoi ça sert#

Jusqu'ici un sprite 2D pouvait porter une teinte et une image, et rien d'autre. Pour détacher un personnage de son décor, il fallait fabriquer l'effet à la main : créer un second sprite, le teindre en noir, le décaler, le ranger derrière, et le remettre en phase avec le premier à chaque image. Ça marche. C'est aussi quatre lignes de comptabilité par personnage, et le jour où l'une d'elles est oubliée, l'ombre traîne derrière son propriétaire.

Quatre commandes le font pour vous, sur les sprites et sur le texte :

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

SpriteShadow(hero, 4, 4, 6, 128)        ' décalage x, décalage y, douceur, opacité
SpriteOutline(hero, 2, 255, 255, 255)   ' épaisseur, couleur
SpriteGlow(hero, 0, 240, 255, 8)        ' couleur, taille

DO
  Sync()
LOOP

Rien d'autre à entretenir. Déplacez le sprite, changez sa taille, retournez-le, jouez son animation : les effets suivent.

Les quatre effets#

L'ombre#

SpriteShadow(spriteID, decalageX, decalageY, douceur, opacite)
SpriteShadowColor(spriteID, rouge, vert, bleu)

decalageX et decalageY sont dans vos unités d'écran, comptées comme SpritePosition : un Y positif va vers le bas. douceur étale l'ombre ; opacite va de 0 à 255. La couleur est noire tant que vous n'en dites rien.

SpriteShadow(hero, 3, 5, 4, 110)
SpriteShadowColor(hero, 20, 0, 40)      ' une ombre froide, pas une ombre grise

Une ombre d'opacité 0 n'est pas une ombre. C'est comme ça qu'on la coupe.

Le contour#

SpriteOutline(spriteID, epaisseur, rouge, vert, bleu)

Le contour suit la silhouette du sprite, pas le rectangle qui la contient : un PNG transparent reçoit un contour autour du dessin, pas autour du fichier. L'épaisseur est en unités d'écran, et de 1 à 3 est la plage utile. Au-delà, les huit directions dont le contour est fait commencent à se voir comme huit lobes.

SpriteOutline(hero, 2, 255, 255, 255)   ' le contour lisible sur n'importe quoi
SpriteOutline(hero, 0, 0, 0, 0)         ' coupé

Le halo#

SpriteGlow(spriteID, rouge, vert, bleu, taille)

Un halo coloré, qui déborde de taille unités au-delà de la silhouette. Il se range derrière le sprite et devant l'ombre, ce qui permet d'utiliser les deux.

SpriteGlow(bonus, 255, 210, 0, 10)      ' celui-là vaut le détour

Le flou#

SpriteBlur(spriteID, rayon)

Le flou est le seul des quatre à ne pas se dessiner autour du sprite : il remplace l'image du sprite par une copie floue. Deux conséquences à connaître :

  • il ne coûte rien par image (le flou est calculé une fois), et
  • il est coupé au bord de l'image. Un dessin qui touche le bord de son PNG perd la part de halo qui tomberait dehors. Laissez quelques pixels transparents de marge dans le fichier si vous comptez le flouter.

SpriteColor continue de fonctionner sur un sprite flou.

Tout couper, et tout rendre#

SpriteEffects(spriteID, 0)   ' les quatre coupés
SpriteEffects(spriteID, 1)   ' les quatre rendus, exactement tels quels

SpriteEffects n'efface pas vos réglages. C'est un interrupteur, pas une remise à zéro : un personnage peut clignoter sans qu'il faille re-déclarer son ombre après coup.

Les mêmes quatre sur le texte#

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

TextOutline(score, 3, 0, 0, 0)          ' lisible sur n'importe quel fond
TextShadow(score, 2, 2, 4, 160)
TextGlow(score, 255, 80, 0, 6)
TextBlur(score, 0)
TextEffects(score, 1)

Les signatures sont exactement celles des sprites : il n'y a rien de neuf à apprendre. Deux différences de comportement :

  • Le halo l'emporte sur la douceur de l'ombre. Les deux passent par le même mécanisme, et un halo sans douceur est invisible alors qu'une ombre sans douceur reste une ombre. Quand vous demandez les deux, c'est le halo qui prend la douceur.
  • Ça s'applique au texte vectoriel, pas au texte en police bitmap. Un texte dessiné avec TextFontImage est une composition de sprites ; les commandes y sont ignorées sans bruit. Dans ce cas, posez les effets de SPRITE sur les sprites.

Un exemple complet : lisible sur n'importe quoi#

Le vrai problème que ces commandes règlent, c'est un contraste que vous ne maîtrisez pas. Un score blanc est invisible sur la neige ; un score noir est invisible la nuit. Un contour règle la question une fois, pour tous les fonds que vous dessinerez.

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

Relire un effet#

Chaque paramètre écrit a son lecteur, comme SpriteColor a SpriteColorRed. C'est la règle du dialecte, et ces commandes la suivent.

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

alpha = SpriteShadowAlpha(hero)      ' de 0 à 255
taille = SpriteGlowSize(hero)        ' 0 veut dire pas de halo

Le jeu complet, par famille : SpriteShadowOffsetX, SpriteShadowOffsetY, SpriteShadowBlur, SpriteShadowAlpha, SpriteShadowColorRed, SpriteShadowColorGreen, SpriteShadowColorBlue, SpriteGlowRed, SpriteGlowGreen, SpriteGlowBlue, SpriteGlowSize, SpriteOutlineThickness, SpriteOutlineRed, SpriteOutlineGreen, SpriteOutlineBlue, SpriteBlurRadius, SpriteEffectsEnabled, plus les mêmes dix-sept préfixées Text.

Deux règles à connaître :

  • Un sprite qui n'a jamais reçu d'effet se dit ALLUMÉ. C'est l'état par défaut ; répondre 0 dirait à un programme que ses effets sont coupés avant même qu'il en pose un.
  • Interroger un sprite qui n'existe pas rend 0, ça n'arrête pas le programme. C'est ainsi que le dialecte traite les questions partout : SpriteHit rend 0 quand rien n'est touché, et les programmes lisent ce 0 sans le tester.

Ce que ça coûte#

L'ombre, le contour et le halo sont dessinés, donc ils coûtent des appels de dessin : un pour l'ombre (trois quand elle est douce), quatre pour un halo, huit pour un contour. Un écran de deux cents sprites contourés est un écran qui dessine dix-huit cents quads, et ça se voit. Ces effets sont faits pour la poignée de choses que le joueur doit regarder : le personnage, le bonus, l'unité sélectionnée.

Le flou ne coûte rien par image.

Une règle pratique : des effets sur ce qui bouge et qui compte, aucun sur le décor.

Ce qui passe dans un jeu exporté#

CibleOmbre, contour, haloFlouEffets de texte
L'éditeurouiouioui
Export HTML5ouiouioui
Natif et wasm (Bevy)ouinonnon

L'ombre, le contour et le halo sont faits de sprites ordinaires : ils voyagent partout où les sprites voyagent. Le flou et les effets de texte demandent un pont que les cibles Bevy n'ont pas encore : le rapport de conversion de l'export les nomme plutôt que de les laisser tomber en silence, donc un build vous dit ce qu'il n'a pas pu emporter.

Ce qui n'existe pas#

Il n'y a pas de SpriteEffect(id, "glow", ...) prenant le nom de l'effet en chaîne. Chaque effet est sa propre commande pour que ses paramètres puissent être nommés, documentés et complétés à la frappe. Au-delà de ces quatre-là, deux autres commandes prennent le relais : SpriteBlendModes(id, src, dst) pour un mélange additif ou multiplié (un feu, un laser), et SpriteShader(id, shaderID) pour poser sur un sprite un shader compilé par LoadSpriteShader. Toutes deux étaient déclarées au catalogue depuis longtemps sans implémentation, et les appeler levait une erreur ; elles fonctionnent maintenant.