Monday, September 27, 2010

Czas goni nas (2)

Zgodnie z zapowiedzią czas na TimerManagera. Jak sama nazwa na to wskazuje, umożliwia on zarządzanie timerami utworzonymi w kasuroid. Główną metodą jest create():
public Timer create()
{
    Timer timer = new Timer();
    mTimers.add(timer);

    return timer;
}
Tworzy ona obiekt Timer i dodaje go do wektora mTimers (przechowuje wszystkie referencje do timerow utworzonych w TimerManagerze). Istnieje także możliwość usunięcia tak utworzonego timera poprzez zawołanie metody remove:
public int remove(int id)
{
    Timer timer = null;
    Enumeration e = mTimers.elements();
    int i = 0;
    while (e.hasMoreElements())
    {
        timer = e.nextElement();
        if (timer.getId() == id)
        {
            Debug.inf(getClass().getName(), "Timer with id " + id + " found. Removing.");
            mTimers.remove(i);
            return RetCode.SUCCESS;
        }
        i++;
    }

    return RetCode.BAD_PARAM;
}
Oczywiście nic nie stoi na przeszkodzie, aby tworzyć timery samodzielnie, bez udziału TimerManagera. Należy wtedy jednak pamiętać, aby w każdej iteracji głównej pętli Corea zawołać metodę update w celu  zaktualizowania czasu timera.
Każdy utworzony timer identyfikowany jest poprzez unikalne id. TimerManager udostępnia interfejs do sterowania timerami właśnie poprzez id. Są to (znane już metody z klasy Timer):
  • start(int id)
  • stop(int id)
  • pause(int id)
  • resume(int id)
Dodatkowo, interfejs TimerManagera został rozszerzony o dodatkowe metody do zarządzania wszystkimi timerami, a mianowicie:
  • startAll()
  • stopAll()
  • pauseAll()
  • resumeAll()
  • updateAll()
Myślę, że wszystko jest jasne. Aby zademonstrować działanie TimerManagera utworzyłem test Timers. Jest to jedno activity (TestCaseTimersActivity) z jednym stanem (TestCaseTimersState). W teście tworzone są automatycznie dwa timery (timer1 oraz timer2). Użytkownik ma możliwość sterowania timerami poprzez przyciski (mocno symboliczne - na UI przyjdzie jeszcze czas :P) umieszczone na głównym ekranie. A tak test Timers wygląda w praktyce:

Przyciski niebieskie "wołają" odpowiednio (od lewej) startAll, stopAll, pauseAll, resumeAll. Przyciski szare sterują timerem 1, natomiast przyciski różowe sterują timerem 2. W centralnej części wyświetlane są aktualne sekundy poszczególnych timerów. Polecam zassanie źródeł i sprawdzenia timerów w praktyce :)

Wednesday, September 22, 2010

Czas goni nas (1)

Załóżmy, że czas wyrenderowania ramki to 100ms na sprzęcie A. Na sprzęcie B ta sama ramka renderowana jest w ciągu 20ms. Jak łatwo policzyć na sprzęcie A będzie renderowanych 10 klatek na sekundę. Na sprzęcie B będzie to już 50 klatek na sekundę. Jeśli teraz będziemy aktualizować pozycję obiektu o 10 pikseli w każdej ramce to po sekundzie obiekt przebędzie drogę:
  • na sprzęcie A: 10 * 10 pikseli = 100 pikseli
  • na sprzęcie B: 50 * 10 pikseli = 500 pikseli
Jak widać na szybszej maszynie B obiekt przebył znacznie większą drogę niż na maszynie A. Aby wyeliminować to zjawisko animacje obiektów należy opierać właśnie o czas. Wtedy prędkość poruszania się obiektu może być wyrażona w pikselach / sekundę (a nie jak w przykładzie piksele / klatkę), zatem:
  • na sprzęcie A: 1 sekunda * 10 pikseli / sekunda = 10 pikseli / sekunda
  • na sprzęcie B: 1 sekunda * 10 pikseli / sekunda = 10 pikseli / sekunda
Każdy poważny framework (a takim przecież ma być kasuroid :P) powinien posiadać możliwość zarządzania czasem. W kasuroid za czas odpowiedzialna jest klasa Timer. Udostępnia ona podstawowy interfejs do sterowania czasem:
public class Timer
{
    public int start() {}

    public int stop() {}

    public int pause() {}

    public int resume() {}

