V devátém díle našeho seriálu o programování her v XNA se podíváme na to, jak využít ovládání pohybovým senzorem/gyroskopem.
V tomto díle se pustíme do ovládání raketky pohybovým senzorem. Vyladění jeho vstupu pro hru může dát často dost nastavování a experimentování, zvlášť pokud budeme chtít nabídnout uživatelům i nějakou možnost kalibrace. Způsob, který si ukážeme v tomto návodu, by nám měl ale stačit pro většinu her.
Tip: Podívejte se, jak vypadá hra, kterou si díky tomuto seriálu můžete naprogramovat
Do našeho projektu si přidáme ještě jednu pomocnou třídu, nazveme si ji AccelerometerInput.cs. Samotné XNA nemá podporu pro pohybový senzor vestavěnou, budeme si proto muset do projektu přidat ještě odkaz na knihovnu z mobilního Silverlightu. V paletce Solution Explorer klikneme pravým tlačítkem myši na References a zvolíme Add Reference…. V seznamu vybereme Microsoft.Devices.Sensors.
Naši přidanou třídu AccelerometerInput si opět označíme jako public a doplníme jí usingy Microsoft.XNA.Framework a Microsoft.Devices.Sensors. Vložíme si do ní tyto dvě datové položky:
public Vector3 RawValue = Vector3.Zero; Accelerometer accel;
RawValue bude veřejně přístupná datová položka, ve které budeme mít uložené aktuální hodnoty přečtené z akcelerometru, accel potom bude odkaz na aktuální objekt akcelerometru. Je dobré zmínit, že akcelerometr funguje jinak, než ostatní věci v XNA (vstup z dotykového displeje apod.). Informaci o aktuálním náklonu zařízení si nezjišťujeme sami na začátku každého volání metody Update(), naopak nám akcelerometr tyto hodnoty sám aktivně posílá pravidelně pomocí událostí (eventů). Chová se tedy podobně, jako například myš ve Windows Forms aplikacích (tj. nemusíme se pořád ptát, jaká je její aktuální hodnota, ale při její změně se nám sama vyvolá potřebná událost). Tohle je docela příjemné chování. My si ho ale budeme muset nějak napasovat na zbytek XNA Frameworku.
V objektu AccelerometerInput si budeme udržovat aktuální přečtenou hodnotu v položce RawValue, tu si budeme aktualizovat vždy z dané události. Ze samotné hry už potom budeme přistupovat jen k této položce, v ní budeme mít vždy aktuální hodnotu. Přidáme si do této třídy metodu pro spuštění čtení dat z pohybového senzoru:
public void StartAccelerometerInput() { if (Accelerometer.IsSupported) { accel = new Accelerometer(); accel.CurrentValueChanged += new EventHandler<SensorReadingEventArgs<AccelerometerReading>>(accel_CurrentValueChanged); accel.Start(); } }
Vidíme zde navázání se na danou událost. Opět napíšeme += a zmáčkneme dvakrát klávesu Tab, vygeneruje se nám potřebná metoda. Do ní si doplníme jednoduché nastavení položky RawValue, celá metoda bude vypadat takto:
void accel_CurrentValueChanged(object sender, SensorReadingEventArgs<AccelerometerReading> e) { RawValue = e.SensorReading.Acceleration; }
Událost CurrentValueChanged je novinka ve Windows Phone SDK 7.1. Před tím se používala událost ReadingChanged. Tato nová varianta má výhodu, že si můžeme nastavit přesný interval, pod jakým by nám měly přicházet události. Ve výchozím nastavení se nám volají každé 2 milisekundy (tj. 500x za sekundu), tento interval si můžeme změnit pomocí položky TimeBetweenUpdates, například takto (použil jsem zkrácený zápis, kdy se položka nastaví už při vytváření objektu):
accel = new Accelerometer { TimeBetweenUpdates = TimeSpan.FromMilliseconds(10) };
My si tento interval měnit nejspíš nebudeme, postačí nám výchozí nastavení.
Z akcelerometru nám přichází hodnoty X, Y, a Z. Hodnoty X a Y se pohybují od –1 do 1, udávají aktuální náklon telefonu v ose X a Y. Nulové hodnoty znamenají, že telefon leží v klidu na rovné podložce. Hodnota –1 a 1 pro osu X znamená, že telefon je postaven na levé nebo pravé hraně. Obdobně s osou Y. Hodnota Z se může pohybovat od -1 až např. do 4, udává nám aktuální gravitační zrychlení. Pokud budeme máchat hodně telefonem ve vzduchu, hodnota Z bude vysoká. Můžeme si náš akcelerometr nyní zkusit použít v naší třídě Game1. Na začátku si ho nadeklarujeme:
AccelerometerInput accelInput = new AccelerometerInput();
V metodě Initialize() si spustíme jeho čtení:
accelInput.StartAccelerometerInput();
A v metodě Draw() si potom pomocí něho zkusíme rozpohybovat raketku (kód si vložíme někam na začátek této metody):
// Pohyb raketky
float yValue = -accelInput.RawValue.X * 1.5f; yValue = Math.Max(-1f, Math.Min(1f, yValue)); yValue = (yValue + 1f) / 2f; jet.Position.Y = yValue * GraphicsDevice.Viewport.Height - (jet.Size.Y / 2f);
Jak jsme si řekli, hodnoty z akcelerometru nám přichází od –1 do 1. My si budeme chtít nastavovat Y pozici raketky (od nuly až do výšky obrazovky), podle hodnot X akcelerometru. Musíme dát pozor na to, že při zobrazení obrazovky na šířku máme sice správně přehozené souřadnice vykreslování, vstup z akcelerometru ale zůstane stejný. Ten není závislý na dané orientaci displeje. Pokud chceme programovat hru využívající zobrazení na výšku i na šířku, musíme si souřadnice akcelerometru přepočítávat sami (tj. v některých případech prohodit osu X a Y).
Zkusíme si nyní spustit naši hru. Akcelerometr se dá nasimulovat i v emulátoru, můžete si zde zkusit kliknout na malou šipečku napravo a poté otestovat hýbání s telefonem (taháním za červenou tečku). Určitě ale doporučím si hru spustit i na opravdovém zařízení. Když si ji otestujeme na telefonu, s největší pravděpodobností zjistíme, že raketka se sice pohybuje celkem správně, ale hodně trhaně. Je to proto, že vstup z akcelerometru je hodně zašuměný. Je to docela nepřesná součástka, dává nám jen určitou hlavní informaci o náklonu.
Spolu s Mangem přišlo také nové rozhraní pro čtení ze senzorů, takzvané Motion API. To by mělo využívat všech dostupných součástek v telefonu (akcelerometru, kompasu a u novějších zařízení i gyroskopu). Hodí se například pro programování “rozšířené reality”. Dává nám k dispozici mnohem více informací, nejen aktuální gravitační zrychlení telefonu, ale i například konkrétní hodnoty pootočení telefonu v 3D prostoru (Yaw, Pitch a Roll). Tyto hodnoty jsou už vyhlazené, takže máme určitým způsobem ušetřenou práci. Pokud chceme toto nové API využít, stačí nám jen změnit klíčové slovo Accelerometer za Motion. Jak ale zjistíme, pro použití ve hrách jsou zde hodnoty vyhlazené možná až moc, odezva je hodně zpomalená. My si ukážeme rozšíření klasického přístupu pomocí třídy Accelerometer.
Pro vyčištění vstupu z akcelerometru existuje několik metod. Obecně můžeme použít jakýkoliv způsob odšumování signálu, například různé lowpass filtry apod. Nám ale bude stačit využít obyčejného aritmetického průměru, s tím, že navíc budeme ignorovat malé změny v osách X a Y. Uvidíme, že tento způsob bude působit docela dobře (hodnoty parametrů si případně sami doladíte). Do naší třídy AccelerometerInputsi přidáme ještě tyto datové položky:
// Vyhlazování vstupu akcelerometru
const int samplesCount = 10; Vector3[] accelMemory = new Vector3[samplesCount]; Vector3 accelSum = Vector3.Zero; bool memoryInitialized = false; int bufferIndex = 0;
Položka samplesCount bude velikost našeho vyhlazovacího okénka. Poslední hodnoty si budeme udržovat v poli accelMemory, do toho si budeme zapisovat podle indexu bufferIndex jako do “kruhového” bufferu. Stejně, jako jsme měli položku RawValue, tak si do deklarací také přidáme položku Value, ve které si budeme udržovat aktuální vyhlazený výstup:
public Vector3 Value = Vector3.Zero;
Událost CurrentValueChanged si doplníme o tento kód, ve kterém si budeme naše pole posledních hodnot aktualizovat a určovat z něj výslednou průměrnou hodnotu:
lock (accelMemory) { if (!memoryInitialized) { for (int i = 0; i < samplesCount; i++) accelMemory[i] = RawValue; accelSum = RawValue * samplesCount; memoryInitialized = true; } bufferIndex++; if (bufferIndex >= samplesCount) bufferIndex = 0; accelSum += RawValue; accelSum -= accelMemory[bufferIndex]; accelMemory[bufferIndex] = RawValue; Value = accelSum / samplesCount; }
Takto máme naprogramovaný aritmetický průměr, vždy přes posledních 10 hodnot. Tohoto kódu se nemusíte bát, bude vám ho stačit napsat jen jednou, třídu AccelerometerInput si pak jednoduše budete moci využívat i v dalších hrách. Ještě si sem přidáme ignorování malých hodnot, aby nám raketka neplavala nahoru a dolů, pokud si budeme mobil jen volně držet v ruce. Před accelSum += RawValue si přidáme ještě tento kousek kódu:
// Nízké hodnoty
if (Math.Abs(accelMemory[bufferIndex].X - RawValue.X) < 0.03f) RawValue.X = accelMemory[bufferIndex].X; if (Math.Abs(accelMemory[bufferIndex].Y - RawValue.Y) < 0.03f) RawValue.Y = accelMemory[bufferIndex].Y;
Můžeme si nyní odzkoušet, jak se toto naše vyhlazování vstupu bude chovat. V metodě Draw() ve třídě Game1 si změníme řádek, kde jsme získávali vstup z akcelerometru, z hodnoty RawValue na Value:
float yValue = -accelInput.Value.X * 1.5f;
A zkusíme si spustit hru. Nyní by už měla být poměrně dobře hratelná. V příštím díle si odzkoušíme odchytávání gest na displeji, povíme si, jak vykreslit text nebo přehrát hudbu a doplníme si do naší hry pár posledních drobností.