Pages

Tuesday, September 14, 2010

Kasuroid tests

Dodałem do projektu możliwość odpalania testów. No, powiedzmy, że nie są to testy w pełni funkcjonalne (niestety nie mam czasu, żeby bardziej bawić się w testowanie), ze sprawdzaniem statusu/czasu wykonania itd.. Możliwe jest jednak uruchamianie pojedynczych activity (z racji tego, że większość testów i tak będę przeprowadzał wizualnie jest to całkiem dobre rozwiązanie). Na dodatek activity można pogrupować, np. podczas prac nad Rendererem wszystkie ew. test casey będą lądować do grupy Renderer.

Aby wszystko powyższe było możliwe dodałem następujące klasy:

KasuroidTestActivity - jest to w zasadzie proste UI (w oparciu o ListView) do zarządzania testami. Tworzy obiekt TestManager. W tej klasie tworzona jest również podczas inicjalizacji (przynajmniej na razie) struktura testów, czyli definiowane są grupy i przypisywane do nich odpowiednie testy. Lista grup / testów przełączana jest dynamicznie.

TestManager - w skrócie zarządza testami. Jest odpowiedzialny za uruchamianie "testów" oraz (może w przyszłości..) prowadzenie statystyk itd..

TestCaseGroup - trzyma listę testów. Każda grupa (podobnie jak obiekty klasy TestCase) identyfikowane są poprzez nazwę oraz unikalny (w skali wszystkich testów) identyfikator.

TestCase - to co najważniejsze w tej klasie to zmienna trzymająca hmm.. znacznik? klasy activity, która ma zostać uruchomiona. Początkowo TestCase dziedziczył po KasuroidActivity, jednak problemem okazało się uruchamianie takiego activity (konstruktor uruchamianego activity musi (?) być bezparametrowy, co w naszym przypadku się nie sprawdza, gdyż TestCase jako parametr przy tworzeniu bierze nazwę testu). Poza tym w TestCaseGroup przechowywane są utworzone już obiekty typu TestCase, więc tworzenie ich na nowo (tym razem przy użyciu bezparametrowego konstruktora) jest odrobinę bez sensu.

W jaki sposób zdefiniować strukturę testów? Nic prostszego:
    TestCaseGroup group = new TestCaseGroup("Group1");
    group.addTestCase(new TestCase("Test11", KasuroidActivity.class));
    group.addTestCase(new TestCase("Test12", KasuroidActivity.class));
    group.addTestCase(new TestCase("Test13", KasuroidActivity.class));
    group.addTestCase(new TestCase("Test14", KasuroidActivity.class));
    mTestManager.addTestGroup(group);

    group = new TestCaseGroup("Group2");
    group.addTestCase(new TestCase("Test21", KasuroidActivity.class));
    group.addTestCase(new TestCase("Test22", KasuroidActivity.class));
    group.addTestCase(new TestCase("Test23", KasuroidActivity.class));
    group.addTestCase(new TestCase("Test24", KasuroidActivity.class));
    group.addTestCase(new TestCase("Test25", KasuroidActivity.class));
    group.addTestCase(new TestCase("Test26", KasuroidActivity.class));
    mTestManager.addTestGroup(group);
Jak widać powyżej utworzone zostały dwie grupy testów: Group1 oraz Group2. Do każdej z grup przynależy również kilka testów. Każdy test odpala KasuroidActivity (na dzień dzisiejszy nie ma za wiele innych klas testowych :) ). Oczywiście powyższa struktura testów jest tylko testem.

I tak po odpaleniu KasuroidTestActivity ukazuje się lista grup testów:
Po wybraniu "Group1" wyświetlona zostaje lista testów przypisanych do "Group1":
Aby przejść do widoku grup testów należy wcisnąć "Back button". Po wybraniu testu z listy zostanie uruchomione przypisane activity.