    public int update() {}
}
Wszelkie obliczenia odbywają się w metodzie update():
public int update()
{
    if ((isStarted() == true) &&
        (isPaused() == false))
    {
        mLastFrame = mCurrentFrame;
        mCurrentFrame = System.currentTimeMillis();
        mDelta = mCurrentFrame - mLastFrame;
        mTimeElapsed += mDelta;
        mDeltaSecs = (float)mDelta / mTimerFrameLength;
    }

    return RetCode.SUCCESS;
}
Pomiar czasu opiera się na różnicy czasów dwóch kolejnych ramek, więc potrzebne jest zainicjalizowanie mLastFrame oraz mCurrentFrame podczas startowania timera:
public int start()
{
    if (isStarted() == false)
    {
        mCurrentFrame = mLastFrame = System.currentTimeMillis();
        mTimeElapsed = 0;
        mDelta = 0;
        mDeltaSecs = 0.0f;

        Debug.inf(getClass().getName(), "Timer with id: " + mId + " started.");
        return RetCode.SUCCESS;
    }
    else
    {
        Debug.warn(getClass().getName(), "Timer already started!");
        return RetCode.SUCCESS;
    }
}
Warto zauważyć, że w update() obliczam od razu liczbę sekund trwania jednej ramki. Doszedłem do wniosku, że dużo obiektów będzie bazowało swoje animacje właśnie na podstawie tej wartości. Dlatego też wystarczy policzyć ją raz per ramka. Każdy obiekt może następnie skorzystać z poniższej metody do pobrania czasu (w sekundach):
public float getTimeDeltaSecs()
{
    return mDeltaSecs;
}
.. i obliczyć na jego podstawie kolejny krok animacji.

Timer liczy także czas jaki upłynął od czasu wywołania metody start() (zmienna mTimeElapsed).

I to chyba tyle. W następnej części przedstawie TimerManagera.

Sunday, September 19, 2010

Kapryśny Android

Sprawa dotyczy zachowania się activity podczas obrotu. Aby obrócić ekran emulatora należy użyć CTRL+F12. Wspomniana kombinacja klawiszy obraca ekran o 90 stopni. Możemy w ten sposób przetestować jak zachowa się nasza aplikacja podczas obracania rzeczywistego urządzenia. Co się okazuje (ja byłem całkiem mocno zaskoczony :)) podczas takiego obrotu niszczone jest bieżące activity, a następnie tworzone nowe. Niezła niespodzianka, prawda? ;)

Problem okazał się całkiem poważny, gdyż podczas onCreate oraz onDestroy (a dokładniej podczas tworzenia / niszczenia powierzchni w KasuroidView), kasuroid jest odpowiednio inicjalizowany / kończy pracę. Okazało się, że startowanie Corea w KasuroidView nie jest dobrym pomysłem (szczerze mówiąc od samego początku takie rozwiązanie mi się nie podobało i prędzej czy później chciałem to zmienić).

Pierwszym podejściem do rozwiązania problemu było zablokowanie przejścia do trybu landscape (obrót o 90 stopni w stosunku do trybu portrait, w którym odpalany jest emulator). Aby wymusić dany tryb wyświetlania aplikacji (bez możliwości jego zmiany) należy w manifeście dla odpowiedniego activity dodać android:screenOrientation="portrait". Niestety (jak można się domyślić) nie rozwiązało to problemu w 100% :) Ekran owszem nie jest skalowany do trybu landscape przy obrocie, ale activity nadal jest niszczone. Na dodatek chciałbym, żeby kasuroid jednak wspierał tryb landscape.

Chwila szukania / czytania API i.. jest światełko w tunelu. Okazuje się, że na activity tuż przed onDestroy wołana jest metoda onSaveInstanceState. Umożliwia ona zapisanie stanu aplikacji, który następnie może zostać odtworzony w onCreate

Na teraz nie bardzo sobie wyobrażam jakie dane mógłbym zapisywać (w przypadku gry mogą to być jednak parametry leveli itd.), ale to co mogę zrobić w takiej sytuacji to zabezpieczyć Core przed restartowaniem. Warto wspomnieć, że Core jest singletonem. Co za tym idzie obiekt Corea przechowywany jest w zmiennej statycznej, a te nie są niszczone, jeśli activity jest przenoszone do hmm.. tła (minimializowanie) lub np. jak w tym przypadku, następuje obrót ekranu.

Przede wszystkim rozszerzyłem interfejs Corea o możliwość zatrzymywania i włączania głównego wątku (metody stop() oraz start()). Wątek nie robi nic poza wołaniem runSlice, więc można spokojnie nim manipulować ;). Następnie cały proces inicjalizacji / przerywania pracy / startowania Corea przeniosłem do KasuroidActivity.

A w jaki sposób sprawdzić czy activity jest jedynie maksymalizowane czy też tworzone całkiem od nowa? Hmm.. zastosowałem trochę "hardkorowe" podejście i pewnie zostanę mocno skarcony przez znawców Androida :P. Zauważyłem, że jeśli activity wraca do pierwszego planu to parametr w onCreate nie jest zerowy (lub nie jest nullem). Wystarczy zatem sprawdzić uchwyt do Bundle przekazanego jako parametr i wtedy albo uaktualniamy jedynie powierzchnię Renderera albo inicjalizujemy Corea. Następnie wystarczy już tylko wystartować wątek Corea.

