Sprite ও text effect

2D sprite ও text-এ shadow, outline, glow এবং blur, প্রতিটির খরচ কত, একটা চলমান sprite-এ এটা কী করে, এবং এটা যে হাতে-বানানো রেসিপিটা প্রতিস্থাপন করে, সহ।

এটা কীসের জন্য#

এখন পর্যন্ত একটা 2D sprite একটা tint আর একটা ছবি বহন করতে পারত, আর কিছুই না। একটা চরিত্রকে তার ব্যাকগ্রাউন্ড থেকে তুলে ধরতে আপনাকে হাতে ইফেক্টটা বানাতে হতো: একটা দ্বিতীয় sprite তৈরি করুন, সেটা কালো tint করুন, offset করুন, পেছনে বসান, আর প্রতিটা ফ্রেমে প্রথমটার সাথে তাল মিলিয়ে রাখুন। এটা কাজ করে। এটা প্রতি চরিত্রে চারটা অতিরিক্ত লাইনের হিসাবও, আর যেদিন এর একটা ভুলে যাবেন সেদিন shadow তার মালিকের পেছনে পিছিয়ে থাকবে।

চারটা কমান্ড আপনার হয়ে এটা করে, sprite ও text দুটোতেই:

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

রক্ষণাবেক্ষণের আর কিছুই নেই। sprite সরান, স্কেল করুন, ফ্লিপ করুন, এর অ্যানিমেশন চালান: ইফেক্টগুলো অনুসরণ করে।

চারটা ইফেক্ট#

Shadow#

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

offsetX এবং offsetY আপনার নিজের স্ক্রিন ইউনিটে, SpritePosition-এর মতোই গোনা: পজিটিভ Y মানে নিচেsoftness shadow ছড়িয়ে দেয়; opacity 0 থেকে 255 পর্যন্ত চলে। অন্যরকম না বললে রংটা কালো।

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

opacity 0-এর একটা shadow মানে কোনো shadow নেই। এভাবেই আপনি এটা বন্ধ করেন।

Outline#

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

outline sprite-টা যে rectangle-এ থাকে সেটা নয়, sprite-এর silhouette অনুসরণ করে: একটা transparent PNG ড্রয়িংয়ের চারপাশে একটা outline পায়, ফাইলের চারপাশে নয়। Thickness স্ক্রিন ইউনিটে, আর 1 থেকে 3 হলো কাজের রেঞ্জ। এর বেশি হলে outline যে আটটা দিক দিয়ে তৈরি সেগুলো আটটা lobe হিসেবে দেখা দিতে শুরু করে।

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

Glow#

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

একটা রঙিন halo, silhouette-এর বাইরে size ইউনিট ছড়িয়ে। এটা sprite-এর পেছনে আর shadow-এর সামনে বসে, এটাই আপনাকে দুটোই ব্যবহার করতে দেয়।

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

Blur#

SpriteBlur(spriteID, radius)

Blur বাকিদের থেকে আলাদা। বাকি তিনটে sprite-এর চারপাশে আঁকা হয়; blur sprite-এর নিজের ছবিকে তার একটা blur করা কপি দিয়ে বদলে দেয়। এর মানে জানার মতো দুটো জিনিস:

  • এটা প্রতি ফ্রেমে কিছুই খরচ করে না (blur করা ছবিটা একবারই গণনা করা হয়), এবং
  • blur ছবির প্রান্তে ক্লিপ করা। একটা ড্রয়িং যা তার PNG-এর প্রান্ত ছোঁয় সেটা তার halo-র সেই অংশ হারায় যেটা বাইরে পড়ত। blur করার পরিকল্পনা করলে ফাইলে কয়েক পিক্সেলের transparent মার্জিন রাখুন।

SpriteColor একটা blur করা sprite-এও কাজ করতে থাকে।

সবকিছু বন্ধ করা, এবং আবার চালু করা#

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

SpriteEffects আপনার সেটিংস মুছে ফেলে না। এটা একটা সুইচ, রিসেট নয়, তাই একটা চরিত্র ফ্ল্যাশ করতে পারে আর পরে আপনাকে তার shadow আবার ঘোষণা করতে হয় না।

text-এ একই চারটা#

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)

সিগনেচারগুলো sprite-এরগুলোর সাথে হুবহু মেলে, তাই নতুন করে শেখার কিছু নেই। আচরণে দুটো পার্থক্য:

  • একটা glow একটা shadow-র softness-এর উপর জেতে। দুটোই একই mechanism দিয়ে আঁকা হয়, আর কোনো softness ছাড়া একটা glow অদৃশ্য, অথচ কোনো softness ছাড়া একটা shadow তবুও একটা shadow। দুটোই চাইলে, glow-টাই softness পায়।
  • এগুলো vector টেক্সটে প্রযোজ্য, bitmap-font টেক্সটে নয়। TextFontImage দিয়ে আঁকা একটা টেক্সট sprite-এর একটা কম্পোজিশন; কমান্ডগুলো এর উপর নীরবে উপেক্ষা করা হয়। সেক্ষেত্রে sprite-গুলোতে sprite ইফেক্ট ব্যবহার করুন।