Jeszcze może kilka słów na temat czemu takie akurat rozwiązanie. Głównie dlatego, żeby móc w prosty sposób śledzić postęp prac nad kasuroid oraz z.. lenistwa. To czego najbardziej nie lubię to tworzenia nowych projektów i ustawiania wszystkich ścieżek itd.. (a tak musiałbym robić jeśli chciałbym mieć sprawdzane kolejne funkcjonalności frameworka). W powyższym rozwiązaniu jedyne co będę musiał zrobić to utworzyć (poprzez skopiowanie już istniejącego) nowe activity (swoją drogą dziedziczące po KasuroidActivity) i dodać do odpowiedniej grupy testów. Dodatkową zaletą takiego rozwiązania jest fakt, że podczas odpalania activity inicjalizowany jest cały kasuroid framework. Symulowany jest więc normalny cykl życia przyszłych gier w oparciu o kasuroid

Wszystko w jednym miejscu, w jednym projekcie. Cudnie!

Sunday, September 12, 2010

quit!

Jak to często bywa koncepcja klaruje się w trakcie samej pracy (w tym przypadku nie jest inaczej :P). W trakcie prac nad małym środowiskiem testowym okazało się, że nie ma opcji wyjścia z aplikacji :) Głównie dlatego, że beż czarowania nie ma sposobu na zatrzymanie wątku Core przy użyciu samego Core. Jako, że całe sterowanie ma się odbywać właśnie przez Core musi być to możliwe. Dlatego też, tak jak przewidywałem we wcześniejszym wpisie, KasuroidThread staje się wewnętrzna klasą Core. Jest to o tyle wygodne, że teraz mamy pełną kontrolę nad sposobem odpalania Core. Na dodatek zmniejsza się liczba klas wejściowych do frameworka (btw. w planie jest aby jedynym punktem wejścia do kasuroid była klasa KasuroidActivity). Interfejs Core został rozszerzony o metody start() oraz stop() oraz dodałem kontrolę stanów, w których Core może się znajdować.

Aby zakończyć pracę frameworka należy wywołać metodę quit():
    public void quit()
    {
        Activity act = (Activity)mContext;
        act.finish();
    }
mContext wskazuje na activity, w którym utworzony jest Core. Jako, że w Androidzie aplikacja jest odpalana poprzez Activity, zamknięcie samej aplikacji musi się odbyć również poprzez zamknięcie Activity. Można to zrobić np. poprzez zawołanie metody finish() na danym activity. finish() zamyka kolejne widoki i (pośrednio) w KasuroidView również Core. Poniżej znajduje się fragment LogCat z inicjalizacji oraz kończenia aplikacji:

Starting activity: Intent { cmp=com.kasuroid/.KasuroidActivity } 
com.kasuroid.core.Core:Core()
com.kasuroid.core.Core:init()
com.kasuroid.core.TimerManager:TimerManager initialized
com.kasuroid.core.SceneManager:SceneManager initialized
com.kasuroid.core.ResourceManager:ResourceManager initialized
com.kasuroid.core.Renderer:Renderer initialized
com.kasuroid.KasuroidActivity:Debug output
KasuroidView:surfaceCreated
com.kasuroid.core.Core:start()
Displayed activity com.kasuroid/.KasuroidActivity: 279 ms (total 279 ms)
com.kasuroid.core.Core:Touch event, going to close app!
com.kasuroid.core.Core:stop()
com.kasuroid.core.Core:term()
com.kasuroid.core.TimerManager:TimerManager terminated
com.kasuroid.core.SceneManager:SceneManager terminated
com.kasuroid.core.ResourceManager:ResourceManager terminated
com.kasuroid.core.Renderer:Renderer terminated
com.kasuroid.KasuroidActivity:onDestroy

