Knihovna ClanLib (8)

28. 6. 2004
Doba čtení: 7 minut

Sdílet

Dnes dokončíme povídání o třídě CL_Sprite. Povíme si, jak je možné pohodlně vytvořit obrázek s průhledným pozadím pomocí programu Image Magic (věc, která se může hodit nejen programátorům v ClanLibu - uvedený příkaz je skriptovatelný a může vám tedy ušetřit hodně času). Uděláme si také úvod do signálů a slotů, což jsou velcí pomocníci při realizaci komunikace mezi objekty.

Několik poznámek ke spritům

Se třídou CL_Sprite jsme se již dosti podrobně seznámili v předchozích dvou dílech. Přesto jsem vám zůstal dlužen ještě několik triků, které se mi při přáci s ClanLibem poměrně osvědčily.

Jedním z problémů, které je obvykle nutné řešit, pokud se rozhodneme pracovat s animacemi skládajícími se z několika obrázků, je ten, že takové obrázky jsou typicky obdelníky. Obvykle však obdelník není to, co chceme vykreslovat, ba naopak typicky chceme vykreslit nějaký hodně nepravidelný tvar, např. postavu panáčka ve hře. V takové situaci bývá nepřípustné, aby se společně s panáčkem vykreslilo i pozadí.

Klasická situace je, že máme obrázek uložený v nějakém grafickém souboru. Nejčastěji jsou to soubory s příponami jako bmp, png, jpg, tga (a mnoho dalších). Tyto přípony rozlišují různé způsoby, jakými se dají informace o našem obrázku uložit. Ty se mohou docela lišit zejména způsobem, jak data komprimují, aby byl soubor co nejmenší, aby se potřebná data získala co nejrychleji? Přiznám se, že o těchto způsobech nevím skoro nic, avšak pro naše potřeby bude stačit následující zjednodušený popis.

Pro každý bod (pixel) našeho obrázku jsou v souboru čtyři údaje. Tři z nich udávají podíl jednotlivých barevných složek (červená, zelená, modrá) na výsledné barvě tohoto pixelu. Čtvrtým údajem je průhlednost pixelu. Tento údaj je pro nás nejpodstatnější. ClanLib ho totiž sám dokáže přečíst a při vykreslování obrázku resp. snímku spritu tuto průhlednost zachovává. To je velice příjemné, protože nejen, že tak můžeme vytvořit zcela průhledné pozadí, ale dokonce v samotném obrázku mohou být některé části třeba částečně průhledné (jako když se díváte přes barevné sklíčko – vidíte, co je pod ním, ale v jiných barvách než bez něj).

Pokud jste zkušenými grafiky, je pro vás toto jedinou podstatnou informací – ClanLib bere v úvahu masku průhlednosti, kterou váš obrázek má, a vykreslí ho podle vašich představ, navíc půhlednost a barevnost můžete ještě později ovlivnit přímo z programu pomocí metod CL_Sprite, které jsme již popsali.

V opačném případě asi stojíte před problémem, před nímž jsem stál i já, a ptáte se, jak tuto půhlednost do obrázku dostat, jak změnit již hotový obrázek, aby pozadí bylo průhledné?

Mně osobně se osvědčilo řešení, které mi poradil kamarád (za což mu moc děkuji). Tím řešením je program Image Magic, který je volně dostupný pro Linux i pro Windows. Jedná se o skriptovatelný program, který dělá čest svému jménu a s obrázky dokáže opravdu velkou spoustu kouzel. Jedním z těch, které se velice hodily mně, je schopnost převést jeden obrázek na druhý s tím, že v cílovém se stanou všechny pixely zvolené barvy průhlednými.

Pokud toto dokážeme, můžeme prostě kreslit naše obrázky třeba na bílé pozadí, jak jsme to dosud dělali, a pak je prostě překonvertovat na obrázky, v nichž se bílá automaticky označí za průhlednou.

Příkaz Image Magicu, který převede zdrojový obrázek na cílový obrázek formátu daného jeho příponou se zadanou průhlednou barvou, je takovýto:

convert '-transparent' 'rgb(R,G,B)' zdroj cil 

Konkrétně pokud chcete převést obr.bmp na obr.tga tak, že bílá barva bude průhledná, stačí napsat:

convert '-transparent' 'rgb(255,255,255)' obr.bmp obr.tga 

Ve Windows by měly fungovat verze bez apostrofů. Příkaz se zadává, jak už je asi patrné, z příkazové řádky.

V některých grafických editorech, jako je třeba Gimp, PhotoPaint nebo PhotoShop, je samozřejmě možné s maskou průhlednosti pracovat také.

Nutno podotknout, že některé formáty průhlednost vůbec nemusí podporovat, což však není případ žádného z formátů podporovaných ClanLibem.

Úvod do signálů a slotů

Zasílání zpráv mezi objekty je jedním ze základních principů objektově orientovaného programování. Myšlenka je asi taková, že máme jakýsi abstraktní svět, v němž existují, vznikají a zanikají objekty, a tyto objekty mezi sebou komunikují zasíláním zpráv. Užitečnost tohoto přístupu je například v tom, že velmi dobře funguje společně se zapouzdřením. Pokud objekt A chce vykonat nějakou akci, třeba proto, že ho o to zprávou požádal jiný objekt, provede ji typicky sám. Může se však stát, že vykonání jeho úkolu je závislé i na činnosti jiného objektu B, a v tu chvíli objekt A zašle zprávu objektu B o tom, jakou činnost od něj požaduje.

