Dart StageXL - Jak napsat flashovku bez Flashe (a námahy)
Dart je vynikající jazyk a StageXL je vynikající knihovna. Umožní vám nad HTML canvas naprogramovat hru, kterou si pustíte i na Androidu nebo iOS. Pochopitelně v prohlížeči, bez rozšíření a pluginů. Následující článek je v podstatě přepisem mého workshopu z letošního DevFest 2014.
Výstupem tutoriálu je jednoduchá interaktivní blbůstka á la Rorschachův test, kterou si můžete online prohlédnout na we.are.hiring.cz (Chrome prosím, ale ne kvůli StageXL, ale kvůli Polymeru, o tom ovšem jindy).
(Článek původně vyšel na zdrojak.cz)
Co budete potřebovat
Postačí vám DartEditor a základní znalost Dart-u, žádné pokročilé techniky nám nehrozí. Zdrojáky tutoriálu jsou na GitHub, takže si je stáhněte, otevřete v DartEditoru a jdeme na to.
pubspec.yaml
V závislostech vidíte, že toho moc nevidíte:
name: devfest_2014_stageXL
author: +TomasZverina
description: Workshop o StageXL
dependencies:
browser: any
stagexl: any
StageXL je poměrně stabilní, takže “any” nám tu nijak zvlášť nevadí, ale pokud se bojíte, specifikujte si verzi přesně.
Krok 0: Inicizalizace StageXL
StageXL je technologie pro web. Obvykle svůj projekt pomocí “pub” kompilujete do JavaScriptu a nasazujete na web server. Celkem logicky je tedy základem html dokument. Kromě běžné inicializace Dart-u pak potřebujete jen:
<canvas id="stage" width="500" height="400" style="border: 1px solid black;"></canvas>
Nad tímhle canvasem bude StageXL operovat. Takhle ho nastartujeme …
var canvas = html.querySelector('#stage');
_stage = new Stage(canvas);
var renderLoop = new RenderLoop();
renderLoop.addStage(_stage);
… a takhle do něj přidáme první objekt:
var shape = new Shape();
shape.graphics.rect(0, 0, 50, 50);
shape.graphics.fillColor(Color.Red);
shape.x = 100;
shape.y = 100;
_stage.addChild(shape);
Asi je jasné co to dělá - na souřadnice 100x100 dej červený čtverec 50x50.
Shape je objekt z knihovny StageXL, který se dědí z DisplayObject. DisplayObject je bázová třída pro všechno co se ve StageXL renderuje a obsahuje všechno, co tak můžete potřebovat pro 2D grafiku:
num x = 0.0; // souřadnice objektu v rodiči
num y = 0.0; // souřadnice objektu v rodiči
num pivotX = 0.0; // referenční bod [0,0], ke kterému vztahuji svoje ...
num pivotY = 0.0; // souřadnice. Moje "těžiště", střed rotace atd.
num scaleX = 1.0; // natažení v ose X
num scaleY = 1.0; // natažení v ose Y
num skewX = 0.0; // zkosení
num skewY = 0.0;
num rotation = 0.0; // rotace
num alpha = 1.0; // průhlednost (1=plný, 0=zcela průhledný)
bool visible = true; // je viditelný?
Jestli je můj objekt Shape, Bitmap nebo skupina více objektů nastrkaná do jednoho Sprite - to už je jedno.
A rovnou si ukážeme, jak objekty animovat. Náš Shape necháme, aby se během 100 vteřin (konstanta REPEAT) 100x otočil o 380°.
Tween t = new Tween(shape, REPEAT * 1, TransitionFunction.linear);
t.animate.rotation.to(REPEAT * math.PI);
_stage.renderLoop.juggler.add(t);
Animace se dělají tak, že vytvoříte Tween, řeknete co a jak dlouho se má animovat a přidáte Tween do “juggleru” - správce animací.
Když se teď ohlédnete na to málo co jsme zatím vytvořili, zjistíte, že už nám vlastně skoro nic nechybí. Teď už můžeme napsat v podstatě cokoliv:
- umíme si připravit HTML a zinicializovat prostředí
- umíme přidávat a pozicovat objekty
- umíme je animovat
Zatím neumíme pracovat s obrázky (bitmapami) a neumíme přijímat události (myš, touch).
Krok 1: ResourceManager
Stáhnout si obrázky, zvuky apod. zdroje nějakou dobu trvá. Bylo by užitečné mít možnost si vypsat seznam věcí, které budeme potřebovat, zobrazit “laoding” screen a až se zdroje stáhnou, tak abychom mohli náš “stage” spustit. K tomu slouží ResourceManager.
void main() {
_Rorschach r = new _Rorschach();
r.initGame();
}
class _Rorschach {
ResourceManager _resourceManager = new ResourceManager();
initGame() {
_resourceManager
..addBitmapData("bg", "bg.jpg")
..addBitmapData("stain", "stain.png");
_resourceManager.load().then(_runGame);
}
_runGame(_) {
// muzeme to spustit
}
}
Vytvoříme instanci ResourceManager, zaregistrujeme zdroje, které budeme potřebovat, a spustíme load(). Load() vrací Future<ResourceManager>, tzn. až bude staženo (then), zavolá se naše metoda _runGame, která ResourceManager bere jako argument. V tomhle případě ale argument ignoruji, ResourceManager mám uložený jako vlastnost mojí třídy _Rorschach.
Stažené bitmapy pak můžeme použít ve stage:
var bgBitmap = new Bitmap(_resourceManager.getBitmapData("bg"));
_stage.addChild(bgBitmap);
Bitmap je opět DisplayObject, takže ji můžeme pozicovat, otáčet, animovat, …
Krok 2: Responzivita
V tomto kroce si upravíme HTML do finální podoby a ukážeme si, jak StageXL nastavit, aby dobře fungovala bez ohledu na velikost obrazovky.
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1, user-scalable=no">
<script async type="application/dart" src="step01.dart"></script>
<script async src="packages/browser/dart.js"></script>
<style type="text/css">
body { margin: 0; padding: 0; overflow: hidden; }
#stage { position: absolute; width: 100%; height: 100%; }
</style>
</head>
<body>
<canvas id="stage"></canvas>
</body>
</html>
Canvas si nastylujeme na celou obrazovku a meta tagem viewport vysvětlíme prohlížeči, jak má s naším dokumentem zacházet. V tomto případě vytváříme spíš aplikaci než WWW stránku, takže si můžeme dovolit takové neslušnosti jako user-scalable=no.
Teď je potřeba StageXL naučit, jak s dostupným prostorem pracovat.
var bgBitmap = new Bitmap(_resourceManager.getBitmapData("bg"));
var canvas = html.querySelector('#stage');
_stage = new Stage(canvas, width: bgBitmap.bitmapData.width, height: bgBitmap.bitmapData.height);
_stage.scaleMode = StageScaleMode.NO_BORDER;
_stage.align = StageAlign.NONE;
V konstruktoru Stage předáme rozměry, na které ji optimalizujeme - v našem případě šířku a výšku Bitmapy, kterou máme na pozadí.
Režimem StageScaleMode.NO_BORDER jí říkáme: “Vyplň dostupné místo tak, aby kolem tebe nebyl žádný prázdný prostor, ale nedeformuj se”.
Režim StageAlign.NONE zařídí, aby se Stage centrovala uprostřed dostupného viewportu. Pusťte si hotový krok 5 a zahýbejte velikostí okna, myslím, že to bude jasné.
Kromě toho si v kroce připravíme pole skvrn, které budeme později zobrazovat.
for (int a = 0; a < 2 * STAINS_COUNT; a++) {
Bitmap stain = new Bitmap(stainData);
stain.visible = false;
stain.pivotX = stainPx;
stain.pivotY = stainPy;
if (a % 2 == 1) {
// zrcadlove prevracene skrvrny na druhe strane události myši
stain.scaleX = -1;
}
STAINS.add(stain);
_stage.addChild(stain);
}
Krok 3: Umísťování skvrn na stage
- html - se nezměnilo
- dart
Teď už to začne být zajímavější. Napojíme se na stream událostí myši nad stage a budeme kreslit.
int _stainPointer = 0;
...
_stage.onMouseMove.listen(_placeStain);
...
void _placeStain(MouseEvent event) {
double rotation = _randomGenerator.nextDouble() * PI;
double x = event.stageX;
double y = event.stageY;
STAINS[_stainPointer].x = x;
STAINS[_stainPointer].y = y;
STAINS[_stainPointer].rotation = rotation;
STAINS[_stainPointer].visible = true;
// a presunem ukazatel na dalsi
_stainPointer = (_stainPointer + 1) % STAINS_COUNT;
}
Mimochodem - proč vůbec mám nějaké pole těch skvrn a řeším tu složitosti se _stainPointer? Inu tak - rád recykluju. Mohl bych pokaždé vytvořit novou Bitmap a staré Bitmapy zahazovat, ale garbage collector je taky jenom člověk, tak proč ho trápit.
Krok 4: Zrcadlení skvrn
- html - se nezměnilo
- dart
V dalším kroce zařídím, aby se skvrny zobrazovali nejen pod myší, ale i zrcadlově na druhé straně “papíru”. To už je jen nudná 2D geometrie, tak se jen lehce mrkněte do zdrojáků, co se změnilo.
Krok 5: Filtr událostí
No a jsme na konci! Teď už nám to dělá co chceme, snad jen že se skvrny objevují zbytečně často. K tomu, abych jejich četnost omezil, použiju API Dart streamů, konkrétně metodu “where”:
_stage.onMouseMove.where(_timeToPlaceFilter).listen(_placeStain);
...
bool _timeToPlaceFilter(Event event) {
_nowTime = new DateTime.now().millisecondsSinceEpoch;
return (_nowTime - _lastEventTime > MINIMUM_DELAY);
}
void _placeStain(Event event) {
...
_lastEventTime = _nowTime;
}
Kudy dál
StageXL je velmi silný nástroj. Pokud se vám líbí Dart, není v podstatě o čem přemýšlet. Kdekoliv, kde funguje Dart (resp. Dartem vygenerovaný JavaScript), bude fungovat i StageXL.
Pokračujte pochopitelně na stránkách StageXL. Pravdou je, že dokumentace knihovny není úplně košatá, ale je tam alespoň spousta komentovaných a názorných příkladů, které si můžete nastudovat.
Kromě toho co jsem vám tu ukázal, umí StageXL masky, filtry, WebGL akceleraci, přehrávání zvuků a spoustu dalších libůstek.
P.S.: RIP, Flash. Chybět nám nebudeš, muahahaha!