Jak widać wszystkie moduły są już ładowane oraz zwalniane. Aha, com.android.kasuroid ewoluował (nie bez bólu..) do com.kasuroid. Zmieniłem pakiet główny ponieważ kasuroid jako taki nie jest częścią pakietu android a całkiem oddzielnym komponentem. 
Ponieważ Core odpalany jest w oddzielnym wątku, cały dostęp (a przynajmniej do najważniejszych składowych) musi być synchronizowany (zmienna mLock w Core). 
Okazało się także, że CoreThread potrafi wywłaszczyć wszystkie zasoby i nie jest możliwa komunikacja z użytkownikiem / systemem. Aby rozwiązać ten problem musiałem uśpić na chwilę wątek i oddać trochę CPU systemowi (dodanie sleep w run() wątku). Dodatkową zaletą uśpienia wątku (oprócz możliwości komunikacji z systemem i otrzymywania różnych eventów) jest oszczędzanie baterii - CPU nie działa wtedy na 100%.

W następnym wpisie przedstawię więcej szczegółów na temat testowania frameworka.

Wednesday, September 8, 2010

What a terrible failure

Android SDK dostarcza możliwość logowania różnych sytuacji zaistniałych w kodzie za pomocą klasy Log. Zrobiłem małego wrapera i w kasuroid logowanie jest możliwe za pomocą klasy Debug. A dlaczego tak? A po to, żeby za każdym razem nie wpisywać chociażby "taga" oraz mieć większą kontrolę nad formatem wypluwanych danych (i np. w prosty sposób zrobić logowanie do pliku, na serwer czy gdzie tam komu się podoba). Domyślnie logi można podejrzeć w LogCat:


Btw. klasa "Log" jest przykładem na to, jakim poczuciem humoru obdarzeni są programiści gugla :) Jedną z metod tej klasy jest... "wtf" :) Nie, nie. Nie jest to "What The Fuck?!", ale prawie równie wymowne "What a Terrible Failure" :)

Z pozostałych rzeczy.. Główne activity zmieniło nazwę na KasuroidActivity. KasuroidThread został również rozbudowany. Jest to klasa wątku, w którym odpalany jest core frameworka. Możliwe, że później KasuroidThread zostanie wchłonięty przez Core, ale na razie zostawiam to tak jak jest.

Aby wystartować framework należy w zasadzie utworzyć obiekt klasy KasuroidThread i wywołać metode proceed. Metoda ta inicjalizuje (poprzez Core) wszystkie moduły:
  1 public int proceed()
  2 {
  3     int ret = RetCode.SUCCESS;
  4         
  5     ret = Core.getInstance().init(surface, context);
  6     if (ret != RetCode.SUCCESS)
  7     {
  8         Debug.err(this.getClass().getName(), "Core not initialized!");
  9         return ret;
 10     }
 11 
 12     setRunning(true);
 13     start();
 14 
 15     return RetCode.SUCCESS;
 16 }
Aby zakończyć pracę należy wywołać metodę shutDown:
  1 public int shutDown()
  2 {
  3     // Shut down the core thread.
  4     boolean retry = true;
  5     setRunning(false);
  6     while (retry) 
  7     {
  8         try 
  9         {
 10             this.join();
 11             retry = false;
 12         } 
 13         catch (InterruptedException e) 
 14         {
 15             Debug.err(this.getClass().getName(), e.toString());
 16             return RetCode.FAILURE;
 17         }
 18     }
 19 
 20     int ret = Core.getInstance().term();
 21     if (ret != RetCode.SUCCESS)
 22     {
 23         Debug.err(this.getClass().getName(), "Core wasn't properly terminated!");
 24     }
 25 
 26     return ret;
 27 }
Główną pętlę całego frameworka zawiera metoda run wątku:
  1 public void run() 
  2 {
  3     while (isRunning) 
  4     {
  5         Core.getInstance().runSlice();
  6     }
  7 }

Jak widać nie robi ona praktycznie nic oprócz wołania metody runSlice z Corea. To właśnie w runSlice będą się dziać wszystkie magiczne aktualizacje sceny, fizyki i rysowanie po ekranie.

Sunday, September 5, 2010

KasuroidView

Czas na zdefiniowanie widoku. Do com.android.kasuroid wrzuciłem KasuroidView. Definicja nowego widoku znajduje się w /layout/kasuroid_layout.xml:

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent">
    <com.android.kasuroid.KasuroidView
        android:id="@+id/id_kasuroid_view"
        android:orientation="vertical"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"/>