V zásadě pak jsou dvě možnosti. Buď A potřebuje počkat, až B svou činnost dokončí, nebo ne. Při programování v C++ obvykle používáme první možnost. Zaslání zprávy objektu B objektem A je vlastně zavoláním nějaké metody objektu B objektem A. Například když z nějaké metody objektu A voláme B.Run(), znamená to, že objekt A posílá zprávu Run objektu B.

K zaslání zprávy tedy potřebujeme znát adresu příjemce, což je v našem případě identifikátor objektu B, a typ zprávy, tj. Run (bez parametrů).

C++ je jazyk, který byl navržen se značným důrazem na výkonost, a proto nechává na programátorovi samotném, jakým způsobem a do jaké míry bude simulovat objektově orientovaný svět, dává mu k tomu však dostatečné prostředky.

Obvykle si vystačíme s tím, že píšeme třídy a používáme jejich instance a zprávy mezi nimi zasíláme tak, jak bylo výše popsáno. Občas by se nám však mohla hodit i trochu větší abstrakce a pružnost v komunikaci mezi objekty.

To, co nás totiž velmi omezuje při dosud popsaném způsobu komunikace, je požadavek, že odesilatel musí znát adresy všech příjemců. Představte si třeba, že máte objekt Bomba, který když vybuchne, zničí všechno kolem sebe. Když se tedy Bomba rozhodne vybuchnout, potřebujeme, aby se o tom dozvěděly všechny objekty v jejím okolí, tj. potřebujeme jim o tom zaslat zprávu. Asi si dovedete představit, jak nepříjemné je udržovat v Bombě adresy všech objektů v jejím okolí.

V takové situaci by se nám mnohem víc líbil jiný přístup při zasílání zpráv. Chtěli bychom, aby Bomba prostě vyslala zprávu o tom, že vybuchla, zcela neadresně do našeho abstraktního světa, a ten, koho by zajímala, by ji měl možnost přijmout. To znamená, že bychom přenesli zodpovědnost na příjemce. Každý objekt by se pak prostě rozhodl, které zprávy ho zajímají, a ty by pak přijímal.

Přesně takový způsob komunikace mezi ojekty nám umožňují ClanLibovské signály a sloty (princip přejatý z qt). Signál je zpráva, kterou objekt vyšle do světa, aniž by udal adresáta (spam, který se dostane jen k těm, kteří ho chtějí :-)). Slot je zařízení, které ze světa odchytává zprávy zvoleného typu a reaguje na příjem způsobem, který si zvolíme (typicky volá nějakou metodu příjemce).

Typickými oblastmi, kde nacházejí signály a sloty velmi dobré uplatnění, jsou například odchytávání událostí klávesnice, myši, GUI a další.

Signály a sloty v ClanLibu

Pojďme se tedy podívat na konkrétní jednoduchý příklad použití signálů a slotů inspirovaný jedním z ClanLibovských overview:

class T_Hodnota {
public:
    T_Hodnota() : HHodnota(0) {};

    int Hodnota() const { return HHodnota; };

    // nastavi novou hodnotu a vysle signal
    void set_Hodnota(int);

    // signal vysilany pri zmene hodnoty
    CL_Signal_v1<int> sig_ZmenaHodnoty;

private:
    int HHodnota;
};

Třída T_Hodnota uchovává celočíselnou hodnotu. Tuto hodnotu můžeme získat a modifikovat. Podstatné je to, že modifikace vyvolá signál, který informuje okolní svět o tom, že byla hodnota změněna. CL_Signal_v1 je šablona. Z části názvu za posledním podtržítkem je možné určit počet parametrů (nějaká rozumně malá čísla – v příkladě je to 1) a typ návratové hodnoty. Doposud je myslím podporován pouze void označený písmenem v. Podívejme se na implementaci:

void T_Hodnota::set_Hodnota(int H) {
    if (H != HHodnota) {
        HHodnota = H;
        sig_ZmenaHodnoty(v);
    }
}

Jak je vidět, vyslání signálu vyvoláme tak, že ho zavoláme, jako by se jednalo o funkci.

Nyní třída, která otestuje tu předešlou. Využívá slot slot_ZmenaHodnoty k připojení sig_ZmenaHodnoty:

ict ve školství 24

class T_HodnotaTest {
public:

  // v konstruktoru pripojime signal do slotu
  T_HodnotaTest();

  void Test();

private:

  // udalost vyvolana zmenou hodnoty
  void on_ZmenaHodnoty(int H);

  T_Hodnota Hodnota;

  // prislusny slot do nejz pripojime signal
  CL_Slot slot_ZmenaHodnoty;
}

T_HodnotaTest::T_HodnotaTest() {

  // pripojeni signalu do slotu
  slot_ZmenaHodnoty = Hodnota.sig_ZmenaHodnoty.connect(
    this, &T_HodnotaTest::on_ZmenaHodnoty);
}

void T_HodnotaTest::Test() {
  Hodnota.set_Hodnota(49);
}

void T_HodnotaTest::on_ZmenaHodnoty(int H) {
  std::cout << "Nova hodnota je: " << H << std::endl;
}

Na tomto místě pro jistotu podotýkám, že byste jako programátoři neměli dělat žádné předpoklady o tom, v jakém pořadí příjemci odchytí určitý signál, pokud jich je několik.

Závěrem

Tímto příkladem zakončíme dnešní úvod do signálů a slotů a také celý článek. Příště se k této tématice ještě vrátíme. Povíme si ještě nějaké podrobnosti a podíváme se na nějaké konkrétnější příklady využití.

Autor článku