28. července 2008

Programování postupných procesů ve Flashi

Při vytváření složitější Flash animace často narazíte na problém, jak šikovně naprogramovat jednotlivé na sebe postupně navazující fáze animace, kdy pro přechod z jedné fáze do druhé je nutné, aby byla splněna pokaždé jiná podmínka. Použití konstrukce if - else if - else je přitom problematické, protože do ní musíte zahrnout nejen podmínky nutné pro provedení určité části kódu, ale také podmínky, které vylučují provedení dané části částí kódu po nabytí platnosti jiných podmínek.

Je nutné opravdu dobře promyslet všechny možné kombinace podmínek a přesně je vymezit. Musíte se přitom zabývat i ošetřením kombinací, které Vás vlastně ani nezajímají. Ve výsledku pak často obdržíte velmi nepřehledný a nepružný kód, který nám znesnadní práci a budoucí úpravy.

Ano, je pravda, že následující kód je jasný, elegantní a přehledný:

_root.onEnterFrame = function()
{
// stanoveni aktualni hodnoty promenne cas
if (cas > 5)
{
// kod pro casovy interval (5,10>
}
else if (cas > 10)
{
// kod pro casovy interval (10,15>
}
else if (cas > 15)
{
// kod pro casovy interval (15,20>
}
else if (cas > 20)
{
// nic ... uz se nema provadet zadny kod
}
else
{
// kod pro casovy interval <0,5>
}
};

Nyní si ale představte, že po dosažení času 5s chcete nějaké akce provést jen jednorázově a po té až do 10s provádět jiné příkazy. Dále, že po dosažení 10s musíte otestovat, jestli již doběhla nějaká jiná část animace až do konce a teprve pak provést opět jednorázově nějaké příkazy a až do 15s provádět pak úplně jiné a teprve pak vás bude zajímat, jestli čas už přesáhl 15s atd. Vznikne Vám pak kód, z něhož se kamsi vytratila elegance a hlavně "jednoznačnost na první pohled":

_root.onEnterFrame = function()
{
// stanoveni aktualni hodnoty promenne cas
if (cas > 5)
{
if (!cas5)
{
// kod jednorazovy po dosazeni 5s
cas5 = true;
}
else
{
// kod pro casovy interval (5,10>
}
}
else if (cas > 10)
{
if (dobehlaAnimacePro10)
{
if (!cas10)
{
// kod jednorazovy po dosazeni 10s a splneni podminky dobehnuti jine animace
cas10 = true;
}
else
{
if (cas > 15)
{
// kod pro casovy interval (15,20>
}
else if (cas > 20)
{
// nic ... uz se nema provadet zadny kod
}
else
{
// kod pro casovy interval (10,15>
}
}
}
}
else
{
// kod pro casovy interval <0,5>
}
};

Hrůza, že?

Když se nad tím, co provádí uvedený rozhodovací strom zamyslíte, uvědomíte si, že se jedná o postupné fáze nějakého procesu, kde se jako postačující rozlišovací údaj pro provedení té které fáze dá použít pouze název dané fáze. Ten si můžete uložit do pomocné proměnné s příznačným jménem faze a pak už je snadné napsat pouze jednoúrovňový řetězec podmínek if - else if, kde se bude testovat pouze hodnota proměnné faze a nic jiného. Pro ohraničení jednotlivých částí kódu lze stejně tak použít i konstrukci switch - case - ta se mně osobně líbí více:

faze = "_0-5_";
_root.onEnterFrame = function()
{
// stanoveni aktualni hodnoty promenne cas
switch (faze)
{
case "_0-5_" :
// kod pro casovy interval <0,5>
if (cas > 5)
{
faze = "_5_";
}
break;
case "_5_" :
// kod jednorazovy po dosazeni 5s
faze = "_5-10_";
break;
case "_5-10_" :
// kod pro casovy interval (5,10>
if (cas > 10)
{
faze = "_10.1_";
}
break;
case "_10.1_" :
if (dobehlaAnimacePro10)
{
faze = "_10.2_";
}
break;
case "_10.2_" :
// kod jednorazovy po dosazeni 10s a splneni podminky dobehnuti jine animace
faze = "_10-15_";
break;
case "_10-15_" :
// kod pro casovy interval (10,15>
if (cas > 15)
{
faze = "_15-20_";
}
break;
case "_15-20_" :
// kod pro casovy interval (15,20>
if (cas > 20)
{
faze = "_20_";
}
break;
case "_20_" :
// nic ... uz se nema provadet zadny kod
// tuto vetev case vlastne uz ani psat nemusime
break;
}
};