</FrameLayout>

LinearLayout zmieniłem na rzecz FrameLayout, gdyż umożliwia on nakładanie na siebie kolejnych widoków (co też ładnie zostało przedstawione tutaj). Może to być użyteczne np. w przypadku, gdy chcemy wyświetlić jakąś dodatkową informację bezpośrednio na ekran. Warto wspomnieć o android:id. Jest to identyfikator danego elementu, w tym przypadku "id_kasuroid_view". KasuroidView dodawany jest do głównego activity poprzez:
  • setContentView(R.layout.kasuroid_layout);
, gdzie "R" jest to automatycznie generowana z aktualnych zasobów klasa ze zdefiniowanymi identyfikatorami. Należy pamiętać, żeby użyć poprawnej klasy identyfikatorów ("R"). A mianowicie:
  • import com.android.kasuroid.R;
Aplikacje mają być pełnoekranowe. W celu ukrycia paska tytułu wystarczy zawołać:
  • requestWindowFeature(Window.FEATURE_NO_TITLE);
Jako, że zdecydowałem się na użycie oddzielnego wątku do rysowania, wymagane jest dziedziczenie po SurfaceView, co z kolei prowadzi do konieczności zaimplementowania kilku funkcji:
  • surfaceChanged
  • surfaceCreated
  • surfaceDestroyed
Wynikiem powyższego jest... pusty, czarny ekran ;):

Pierwsze źródła

Uff.. po dłuższej przerwie (i zasłużonych wakacjach :P) czas zacząć dłubać coś bardziej konkretnego.

Zgodnie z tym co zostało "powiedziane" w poprzednim poście pierwsze źródła wrzuciłem do repozytorium. Do SVNa używam TortoiseSVN, który to bardzo ładnie integruje się z powłoką Windowsa. Strukturę projektu (która zapewne jeszcze się zmieni) każdy może sobie podejrzeć, wiec przedstawię jedynie to co najważniejsze. W projekcie wydzieliłem dwa pakiety: com.android.kasuroid oraz com.android.kasuroid.core. Taki układ wydaje mi się (przynajmniej na razie) najbardziej sensowny. Do *.core będzie leciało wszystko co jest generyczne dla frameworka.

com.android.kasuroid
  •  Kasuroid.java

com.android.kasuroid.core
  • Core.java
  • Module.java
  • Renderer.java
  • ResourceManager.java
  • RetCode.java
  • Scene.java
  • SceneManager.java
  • SoundManager.java
  • State.java
  • Timer.java
  • TimerManager.java

Póki co nie ma sensu wydzielać oddzielnych podkatalogów na poszczególne moduły. Na tym etapie nie przewiduję, że framework będzie kompilowany do biblioteki (aczkolwiek byłoby to bardzo wygodne rozwiązanie w przypadku gotowego API silnika - co prędko jednak się nie zdarzy :P). Każda gra/aplikacja, chcąca korzystać z kasuroid będzie musiała dodać jego źródła do swojego projektu. Jest to chyba najprostsze z możliwych rozwiązań. Wadą jest jednak to, że trzeba się mocno pilnować, żeby w trakcie rozwijania gry nie namieszać w źródłach samego frameworka. W momencie gdy z frameworka nie korzysta żadna aplikacja nie ma to jednak większego znaczenia :)

Aha, GameCore oraz GameState przechrzciłem na Core oraz State. Dodałem również interfejs Module oraz definicje kodów powrotu metod.

Module.java
public interface Module {

    int init();

    int term();

    String getName();
}

Jestem zwolennikiem jawnego inicjalizowania oraz hmm.. kończenia pracy poszczególnych części kodu (w szczególności modułów). Albo zwyczajnie mam stare przyzwyczajenia z C/C++ ;P

RetCode.java
public class RetCode
{
    public static final int SUCCESS = 0;

    public static final int FAILURE = 1;

    public static final int BAD_PARAM = 2;
}

Czyli raczej standard. Nie jest to ostateczna liczba kodów i będzie ona rozszerzana w miarę potrzeb.