W związku z powyższą zmianą logiki startu activity, do KasuroidActivity dodałem dwie metody onInit oraz onTerm. onInit wołana jest w przypadku kiedy tworzone jest nowe activity, natomiast onTerm, kiedy activity permanentnie jest niszczone.

W przypadku wspomnianego w poprzednim wpisie testu stanów gry, inicjalizacja pierwszego stanu została przeniesiona z onCreate do onInit (rozwiązuje to problem zmiany stanu, w przypadku gdy activity przeszło do tła przy aktywnym stanie drugim).

Źródła uaktualnione, więc zachęcam do podejrzenia kodu.

Btw. cykl życia androidowego activity jest dość ciekawy i bardzo możliwe, że w trakcie rozwijania projektu KasuroidActivity trochę się pozmienia :) 

Jeszcze takie spostrzeżenie. Praca nad kasuroid w całkiem dużym stopniu przypomina programowanie ekstremalne ;)

Do następnego!

Thursday, September 16, 2010

Stany (nie do końca zjednoczone)

Środowisko do testów właściwie gotowe, więc można dłubać dalej. Poniżej postaram się przybliżyć odrobinę o co chodzi z tymi stanami gry i dlaczego warto stosować takie podejście.

Bardzo popularne w grach są różnego rodzaju intra. Np. najprostsze to wyświetlenie przez jakiś czas logo producenta gry. Następnie pojawia się na ogół główne menu gry. Po wybraniu "start/nowa gra" rozpoczyna się gra. To co wymieniłem to właśnie stany gry. Czy można się bez nich obejść, tzn. bez definiowania oddzielnych klas do ich obsługi? Oczywiście, że można, ale czy nie lepiej mieć porządek w kodzie i wszytko ładnie logicznie podzielone?

Jak to wygląda w praktyce?

W kasuroid zdefiniowałem klasę bazową dla stanów gry tj. GameState (tak, State wrócił do pierwotnej nazwy). Posiada ona następujące metody:
  • init()
  • term()
  • pause()
  • resume()
  • update()
  • render()
Powyższe metody oznaczone są jako final i nie mogą być przeciążone przez klasy pochodne. Zdecydowałem się na takie podejście, aby zapewnić wewnętrzna kontrolę stanu. Np. wywołanie update, gdy obiekt nie jest zainicjalizowany wygeneruje błąd. Można jednak przeciążyć metody, które będą wołane w odpowiednich stanach tj.:
  • onInit()
  • onTerm()
  • onPause()
  • onResume()
  • onUpdate()
  • onRender()
Zarządzanie stanami gry zrealizowałem w oparciu o stos (mGameStatesStack w Core). Core posiada interfejs do zarządzania stanami, a mianowicie:
  • changeState(GameState gameState) - czyści stos, po czym wrzuca na niego gameState
  • pushState(GameState gameState) - pauzuje aktualnie znajdujący się na szczycie stosu stan i dodaje gameState
  • popState() - zdejmuje ze stosu aktualny stan i włącza (resume) stan następny
Jest to chyba najprostszy sposób realizacji zarządzania stanami gry. Można jednak przy jego pomocy realizować całkiem ciekawe przejścia w grze (co też zamierzam w odpowiednim czasie pokazać).

Ok, czas na pierwszy oficjalny test :) Dodałem nowy pakiet do projektu: com.kasuroid.test.cases. Do tego pakietu mam zamiar wrzucać wszystkie testowe activity wraz z potrzebnymi klasami. Aby mieć w miarę porządek i jako, że pojedynczy test może składać się z wielu plików, pakiet com.kasuroid.test.cases będę dzielić na drobne "podpakiety". Pierwszy podpakiet to GameStates. Stworzyłem grupę "Game states" i zdefiniowałem TestCaseGameStateActivity oraz dwa "stany gry": TestCaseGameStateState1, TestCaseGameStateState2. Aby zainicjować pierwszy stan wystarczy wywołać change state w activity:
    public void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        
        Core.getInstance().changeState(new TestCaseGameStateState1());
    } 
Gdy jesteśmy w stanie 1 (aktywny TestCaseGameStateState1) i naciśniemy dowolny klawisz to przejdziemy do stanu 2 (TestCaseGameStateState2). Jeśli zostanie naciśnięty klawisz w stanie 2 to nastąpi powrót do stanu 1. Wyjść z programu można w dowolnym momencie klikając na ekran.

Stan 1:
Stan 2:
Kliknięcie na ekran powoduje wyjście z testu:
Aby lepiej zobaczyć jak zachowują się poszczególne elementy kasuroid proponuję uruchomić projekt i podejrzeć logi (próbowałem przekleić chociaż część logów, ale poszczególne wiersze nie mieszczą się w jednej linii, a mi nie bardzo chce się ich ręcznie ciąć :P).

Dobrej nocy!

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.