Takovýto zápis má několik výhod. Pokud si pojmenujete jednotlivé fáze dostatečně výstižně, bude se Vám v kódu lehčeji orientovat. Také již nemusíte řešit ošetřování všech možných kombinací ohraničujících podmínek. Zde řešíte už jen to, co v dané fázi přichází v úvahu a nic víc. Můžete tak snadno přidávat či ubírat další fáze, a je jedno jestli na začátku, konci či přímo v těle stromu procesu. Můžete také některé fáze v rámci ladění programu snadno „přemostit“, a to pouhým přepsáním resp. připojením řádku pro změnu obsahu proměnné faze s odpovídající hodnotou. Můžete si na různých místech procesu vytvořit speciální fáze, které pak můžete použít jako vstupní body při skoku doprostřed procesu atd.

Princip tohoto řešení je, že použijete pomocnou proměnnou faze, přičemž pouze její obsah a nic jiného určuje, co se má v danou chvíli v rámci probíhajícího postupného procesu provádět a co již ne nebo ještě ne. Tím si často spletitý mnohoúrovňový rozhodovací strom abstrahujete do dvou jednoduše pojmutelných logických vrstev. V první vrstvě se pouze na základě obsahu proměnné faze rozhodne, co se bude provádět a teprve v druhé vrstvě, uvnitř každé části procesu se řeší, kdy se má hodnota proměnné faze změnit a jak.

Detekce vyběhnutí kursoru myši ze Stage Flash Animace v AS 2

(doplněno)

Řešil jsem problém, jak v ActionScriptu 2.0 detekovat událost, kdy myš opustí Stage, tzn. že uživatel přejede s kursorem mimo oblast Flash animace někam do okolní html stránky. Chtěl jsem se obejít bez programování nějakých pomocných funkčností mimo vlastní Flash animaci, tedy bez JavaScriptu apod. V ActionScriptu 3.0 je řešení jednoduché, jak možné se dočíst v článku: Zarovnání objektů, detekce opuštění scény na flash.cz. V ActionScriptu 2.0 v3ak nejsou k dispozici odpovídající nástroje. Jak tedy "na to"?

Zdánlivě jednoduché, leč ne vždy funkční řešení:

Pro detekci opuštění Stage kursorem myši se nabízí zdánlivě jednoduché řešení: do Flash animace umísti neviditelný obdélník, který bude od krajů Stage vzdálen na všech stranách o mezeru, jejíž šířku zvolíme nějak rozumně. Pak stačí jen testovat kolizi polohy kursoru myši s tímto obdélníkem. Je-li kursor myši s obdélníkem v kolizi, je zřejmé, že uživatel se myší pohybuje nad Flash animací. Je-li kursor mimo obdélník, tak je uživatel na okraji animace, takže na nic uvnitř animace nemíří, nebo ji kursorem dokonce opustil. Proto je šířku mezery nutno zvolit rozumně, na 10-20% šířky Stage Flash Animace.

Funguje to skvěle. Tedy až do okamžiku, kdy uživatel odjede z Flash animace příliš rychle. Musíme si totiž uvědomit dvě skutečnosti. Za prvé, že testování kolize kursoru myši s obdélníkem uvnitř probíhá pouze jednou za časový interval, který je daný rychlostí přehrávání snímků Flash animace. Délka intervalu se v praxi pohybuje v řádu desetin až setin sekundy, což je na počítačové poměry dlooooouhá doba, během které může kursor myši poskočit o stovky pixelů. Za druhé, jakmile kursor myši opustí Stage Flash Animace, zůstane v proměnných _xmouse a _ymouse, vracejících aktuální polohu kursoru myši, "viset" poslední známá poloha myši před opuštěním Stage a tato poloha by mohla být stále "kolizní".

Co s tím? Zeptal jsem se v diskusi na flash.cz a od Petka jsem dostal tip, jak by to asi mohlo jít. Ten tip vedl k řešení, které se zatím ukazuje jako plně funkční. Díky Petko!

Tedy, jak na to:

Zapomeňme na testování kolize s obdélníkem. Nebudeme to potřebovat. Budeme si naopak do nějaké proměnné typu pole ukládat současnou [1] a předchozí [0] polohu kursoru myši. Budeme to dělat vždy v rámci události onEnterFrame, tedy každý okamžik, kdy Flash animace snímá polohu kursoru myši. Například takto:

_root.mouseX = new Array(0, 0);
_root.mouseY = new Array(0, 0);
_root.onEnterFrame = function()
{
_root.mouseX[0] = _root.mouseX[1];
_root.mouseX[1] = _root._xmouse;
_root.mouseY[0] = _root.mouseY[1];
_root.mouseY[1] = _root._ymouse;
};

Kdykoli pak nastane událost onEnterFrame vypočteme možnou budoucí [2] polohu kursoru a jakmile nám tato předpovězená poloha vyskočí ze Stage Flash animace, víme, že uživatel opustil Flash. Tento výpočet provedeme vždy před tím, než nastavíme nové hodnoty [0] a [1], takže:

_root.mouseX = new Array(0, 0);
_root.mouseY = new Array(0, 0);
_root.mouseOutOfStage = false;
_root.onEnterFrame = function()
{
_root.mouseX[2] = _root.mouseX[1] + (_root.mouseX[1] - _root.mouseX[0]);
_root.mouseY[2] = _root.mouseY[1] + (_root.mouseY[1] - _root.mouseY[0]);
if ((_root.mouseX[2] > Stage.width or _root.mouseX[2] < 0) or (_root.mouseY[2] > Stage.height or _root.mouseY[2] < 0))
{
_root.mouseOutOfStage = true;
}
_root.mouseX[0] = _root.mouseX[1];
_root.mouseX[1] = _root._xmouse;
_root.mouseY[0] = _root.mouseY[1];
_root.mouseY[1] = _root._ymouse;
};

Jednoduché že? Jenže co se situací, kdy uživatel vykoná uvnitř Flash animace pohyb s dost velkým krokem kursoru myši, aby to uvedený script vyhodnotil, jako úmysl opustit oblast Stage Flash animace, ale zastaví jej ještě uvnitř? A jak zahrnout funkčnost, která by nastavila zpětně _root.mouseOutOfStage = false;, když dojde k takové situaci, resp. i tehdy, když se uživatel nad oblast Stage kursorem opět vrátí?

Naštěstí je v ActionScriptu 2.0 k dispozici detekce události, jestli se kursor myši pohnul, či nikoli, a ta nám pomůže vše velmi jednoduchým způsobem vyřešit.

Výsledné řešení detekce opuštění Stage Flash Animace kursorem myši tedy vypadá takto:

_root.mouseX = new Array(0, 0, 0);
_root.mouseY = new Array(0, 0, 0);
_root.mouseOutOfStage = false;
_root.onEnterFrame = function()
{
_root.mouseX[2] = _root.mouseX[1] + (_root.mouseX[1] - _root.mouseX[0]);
_root.mouseY[2] = _root.mouseY[1] + (_root.mouseY[1] - _root.mouseY[0]);
if ((_root.mouseX[2] > Stage.width or _root.mouseX[2] < 0) or (_root.mouseY[2] > Stage.height or _root.mouseY[2] < 0))
{
_root.mouseOutOfStage = true;
}
_root.mouseX[0] = _root.mouseX[1];
_root.mouseX[1] = _root._xmouse;
_root.mouseY[0] = _root.mouseY[1];
_root.mouseY[1] = _root._ymouse;
};
_root.onMouseMove = function()
{
_root.mouseOutOfStage = false;
};

Toto řešení je založeno na předpokladu, který bude zřejmě oprávněný: když už uživatel z rychlého pohybu zabrzdí, takže Stage Flash animace přeci jen na poslední chvíli neopustí, ač by se to z predikce pohybu kursoru myši mohlo zdát, nestihne "zarazit" pohyb kursoru náhle. Než úplně zastaví, udělá kursorem myši ještě alespoň malý "krůček", který nás díky události onMouseMove opět "vrátí do hry".

Pomocí výpočtu předpokládané budoucí polohy bychom zpomalení pohybu mohli zjistit také a pomocí dalších podmínek if ošetřit, ovšem takto je výsledný script přeci jen jednodušší a navíc funguje i pro detekci opětovného najetí kursoru nad Stage.

Doplnění

Při testování se ukázalo, že uvedené řešení nevyhoví v případě, kdy uživatel opustí Stage Flash animace velmi pomalým pohybem myši. Tento problém však můžeme lehce odstranit tak, že se v ActionScriptu nebudeme testovat překročení samotných hranic Stage, ale budeme testovat překročení hranic obdélníkové oblasti odsazené od okraje Stage rovnoměrně po celém obvodu o určitou malou mezeru. Upravený script pak může vypadat takto:

_root.mouseX = new Array(0, 0, 0);
_root.mouseY = new Array(0, 0, 0);
_root.mouseOutOfStage = false;
_root.stagePadding = 2;
_root.mouseXmin = _root.stagePadding;
_root.mouseXmax = Stage.width - _root.stagePadding;
_root.mouseYmin = _root.stagePadding;
_root.mouseYmax = Stage.height - _root.stagePadding;
_root.onEnterFrame = function()
{
_root.mouseX[2] = _root.mouseX[1] + (_root.mouseX[1] - _root.mouseX[0]);
_root.mouseY[2] = _root.mouseY[1] + (_root.mouseY[1] - _root.mouseY[0]);
if ((_root.mouseX[2] < _root.mouseXmin or _root.mouseX[2] > _root.mouseXmax) or (_root.mouseY[2] < _root.mouseYmin or _root.mouseY[2] > _root.mouseYmax))
{
_root.mouseOutOfStage = true;
}
_root.mouseX[0] = _root.mouseX[1];
_root.mouseX[1] = _root._xmouse;
_root.mouseY[0] = _root.mouseY[1];
_root.mouseY[1] = _root._ymouse;
};
_root.onMouseMove = function()
{
_root.mouseOutOfStage = false;
};