Un caso molto interessante da considerare è quello in cui vogliamo leggere sequenze (le c.d. "combo") di input, ad esempio per far compiere al giocatori azioni speciali o combinazioni particolari di mosse.
Ipotizziamo di essere interessati alla sequenza di tasti ABCD
, premuti uno dopo l'altro in rapida successione (diciamo, ad un intervallo di tempo non superiore a 5 decimi di secondo).
Per prima cosa, dichiariamo un'enum (denominata InputSequence
, nel nostro esempio) per contenere lo stato della sequenza letta fino ad ora:
enum InputSequence
{
A, // È stato premuto il bottone A
WaitForB, // In attesa del tasto successivo
AB, // È stato premuto anche B
WaitForC, // In attesa del tasto successivo
ABC, // È stato premuto anche C
WaitForD, // In attesa del tasto successivo
ABCD, // È stato premuto anche D. La sequenza è completa!
NONE, // Nessun bottone premuto
}
Creiamo due variabili di tipo KeyboardState per memorizzare lo stato attuale della tastiera e quello immediatamente precedente, nonché una variabile di tipo double per tenere traccia del tempo trascorso:
KeyboardState curr = new KeyboardState(), prev = new KeyboardState();
double time = 0.0;
Nel metodo Update aggiorniamo gli stati della tastiera e del timer:
prev = curr;
curr = Keyboard.GetState();
time = gameTime.TotalGameTime.TotalSeconds;
Fin qui niente di difficile. Ecco adesso la parte più complessa.
Definiamo un metodo (denominato "AABBs
", nel nostro esempio) in grado di restituire un IEnumerable
per tenere traccia degli stati della nostra combo. Ad ogni ciclo di Update
, andremo poi a leggere il valore successivo da questo IEnumerable
. Per poter funzionare, il nostro metodo dovrà essere capace di ciclare indefinitivamente, per cui utilizzeremo un cliclo while la cui condizione risulti sempre soddisfatta:
IEnumerable<InputSequence> AABBs()
{
while (true) // ciclo infinito
{
if (this.curr.IsKeyDown(Keys.A) && this.prev.IsKeyUp(Keys.A))
{
[…]
A questo punto, leggiamo il nostro input da tastiera per verificare che sia stato premuto il tasto A
e, in caso possitivo, impostiamo il timer al momento corrente (si ricordi che time indica il tempo trascorso dall'inizio del gioco).
Quando viene premuto il tasto A
, ritorniamo lo stato della nostra combo (InputSequence.A
). Per far questo, utilizzeremo la keyword yield
, una feature introdotta a partire da .Net 2.0 per consentire la costruzione di un tipo IEnumerable
senza dover implementare la relativa interfaccia:
IEnumerable<InputSequence> AABBs()
{
while (true)
{
if (this.curr.IsKeyDown(Keys.A) && this.prev.IsKeyUp(Keys.A))
{
double t0 = time;
yield return InputSequence.A;
[…]
Aspettiamo per mezzo secondo che il tasto B
sia premuto. Per far questo, aggiungiamo un nuovo ciclo while
all'interno del principale per verificare che il bottone B
non sia stato ancora premuto e che il timer non sia scaduto (in grassetto nel listato seguente). Qualora la condizione sia soddisfatta, ritorniamo lo stato di attesa (InputSequence.WaitForB
).
IEnumerable<InputSequence> AABBs()
{
while (true)
{
if (this.curr.IsKeyDown(Keys.A) && this.prev.IsKeyUp(Keys.A))
{
double t0 = time;
yield return InputSequence.A;
while (this.curr.IsKeyUp(Keys.B) && time - t0 < 0.5)
yield return InputSequence.WaitForB;
[…]
Ripetiamo adesso per il bottone B (in grassetto). Se il bottone viene premuto, aggiorniamo nuovamente il timer (t0 = time) e ritorniamo la relativa sequenza (AB):
[…]
while (this.curr.IsKeyUp(Keys.B) && time - t0 < 0.5)
yield return InputSequence.WaitForB;
if (this.curr.IsKeyDown(Keys.B) && this.prev.IsKeyUp(Keys.B))
{
t0 = time;
yield return InputSequence.AB;
[…]
Il procedimento si può ripetere all'infinito, fino a raggiungere la sequenza desiderata.In caso che l'input nella sequenza risulti esere sbagliato o se il bottone viene premuto troppo tardi (a distanza di più di mezzo secondo dal precedente), lo stato della sequenza potrà essere resettato come segue:
yield return InputSequence.NONE;
Se invece la sequenza arriva sino in fondo, ritorneremo il relativo stato nel seguente modo:
yield return InputSequence.ABCD;
Così, supponendo di voler leggere la combo ABCD, questo è il codice completo:
IEnumerable AABBs()
{
while (true)
{
if (this.curr.IsKeyDown(Keys.A) && this.prev.IsKeyUp(Keys.A))
{
double t0 = time;
yield return InputSequence.A;
while (this.curr.IsKeyUp(Keys.B) && time - t0 < 0.5)
yield return InputSequence.WaitForB;
if (this.curr.IsKeyDown(Keys.B) && this.prev.IsKeyUp(Keys.B))
{
t0 = time;
yield return InputSequence.AB;
while (this.curr.IsKeyUp(Keys.C) && time - t0 < 0.5)
yield return InputSequence.WaitForC;
if (this.curr.IsKeyDown(Keys.C) && this.prev.IsKeyUp(Keys.C))
{
while (this.curr.IsKeyUp(Keys.D) && time - t0 < 0.5)
yield return InputSequence.WaitForD;
if (this.curr.IsKeyDown(Keys.D) && this.prev.IsKeyUp(Keys.D))
{
yield return InputSequence.ABCD;
while (this.curr.IsKeyDown(Keys.D))
yield return InputSequence.ABCD;
}
else yield return InputSequence.NONE;
}
else yield return InputSequence.NONE;
}
else yield return InputSequence.NONE;
}
else yield return InputSequence.NONE;
}
}
Ci siamo quasi. Adesso dobbiamo solo dichiarare un enumeratore che leggerà la sequenza di combo...
IEnumerator inputs;
... e inizializziarlo nel metodo Initialize
:
inputs = AABBs().GetEnumerator();
Adesso possiamo leggere lo stato corrente della nostra sequenza di input e spostarci quindi al sucessivo:
curr_input = inputs.Current;
inputs.MoveNext();
Come si vede, il codice necessario per creare il nostro sistema di gestione delle combo non è proprio immediato, e può complicarsi non poco nel caso in cui volessimo gestire sequenze di tasti più complesse e variegate. Fortunatamente, utilizzando un iterator di questo tipo, il codice per leggere lo stato della nostra combo rimarrà sempre semplice e pulito!