একটা কাজ করা উদাহরণ: যেকোনো কিছুর উপর পড়ার উপযোগী#

এই কমান্ডগুলো আসলে যে সমস্যাটা সমাধান করে তা হলো এমন contrast যা আপনি নিয়ন্ত্রণ করেন না। একটা সাদা score বরফের উপর অদৃশ্য; একটা কালোটা রাতে অদৃশ্য। একটা outline এটা একবারেই সমাধান করে, আপনি যত ব্যাকগ্রাউন্ডই আঁকুন না কেন।

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

একটা ইফেক্ট আবার পড়া#

আপনি যে প্রতিটা প্যারামিটার লেখেন তার একটা reader আছে, যেভাবে 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 prefix সহ।

জেনে রাখার মতো দুটো নিয়ম:

  • কখনও কোনো ইফেক্ট দেওয়া হয়নি এমন একটা sprite নিজেকে ON হিসেবে জানায়। এটাই ডিফল্ট অবস্থা; 0 জবাব দিলে একটা প্রোগ্রামকে বলা হতো যে সেটা কোনো ইফেক্ট সেট করার আগেই তার ইফেক্ট কেটে দেওয়া হয়েছে।
  • যে sprite নেই তার সম্পর্কে জিজ্ঞেস করলে 0 জবাব আসে, এটা প্রোগ্রাম থামায় না। ডায়ালেক্টটা সব জায়গাতেই প্রশ্নকে এভাবেই দেখে: SpriteHit কিছু স্পর্শ না হলে 0 জবাব দেয়, আর প্রোগ্রামগুলো সেই 0 পরীক্ষা না করেই পড়ে।

এতে যা খরচ হয়#

Shadow, outline এবং glow আঁকা হয়, তাই এগুলোর draw call খরচ হয়: shadow-র জন্য একটা (নরম হলে তিনটা), একটা glow-র জন্য চারটা, একটা outline-এর জন্য আটটা। দুশো outline করা sprite-সহ একটা স্ক্রিন মানে আঠারোশো quad আঁকা একটা স্ক্রিন, আর সেটা বোঝা যাবে। এগুলো সেই হাতে গোনা কয়েকটা জিনিসের জন্য, যেগুলোর দিকে খেলোয়াড়ের তাকানোর কথা: চরিত্র, pickup, নির্বাচিত unit।

Blur প্রতি ফ্রেমে কিছুই খরচ করে না।

একটা ব্যবহারিক নিয়ম: যা নড়ে আর গুরুত্বপূর্ণ তার উপর ইফেক্ট, ব্যাকগ্রাউন্ডে কোনোটাই না।

এক্সপোর্ট করা একটা গেমে যা পার হয়ে যায়#

টার্গেটShadow, outline, glowBlurText effects
এডিটরyesyesyes
HTML5 exportyesyesyes
Native ও wasm (Bevy)yesnono

Shadow, outline এবং glow সাধারণ sprite দিয়ে তৈরি, তাই sprite যেখানেই যায় এগুলোও সেখানে যায়। Blur এবং text ইফেক্টের এমন একটা সেতু দরকার যা Bevy টার্গেটের এখনও নেই: export conversion report এগুলোর নাম বলে দেয়, নীরবে বাদ না দিয়ে, তাই একটা build আপনাকে বলে দেয় এটা কী বহন করতে পারেনি।

যেখানে ইফেক্টগুলো নেই#

ইফেক্টের নাম একটা string হিসেবে নেওয়া SpriteEffect(id, "glow", ...) বলে কিছু নেই। প্রতিটা ইফেক্ট তার নিজের কমান্ড, যাতে এর প্যারামিটারগুলো টাইপ করার সময় নাম দেওয়া, ডকুমেন্ট করা এবং কমপ্লিট করা যায়। এই চারটার বাইরে যেকোনো কিছুর জন্য, আরও দুটো কমান্ড দায়িত্ব নেয়: additive বা multiplied ব্লেন্ডিংয়ের জন্য (একটা আগুন, একটা লেজার) SpriteBlendModes(id, src, dst), এবং LoadSpriteShader দিয়ে কম্পাইল করা একটা shader একটা sprite-এ বসাতে SpriteShader(id, shaderID)। দুটোই অনেকদিন ধরে কোনো ইমপ্লিমেন্টেশন ছাড়াই catalogue-এ ঘোষিত ছিল, আর এগুলো কল করলে একটা এরর দিত; এখন এগুলো কাজ করে।