View Full Version : [Ciclo 6] Pair Programming task 6.2.3 (fek vs cionci clash of the titans)
Implementare le animazioni in sprite cambiando frame ogni decimo di secondo. Al termine del'animazione si deve mostrare il frame di base per 3500 millisecondi prima di ripartire.
http://lnx.rc6.it/diamonds/varie/topaz.png
Il task parla di scrivere in Sprite, ma guardando il codice ci pare piu' semplice scrivere in Gem, quindi proseguiamo per questa strada.
Test list:
- testare che una gemma appena creata abbia solo un frame di animazione
- testare che all'aggiunta di un frame di animazione, la gemma ne abbia due
- testare che all'update della gemma, cicli dal primo al secondo frame di animazione
- testare che ogni frame abbia un tempo di durata
- se la gemma si trova al frame 0 deve passare al frame 1 dopo 3500 ms
- se la gemma si trova al frame X con X > 0 deve passare al frame X + 1 dopo 100 ms
- se la gemma si trova al frame N-1 deve ritornare al frame 0 dopo 100 ms
Per ora iniziamo con questi, ce ne verranno certamente in mente altri.
Primo test: una gemma appena creata ha un frame di animazione.
package it.diamonds.tests;
import it.diamonds.Gem;
import junit.framework.TestCase;
public class TestGemAnimation extends TestCase
{
public void TestNewGemHasOneFrame()
{
Gem gem = Gem.createForTesting();
assertEquals("A new gem must have only one frames", 1, gem.getNumberOfFrames());
}
}
A te :)
(Codice minimo indispensabile per far passare il test)
Aggiunto questo codice a Gem:
public int getNumberOfFrames()
{
return 1;
}
Nuovo test: aggiunta di un frame a Gem
public void testAddingOneFrame()
{
Gem gem = Gem.createForTesting();
gem.addFrame(new Rectangle(10, 10, 20, 20));
assertEquals("gem must have two frames", 2, gem.getNumberOfFrames());
}
Dato che la dimensione dello sprite e' fissa, fornire quest'informazione anche in addFrame mi sembra una duplicazione (non supportiamo scalamenti per ora).
Modificherei il test cosi':
public void testAddingOneFrame()
{
Gem gem = Gem.createForTesting();
gem.addFrame(10, 10);
assertEquals("gem must have two frames", 2, gem.getNumberOfFrames());
}
Faccio compilare il test cosi':
public void addFrame(int x, int y)
{
// TODO Auto-generated method stub
}
Notare come mi e' bastasto cliccare sull'errore e Eclipse ha aggiunto il metodo corretto per me.
Lancio i test, compilano e il test fallisce:
junit.framework.AssertionFailedError: gem must have two frames expected:<2> but was:<1>
Il test si aspetta 2 ma riceve 1. Modifico il codice per far passare il test.
private int numberOfFrames;
public int getNumberOfFrames()
{
return numberOfFrames;
}
public void addFrame(int x, int y)
{
numberOfFrames = 2;
}
Lancio i test e.... fallisce il primo test:
junit.framework.AssertionFailedError: A new gem must have no frames expected:<1> but was:<0>
Non sto barando, e' andata proprio cosi', ho dimenticato di inizializzare il campo numberOfFrames :)
Per fortuna il primo test era li', pronto a ricordarmelo in caso di errore. Sto scrivendo mentre programmo.
Correggo:
private int numberOfFrames = 1;
Lancio i test e va tutto bene.
Ora proseguo per triangolazione con un nuovo test:
public void testAddingTwoFrames()
{
Gem gem = Gem.createForTesting();
gem.addFrame(10, 10);
gem.addFrame(10, 20);
assertEquals("gem must have three frames", 3, gem.getNumberOfFrames());
}
Il test ovviamente fallisce.
junit.framework.AssertionFailedError: gem must have three frames expected:<3> but was:<2>
Cionci a te.
Test passato modificando questo metodo:
public void addFrame(int x, int y)
{
++numberOfFrames;
}
Vado a scrivere il prossimo test...
Il frame corrente deve essere 0 appena viene creata la gemma...
public void testCurrentFrame()
{
Gem gem = Gem.createForTesting();
assertEquals("current frame must be 0", 0, gem.getCurrentFrame());
}
Ecco il metodo che fa passare il test.
public int getCurrentFrame()
{
return 0;
}
Ttestare che all'update della gemma, cicli dal primo al secondo frame di animazione.
public void testUpdate()
{
Gem gem = Gem.createForTesting();
gem.addFrame(10, 10);
gem.update();
assertEquals("current frame must be 1 after an update", 1, gem.getCurrentFrame());
}
Notare come questi due test impongano la convenzione che il primo frame dell'animazione e' il frame 0 e presumibilmente si ciclera' dal frame 0 al frame N-1.
Cionci a te.
Ho aggiunto:
private int currentFrame = 0;
public int getCurrentFrame()
{
return currentFrame;
}
public void update()
{
currentFrame = 1;
}
Il prossimo test ci impone di spostarci di un frame per ogni update che viene fatto...
public void testUpdateWithThreeFrames()
{
Gem gem = Gem.createForTesting();
gem.addFrame(10, 10);
gem.addFrame(20, 20);
gem.update();
gem.update();
assertEquals("current frame must be 2 after two update", 2, gem.getCurrentFrame());
}
Una sola riga da cambiare:
public void update()
{
++currentFrame;
}
Beck sarebbe fiero di noi, forma pura di TDD :)
Test: testare che ogni frame abbia un tempo di durata (in millisecondi)
public void testFrameLengt()
{
gem.addFrame(10, 10, 1000);
assertEquals("frame length is wrong", 1000, gem.getFrameLength(0));
}
Ho spostato la creazione della gemma nel metodo setUp() eliminando la duplicazione e poi ho aggiunto le specifiche per un parametro in piu' al metodo addFrame(). Ora, cionci puo' modificare tutti gli altri test per aggiungere un parametro oppure aggiungere un metodo addFrame() con due parametri che delega al metodo addFrame() con tre parametri passando un valore di default. A lui la scelta.
beh almeno io vi seguo passo passo se può consolarvi :p
beh almeno io vi seguo passo passo se può consolarvi :p
Non sei il solo :Prrr:
Azz... la nostra copertura è saltata...
Aggiunto il codice che fa passare il test:
public void addFrame(int x, int y, int delay)
{
++numberOfFrames;
}
public int getFrameLength(int index)
{
return 1000;
}
Passo a scrivere il prossimo test...
Un saluto a tutti :)
Ovviamente ho corretto tutti gli altri test per compilare senza problemi...
Nuovo test e la situazione si fa interessante:
public void testTwoFrameLengths()
{
gem.addFrame(10, 10, 1000);
gem.addFrame(10, 10, 100);
assertEquals("frame one length is wrong", 1000, gem.getFrameLength(0));
assertEquals("frame two length is wrong", 100, gem.getFrameLength(1));
}
Nuovo test e la situazione si fa interessante:
Si', si fa interessante perche' non saprei come farlo passare con un solo cambiamento semplice. Allora lo tengo un attimo da parte e preparo un altro test che mi porti un po' piu' vicino alla soluzione.
Ecco il test:
package it.diamonds.tests;
import it.diamonds.Frame;
import junit.framework.TestCase;
public class TestFrame extends TestCase
{
public void testLength()
{
Frame frame = new Frame(10, 10, 1000);
assertEquals("Frame length must be 1000", 1000, frame.getLength());
}
}
Ecco il codice che lo fa passare:
package it.diamonds;
public class Frame
{
public Frame(int x, int y, int length)
{
}
public int getLength()
{
return 1000;
}
}
[code]
Vado avanti per triangolazione:
[code]
public void testLength()
{
Frame frame1 = new Frame(10, 10, 1000);
assertEquals("Frame length must be 1000", 1000, frame1.getLength());
Frame frame2 = new Frame(10, 10, 2000);
assertEquals("Frame length must be 2000", 2000, frame2.getLength());
}
Il test fallisce.
Ed ecco il codice che lo fa passare:
public class Frame
{
int length;
public Frame(int x, int y, int length)
{
this.length = length;
}
public int getLength()
{
return length;
}
}
Ora sono un passettino piu' vicino alla soluzione. Ho una classe Frame e posso modificare Gem per usare un'array di questi Frame ed eliminare un po' di campi inutili, semplificando il codice.
E' il momento di fare un po' di refactoring: se sbaglio qualcosa ho i test che mi fermeranno.
Torno a Gem. Aggiungo l'array di Frame.
private ArrayList<Frame> frames;
Modifico il metodo per aggiungere un frame di animazione:
public void addFrame(int x, int y, int delay)
{
frames.add(new Frame(x, y, delay));
++numberOfFrames;
}
Lancio i test.... NullPointerException... touche', ha ragione lui. Ecco il codice per risolvere il problema:
private ArrayList<Frame> frames = new ArrayList<Frame>();
Barra verde ora.
Prossimo passo, proviamo a togliere numberOfFrames:
public int getNumberOfFrames()
{
return frames.size();
}
Lancio i test. Falliti. Non e' giornata :D
Vediamo che dice JUnit:
junit.framework.AssertionFailedError: A new gem must have one frame
expected:<1> but was:<0>
Ovvio, una nuova gemma deve avere un frame di animazione. Io me ne ero dimenticato, ma il test no.
Vado nel costruttore e aggiungo questo:
addFrame(0, 0, 0);
Barra verde, posso eliminare numberOfFrames.
public void addFrame(int x, int y, int delay)
{
frames.add(new Frame(x, y, delay));
}
Ed ora aggiungo il test che avevo momentaneamente cancellato. E poi cambiando una sola riga di codice lo faccio passare:
public int getFrameLength(int index)
{
return frames.get(index).getLength();
}
Lancio i test e... falliti. Ma allora e' un vizio oggi.
junit.framework.AssertionFailedError: frame length is wrong expected:<1000> but was:<0>
Hmmm... il test si aspettava 1000 ma gli e' arrivato 0, pero' sono sicuro che se passo una durata alla classe Frame questa mi torna la durata giusta (quel test e' passato)... Fammi guardare il test:
public void testFrameLength()
{
gem.addFrame(10, 10, 1000);
assertEquals("frame length is wrong", 1000, gem.getFrameLength(0));
}
Chiede che il frame 0 abbia lunghezza 1000, ma, ovvio, il frame che abbiamo aggiunto non e' quello a indice 0, ma e' quello a indice 1, perche' una gemma appena creata ha gia' un frame di animazione. Il test era sbagliato!
Ora lo cambio:
public void testFrameLength()
{
gem.addFrame(10, 10, 1000);
assertEquals("frame length is wrong", 1000, gem.getFrameLength(1));
}
Verde! E passa anche il test di cionci.
Notate come ho fatto un sacco di errori mentre programmavo (sara' la fame :p), ma non ho mai usato il debugger. C'era sempre un test pronto a dirmi esattamente che errore ho fatto e come correggerlo. E anche durante il refactoring non ho mai modificato piu' di una riga di codice senza lanciare i test.
Ci aggiorniamo a dopo pranzo.
Test: se la gemma si trova al frame 0 deve passare al frame 1 dopo 3500 ms
public void testUpdateWithTimer()
{
gem.addFrame(10, 10, 1000);
MockTimer timer = new MockTimer(0);
gem.update(timer);
assertEquals("current frame must be 0 after 0 milliseconds", 0, gem.getCurrentFrame());
timer.advance(3500);
gem.update(timer);
assertEquals("current frame must be 1 after 3500 milliseconds", 1, gem.getCurrentFrame());
}
commento temporaneamente gli altri due test che riguardano update...
Inserisco questo codice in update di Gem:
public void update(AbstractTimer timer)
{
if(timer.getTime() >= 3500)
++currentFrame;
}
il test di fek passa...
Ora però devo fare in modo di far passare gli altri test:
public void testUpdate()
{
gem.addFrame(10, 10, 100);
gem.update(new MockTimer(3500));
assertEquals("current frame must be 1 after an update", 1, gem.getCurrentFrame());
}
questo passa senza modifche al codice...
Ora passiamo al secondo:
public void testUpdateWithThreeFrames()
{
gem.addFrame(10, 10, 100);
gem.addFrame(20, 20, 100);
MockTimer timer = new MockTimer(3500);
gem.update(timer);
timer.advance(1);
gem.update(timer);
assertEquals("current frame must be 1", 1, gem.getCurrentFrame());
timer.advance(99);
gem.update(timer);
assertEquals("current frame must be 2", 2, gem.getCurrentFrame());
}
Il test modifcato in questo modo non passa...
Devo lavorare sul mio codice che quindi non va bene...
Allora modifico addFrame(0, 0 ,0) nel costruttore in addFrame(0, 0, 3500)...
Aggiungo a gem:
private long frameChangeTimeStamp = 0;
Modifico il codice di update in questo modo:
public void update(AbstractTimer timer)
{
long timeStamp = timer.getTime();
if(timeStamp - frameChangeTimeStamp >= frames.get(getCurrentFrame()).getLength())
{
++currentFrame;
frameChangeTimeStamp = timeStamp;
}
}
il test passa...
Se è tutto ok vado a scrivere il prossimo test...
Il nuovo test è questo:
public void testOneUpdateForMultipleFrames()
{
gem.addFrame(10, 10, 100);
gem.addFrame(20, 20, 100);
MockTimer timer = new MockTimer(3600);
gem.update(timer);
assertEquals("current frame must be 2 after one update", 2, gem.getCurrentFrame());
}
Se passa un tempo maggiore di quello necessario al cambio di un solo frame update dovrebbe portare l'animazione comunque al frame corretto...
Ed ecco il codice che fa passare il test:
public void update(AbstractTimer timer)
{
long deltaTime = 0;
deltaTime = timer.getTime() - lastTime;
currentTime += deltaTime;
int frame = 0;
int frameTime = frames.get(frame).getLength();
while (currentTime >= frameTime)
{
frameTime += frames.get(++frame).getLength();
}
lastTime = timer.getTime();
currentFrame = frame;
}
Non e' il codice migliore del mondo, ma fa il suo dovere. Un momento, ma perche' non ho scritto questo codice passo per passo, riga per riga? Perche' sono stato pigro, avevo gia' scritto esattamente questo algoritmino in passato e mi sentivo sicuro del successo. Infatti... non vi sto a riportare i penosi tentativi casuali per far passare tutti i test e le decine di minuti persi. Tutto perche' avevo dimenticato il += all'interno del loop. Eppure ero sicuro che fosse giusto.
Ho imparato la lezione, la prossima volta mi ricordero' di farlo passo passo anche se sono sicurissimo di quello che sto facendo, impieghero' certamente meno tempo di quanto ne ho impiegato questa volta, cercando di fare tutto in un passo.
Test: se la gemma si trova al frame N-1 deve ritornare al frame 0 dopo 100 ms
public void testCyclingAnimation()
{
gem.addFrame(10, 10, 1000);
MockTimer timer = new MockTimer(0);
gem.update(timer);
timer.advance(3500 + 1000);
gem.update(timer);
assertEquals("current frame must be 0 after a complete cycle", 0, gem.getCurrentFrame());
}
Ci sono due frame nell'animazione, dopo 4500 millisecondi il frame corrente dev'essere di nuovo quello iniziale. Fallisce con un out of bound exception, come era lecito attendersi.
A te. Non fare il mio stesso errore, fallo passo per passo :)
Non c'è stato bisogno di andare passo-passo perchè ci voleva un solo passo:
public void update(AbstractTimer timer)
{
long deltaTime = 0;
deltaTime = timer.getTime() - lastTime;
currentTime += deltaTime;
int frame = 0;
int frameTime = frames.get(frame).getLength();
while (currentTime >= frameTime)
{
if(frame == frames.size() - 1)
frame = -1;
frameTime += frames.get(++frame).getLength();
}
lastTime = timer.getTime();
currentFrame = frame;
}
Certo, il codice può essere rimesso in ordine, ma per ora è sufficiente :)
Passo a scrviere il test per la draw...
public void testDrawTwoFrames()
{
gem.addFrame(10, 15, 100);
gem.addFrame(20, 25, 100);
MockEngine engine = MockEngine.createForTesting(800, 600);
MockTimer timer = new MockTimer(3500);
gem.update(timer);
gem.draw(engine);
assertEquals("bad texture drawn", 10.0f, engine.getTextureRect().left(), 0.001);
assertEquals("bad texture drawn", 15.0f, engine.getTextureRect().top(), 0.001);
timer.advance(100);
gem.update(timer);
gem.draw(engine);
assertEquals("bad texture drawn", 20.0f, engine.getTextureRect().left(), 0.001);
assertEquals("bad texture drawn", 25.0f, engine.getTextureRect().top(), 0.001);
}
Porto avanti io il test...
Ora bisogna modificare la classe Frame per memorizzare e poter recuperare i valori delle coordinate...
Per fare questo scrivo questo test:
public void testGetCoordinates()
{
Frame frame1 = new Frame(10, 20, 1000);
assertEquals("erroneous value from getX", 10, frame1.getX());
assertEquals("erroneous value from getY", 20, frame1.getY());
Frame frame2 = new Frame(15, 5, 2000);
assertEquals("erroneous value from getX", 15, frame2.getX());
assertEquals("erroneous value from getX", 5, frame2.getY());
}
Il codice che soddisfa questo test è questo:
public class Frame
{
private int length;
private int x;
private int y;
public Frame(int x, int y, int length)
{
this.x = x;
this.y = y;
this.length = length;
}
public int getLength()
{
return length;
}
public int getX()
{
return x;
}
public int getY()
{
return y;
}
}
Per fare questo mi basta aggiungere una sola riga in update:
public void update(AbstractTimer timer)
{
long deltaTime = 0;
deltaTime = timer.getTime() - lastTime;
currentTime += deltaTime;
int frame = 0;
int frameTime = frames.get(frame).getLength();
while (currentTime >= frameTime)
{
if(frame == frames.size() - 1)
frame = -1;
frameTime += frames.get(++frame).getLength();
}
lastTime = timer.getTime();
currentFrame = frame;
setTextureArea(frames.get(frame).getX(), frames.get(frame).getY());
}
Il task non è ancora finito, dobbiamo ancora gestire le animazioni della gemma illuminata...ma non so se vi siete resi conto, ma abbiamo implicitamente risolto anche il task 6.2.4 !!!
si perchè usi un timer unico... mi accontenterò di fare un po' di refactoring :D
C'è ancora da includere l'update delle gemme in Grid ed inizializzare le nuove gemme con i giusti frame in GemFactory...
Credo che tu possa partere anche ora... Intanto preparo il test per le gemme illuminate per fek !!!
inizializzare le nuove gemme con i giusti frame in GemFactory
Anzi, mi sa che questa cosa la dovremmo fare noi...
Il nuovo test per fek:
public void testBrightenedFrames()
{
gem.addFrame(10, 15, 100);
gem.addFrame(20, 25, 100);
gem.useBrighterImage();
MockEngine engine = MockEngine.createForTesting(800, 600);
MockTimer timer = new MockTimer(3500);
gem.update(timer);
gem.draw(engine);
assertEquals("bad texture drawn", 42, engine.getTextureRect().left());
assertEquals("bad texture drawn", 47, engine.getTextureRect().top());
gem.useNormalImage();
timer.advance(100);
gem.update(timer);
gem.draw(engine);
assertEquals("bad texture drawn", 20, engine.getTextureRect().left());
assertEquals("bad texture drawn", 25, engine.getTextureRect().top());
}
Una sola modifica in update():
setTextureArea(
frames.get(frame).getX() + (this.bright ? 32 : 0),
frames.get(frame).getY());
Poi ho dovuto modificare il test perche' la gemma illuminata e' quella a destra di 32 pixel, non a destra e in basso:
assertEquals("bad texture drawn", 42, engine.getTextureRect().left());
assertEquals("bad texture drawn", 15, engine.getTextureRect().top());
Ora non mi resta che aggiungere una chiamata in Grid per fare l'update delle gemme e aggiungere i frame di animazione coretti alla creazione delle gemme stesse.
Il test ce l'hai...telefono alla mia ragazze e dopo apro MSN...
Ok...giusto... Avevo sbagliato il test...
Lo stai facendo te o vuoi i test ?
Ecco il test che controlla che le gemme siano correttamente animate da Grid:
public void testGridUpdatesAnimations()
{
Grid grid = Grid.createForTesting(new MockGemGenerator());
grid.insertGem(1, 1, gem);
gem.addFrame(10, 15, 100);
MockTimer timer = new MockTimer(3500);
grid.update(timer);
assertEquals("Gem animation hasn't been updated", 1, gem.getCurrentFrame());
}
Ed il codice che lo fa passare e' relativamente banale, un semplice ciclo for in Grid.update(). Da notare che update adesso accetta un timer, come logico:
public void update(AbstractTimer timer)
{
for(int y = grid.length - 1; y >= 0; y--)
{
for(int x = 0; x < grid[y].length; x++)
{
Gem gem = getGemAt(y, x);
if (gem != null)
{
gem.update(timer);
}
}
}
[...]
}
Ed ora un test per creare l'intera sequena di animazione di una gemma e ne testa la temporizzazione:
public void testCreateAnimationSequence()
{
gem.createAnimationSequence();
MockEngine engine = MockEngine.createForTesting(800, 600);
MockTimer timer = new MockTimer(3450 + 100 * 5);
gem.update(timer);
gem.draw(engine);
assertEquals("Last frame of the sequence is wrong", 32 * 5, engine.getTextureRect().top());
}
Ed il semplice codice che crea la sequenza:
public void createAnimationSequence()
{
for (int i = 1; i <= 5; ++i)
{
addFrame(0, 32 * i, 100);
}
}
Ora non resta che provare il gioco e le gemme dovrebbero animarsi! Urrah!
le gemme nel next non vanno animate per ora?
le gemme nel next non vanno animate per ora?
La Storia non le menziona e noi non le tocchiamo.
Ora e' il momento delle tagliatelle ai funghi per me, mi trasferisco in cucina :p
vBulletin® v3.6.4, Copyright ©2000-2025, Jelsoft Enterprises Ltd.