Archive for Programming

Składanie frameworka

2007-09-12 8:57

Kończąc powoli prace nad systemem GUI (a przynajmniej jakimiś sensownymi podstawami tego systemu), zakańczam jednocześnie prace nad “płaską” częścią silnika. Innymi słowy, już wkrótce nie będzie żadnej wymówki i trzeba będzie zabrać się za dodanie upragnionego, a zarazem niezwykle komplikującego życie trzeciego wymiaru :)

Pomyślałem jednak, że najpierw dobrze byłoby poskładać napisane już w cegiełki w sensowną całość i stworzyć coś w rodzaju frameworka. Chodzi tutaj o tę warstwę pośrednią między kodem silnika a użytkownikiem i systemem, czyli szkielet umożliwiający wygodne tworzenie rzeczywistych aplikacji.
W wielu bibliotekach różnie to rozwiązano. Z jednego strony DirectX czy OpenGL zostawiają to całkowicie w gestii programisty. Musi on samodzielnie przygotować chociażby to okienko, w którym będzie się odbywało rysowanie. Z kolei np. SDL bardzo głęboko ingeruje w kod programu, narzucając nawet określoną formę funkcji main.

Najlepsze jest oczywiście takie rozwiązanie, które zapewnia zarówno dużą elastyczność, jak i nie zmusza do napisania kilkuset linijek w celu zobaczenia czegokolwiek. Wydaje mi się, że bliski ideałowi jest pomysł zamknięcia funkcjonalności frameworka w klasę w rodzaju Application, zawierająca metody pozwalające na inicjalizację programu i późniejsze sterowanie nim. Metody tej klasy byłyby wywoływane w funkcji startowej programu, czyli main lub WinMain. Tak to wygląda na przykład w Windows Forms czy w VCL (Delphi):
[delphi]program SomeProject;

uses
Forms,
MainFrm in ‘MainFrm.pas’ {Form1};

{$R *.res}

begin
Application.Initialize;
Application.CreateForm(TMainForm, MainForm);
Application.Run;
end.[/delphi]
Według mnie najlepiej jest, gdy obiekt głównej klasy programu jest albo statycznie tworzonym singletonem, albo zostaje wykreowany przez programistę przy rozruchu aplikacji. Najważniejsze, aby nie zmuszać do dziedziczenia po klasie Application – po to na przykład, by nadpisując metody wirtualne zapewnić możliwość reakcji na zdarzenia (jak wciśnięcia klawiszy czy ruch myszy). Dzięki delegatom, choćby tym z biblioteki FastDelegate, można to zrobić dokładnie tak, jak w “bardziej cywilizowanych” językach programowania.

Tags: ,
Author: Xion, posted under Programming » 2 comments

Prawie wszystko o przycinaniu 2D

2007-09-10 23:19

Rysując grafikę dwuwymiarową dość często chcemy, aby była ona wpasowana tylko w ograniczony obszar ekranu. Jeśli większość obrazu ma być wyrenderowaną trójwymiarowa scena, grafika HUDa nie może zajmować zbyt dużo miejsca. Wielkość rysowanych sprite’ów można kontrolować na wiele sposobów, począwszy od zabawy z macierzami (projekcji i/lub świata), a skończywszy na programowym skalowaniu wielokątów.
Podobnie jest z innym aspektem tego ‘ograniczania’ wyświetlanej grafiki, czyli przycinaniem. Tutaj jednak dużo zależy od tego, jakie kształty chcemy przycinać oraz czym jest region przycinania.

Część wspólna dwóch prostokątów

Najprostszy przypadek dotyczy ograniczenia prostokąta przez inny prostokąt. Przycinanie, czyli wyznaczenie ich części wspólnej, to w tym przypadku tylko kilka porównań współrzędnych tych prostokątów:

  1. const RECT Intersection(const RECT& rc2, const RECT& rc2)
  2. {
  3.    RECT out;
  4.    out.left = max(rc1.left, rc2.left);
  5.    out.top = max(rc1.top, rc2.top);
  6.    out.right = min(rc1.right, rc2.right);
  7.    out.bottom = min(rc1.bottom, rc2.bottom);
  8.    return out;
  9. }

Ponadto trzeba jeszcze tylko sprawdzić, czy powstały prostokąt jest poprawny – czyli czy spełnione są warunki: left mniejsze od right i top mniejsze niż bottom. W przeciwnym wypadku wyjściowe prostokąty nie mają części wspólnej.

Kiedy prostokątem chcemy uciąć coś o innym kształcie, wówczas sprawa znacznie się komplikuje z matematycznego punktu widzenia. Można jednak skorzystać z przydatnego narzędzia wbudowanego w Direct3D i znanego jako scissor test. Jest to specjalna, późna faza potoku renderowania, która odrzuca wszystkie piksele poza tymi mieszczącymi w podanym regionie (w przypadku DirectX 9 chodzi o prostokąt). Skorzystanie z tego mechanizmu jest bardzo proste i ogranicza się do:

  • ustawienia stanu renderowania D3DRS_SCISSORTESTENABLE na TRUE
  • ustawienia prostokąta przycinania za pomocą metody SetScissorRect urządzenia (współrzędne prostokąta liczone są w pikselach według viewportu)

A co zrobić w najbardziej skomplikowanym przypadku: gdy zarówno przycinane figury, jak i region przycinania są niestandardowymi kształtami? Cóż, DirectX to nie GDI i nie można oczekiwać, że będzie miał wszystko :) Nie jest to aczkolwiek sytuacja bez wyjścia, gdyż można ją rozwiązać przy pomocy stencil buffera (‘bufora szablonu’ lub ‘bufora matrycy’, że wymienię co ciekawsze tłumaczenia, jakie dotąd spotkałem). Wymaga to jednak nieco zabawy ze stanami urządzenia, a przede wszystkim renderowania regionu przycinania w postaci prymitywów od nowa w każdej klatce.

Tags: , ,
Author: Xion, posted under Programming » Comments Off on Prawie wszystko o przycinaniu 2D

Domykanie klas i metod

2007-09-09 12:10

W teorii dziedziczenie to niezbyt skomplikowana relacja między klasami, ale w praktyce potrzeba czasem niestandardowych rozwiązań. Niekiedy bowiem możliwość rozszerzenia hierarchii klas w nieskończoność nie jest wcale pożądana. Zazwyczaj chodzi tu o względy projektowe, rzadziej o efektywność.
Pytanie brzmi: czy można jakoś zabezpieczyć konkretną klasę przed dziedziczeniem (czyli zabronić w tworzenia opartych o nią klas pochodnych)?

W C# i Javie jest to możliwe za pomocą specjalnych słów kluczowych – odpowiednio sealed (‘zapieczętowany’) i final. Wystarczy opatrzyć nimi definicję klasy, by kompilator odmówił skompilowania kodu, który wykorzystywałby taką klasę jako bazową:

  1. public sealed class Foo { };
  2.  
  3. // błąd - dziedziczenie zapieczętowanej klasy
  4. public class Bar : Foo { };

Modyfikatory sealed/final można też stosować do metod wirtualnych. W tym przypadku sprawiają one, że dana metoda wirtualna nie może być nadpisywana w klasach pochodnych. Chwilowo nie przychodzi mi do głowy żaden pomysł na pokazanie przydatności takiego triku, ale zapewne takowy przykład istnieje :)

W C++ nie mamy rzecz jasna wspomnianych słów kluczowych (ani żadnych, które działałyby podobnie), ale istnieje sposób na zabezpieczenie klasy przed dziedziczeniem. Jest on jednak brzydki. A nawet bardzo brzydki. Mimo to zaprezentuję go.
Jak to często bywa w takich sytuacjach, sztuczka polega na kreatywnym wykorzystaniu pewnego mechanizmu językowego unikalnego dla C++, który generalnie służy do czegoś zupełnie innego. Tutaj chodzi o dziedziczenie wirtualne – a dokładniej o to, że wirtualna klasa bazowa musi być bezpośrednio inicjalizowana przez każdą klasę pochodną; nawet tą w setnym pokoleniu niżej. Do tego dodać należy jeszcze prywatne konstruktory i deklarację przyjaźni, a następnie zamieszać i doprawić do smaku, by ostatecznie upichcić coś w tym stylu:

  1. class CFoo; // deklaracja klasy, którą chcemy zabezpieczyć przed dziedziczeniem
  2.  
  3. // sztuczna klasa pomocnicza - "blokada"
  4. class CFoo_Lock
  5. {
  6.    // deklaracja przyjaźni
  7.    friend class CFoo;
  8.  
  9.    private:
  10.       // prywatne konstruktory
  11.       CFoo_Lock() { }
  12.       CFoo_Lock(const CFoo_Lock&) { }
  13. };
  14.  
  15. // zapieczętowana klasa
  16. class CFoo : public virtual CFoo_Lock { /* ... */ };
  17.  
  18. // próba dziedziczenia - błąd: brak dostępu do konstruktora CFoo_Lock
  19. class CBar : public CFoo { /* ... */ };

Klasa pomocnicza CFoo_Lock ma prywatny konstruktor domyślny, więc generalnie nie może być on wywoływany. W szczególności nie może być on wywołany przez konstruktor CBar – a jest to konieczne, gdyż CFoo_Lock jest wirtualną klasą bazową i musi być zainicjalizowana bezpośrednio przez dowolną klasę pochodną (nawet niebezpośrednio pochodną). Dlatego też próba dziedziczenia CFoo skończy się błędem mówiącym o braku dostępu do prywatnego konstruktora CFoo_Lock.
Dlaczego zatem CFoo działa? To już zasługa deklaracji przyjaźni umieszczonej w klasie-blokadzie. Przyjaźń międzyklasowa nie jest jednak dziedziczna, więc nasza blokada odmawia dostępu do prywatnych składowych (czyli konstruktorów) klasom pochodnym CFoo. Dzięki temu (i niuansom dziedziczenia wirtualnego) cały ten trik działa.

To rozwiązanie można naturalnie uczynić bardziej automatycznym – wystarczy zaprząc do pracy szablony i preprocesor, by uzyskać prawie to samo co sealed/final w C#/Javie. Trudno będzie jednak uznać to kombinowanie za eleganckie.
Wygląda więc na to, że najlepszym sposobem na zabezpieczenie klasy przed dziedziczeniem w C++, to po prostu… pokazać na nią palcem (tj. komentarzem) i powiedzieć “Nie dziedzicz mnie!”. No i mieć nadzieję, że ktoś nas posłucha.

Tags: , , ,
Author: Xion, posted under Programming » 1 comment

System GUI #6 – Pole wyboru i przycisk opcji

2007-09-06 20:09

Zwykłe przyciski (buttons) można tylko wciskać jak guziki na monitorze lub klawisze na klawiaturze. Wydawałoby się, że nie mają one wiele wspólnego z bardziej zaawansowanymi elementami interfejsu: polami wyboru (checkboxes) czy przyciskami opcji (radio buttons).
Te pierwsze to proste przełączniki: mogą być zaznaczone lub nie, swój stan zmieniają po kliknięciu i zazwyczaj odwzorowane są na jakieś funkcje programu, które mogą być włączane i wyłączane. Te drugie oferują z kolei wybór z większej liczby wariantów, z których tylko jeden może być aktywny w danej chwili. Z punktu widzenia użytkownika największa różnica polega jednak na tym, że pola wyboru mają obok etykiet tekstowych kwadraciki, zaś przyciski opcji – kółka :)

Przykład pól wyboru (checkbox) Przykład przycisków opcji (radio button)

Podobieństwo tych dwóch kontrolek do zwykłych przycisków polega na tym, że w identyczny sposób reagują na zdarzenia od myszy – czyli przede wszystkim kliknięcia. Podobieństwo okazało się na tyle duże, że sensowne okazało się połączenie wszystkich tych trzech rodzajów kontrolek wspólną klasą bazową. Nie lubię nadużywania dziedziczenia, ale akurat tutaj rozciągnięcie hierarchii klas wydaje się rozsądne.

Hierarchia klas kontrolek przyciskopodobnych

Z tych trzech typów kontrolek prawdopodobnie najbardziej interesujący jest przycisk opcji, czyli ten z kółeczkiem :) Trzeba bowiem zastanowić się, w jaki sposób zrealizować grupowanie tych obiektów w zestawy, w ramach których tylko jeden radio button może być zaznaczony. Spotkałem tutaj dwa rozwiązania:

  • Wprowadzenie specjalnej kontrolki-kontenera nazywanej zwykle RadioGroup. Kontener ten może zawierać tylko przyciski opcji, które wówczas nawet nie muszą być samodzielnymi kontrolkami (ale cały czas powinny oczywiście sprawiać takie wrażenie). Taka ‘grupa radiowa’ jako pojemnik może, ale nie musi być widoczna na ekranie. Przy takim rozwiązaniu spełniony jest zawsze warunek, że spośród wszystkich przycisków opcji będących bezpośrednio zawartych w jakimkolwiek kontenerze (RadioGroup lub innym) zaznaczony jest tylko jeden.
    Ten wariant występuje na przykład w bibliotece VCL (Delphi).
  • Dodanie do radio buttona właściwości określającej grupę przycisków, jak choćby jej nazwa (napis) lub indeks (liczba). Wtedy należy zapewnić, że zaznaczenie będzie unikalne w ramach przycisków z tego samego kontenera oraz z tej samej grupy.
    To rozwiązanie występuje w Windows Forms.

Ostatecznie zdecydowałem się na wariant drugi. Z punktu widzenia implementacji najważniejszego elementu – czyli dbania o to, by tylko jedna opcja była zaznaczona – oba rozwiązania są bardzo podobne. Tak czy owak trzeba zawsze najpierw odznaczyć wszystkie przyciski, a potem zaznaczyć ten kliknięty. Jest to przy tym jedna z tych sytuacji, w których bardzo ułatwia życie przeklinany operator rzutowania dynamic_cast :]

Tags: , , ,
Author: Xion, posted under Programming » Comments Off on System GUI #6 – Pole wyboru i przycisk opcji

Bolączki C++ #2 – Pakietowanie kodu

2007-09-01 19:05

W czasach panowania języków proceduralnych i strukturalnych – jak C czy Pascal – możliwości ukrywania i ochrony kodu przed niepowołaną zmianą były, delikatnie mówiąc, słabe. Wszystkie funkcje, pola i struktury były dostępne na zewnątrz i tylko dzięki programistycznej kurtuazji (i czytaniu dokumentacji technicznej, o ile istniała) zawdzięczało się poprawne działanie kodu.
Sytuacja poprawiła się wraz z językami obiektowymi, takimi jak C++. Mamy już klasy, których składniki mogą być określone jako publiczne, chronione (protected) albo prywatne. Jeżeli tylko odpowiednio oznaczymy składowe należące do implementacji, nikt niepowołany nie będzie miał do nich dostępu zwyczajnymi środkami.
Od tego czasu świat poszedł jednak do przodu pod względem technik organizacji kodu. Nowsze języki programowania – jak Java, C# czy Python – posiadają wbudowane mechanizmy pakietów (zwanych w .NET złożeniami – assemblies). Dzięki nim można grupować klasy wykonujące wspólne zadania i zapewniać dostęp do wybranych składowych wszystkich klasom z danego pakietu. W precyzyjnym określaniu widoczności celuje zwłaszcza C#.

W C++ teoretycznie mamy dość podobne możliwości, oferowane przez kilka osobnych mechanizmów językowych. Czymś co zdaje się najbardziej przypominać pakiety lub złożenia są przestrzenie nazw (namespaces). Ich przeznaczeniem jest przede wszystkich jednak zabezpieczenie przed dublowaniem i niejednoznacznością nazw. Na przykład klasa string z STL jest zawarta w przestrzeni std i dzięki temu nie będzie pomylona z jakąś inną klasą łańcuchów znakowych o tej samej nazwie.
Jednak przestrzenie nazw to bardzo ułomny sposób pakietowania. Elementy jednej przestrzeni (klasy, funkcje) domyślnie są dla siebie tak samo obce, jak te należące do różnych przestrzeni. Można to oczywiście zmienić, korzystając z deklaracji przyjaźni; wymaga to jednak znajomości wszystkich tych “przyjaciół” – czyli odpowiednich deklaracji zapowiadających – co jest bardzo niewygodne. Naturalnie można powiedzieć, że wtedy przynajmniej wiemy, kto nas “podgląda”. Lecz co z tego, skoro w takim zaprzyjaźnianiu stosować można wyłącznie zasadę wszystko albo nic: klasa/funkcja będzie widziała albo wszystko składowe, albo tylko publiczne (domyślnie). Bez pokrętnych sztuczek nie da się udostępnić pewnych składników tylko w ramach “pakietu”.

Całkiem osobną kwestią jest też to, jak mechanizm przestrzeni nazw współpracuje z tą zakałą C++, czyli plikami nagłówkowymi i dyrektywą #include. W nowszym językach wystarcza instrukcja podobnej do tej:

  1. using System.Windows.Forms;

żeby jednocześnie zadeklarować użycie pakietu i uchronić się przed koniecznością używania kwalifikowanych nazw (w tym przypadku np. ListBox zamiast System.Windows.Forms.ListBox). W C++ potrzebujemy do tego dwóch dyrektyw:

  1. #include <vector>
  2. using namespace std;

i niby wszystko byłoby w porządku. Problem w tym, że wielce inteligentny mechanizm plików nagłówkowych oraz #include sprawia, że nazw niekwalifikowanych możemy używać praktycznie tylko w plikach *.cpp. Gdyby bowiem umieścić using namespace w nagłówku, to instrukcja ta zostałaby bezmyślnie powielona we wszystkich plikach, które ten nagłówek dołączają. A wtedy przestrzeń nazw nie spełniałaby nawet tej najprymitywniejszej funkcji, czyli ochrony przed wieloznacznością powtarzających się nazw.

Tak naprawdę to same przestrzenie nazw nie są złe. Nawet same pliki nagłówkowe z C++ nie byłyby aż tak bardzo złe, gdyby ich zadaniem nie było tylko ułatwianie życia kompilatorowi. Dopiero połączenie tych dwóch rzeczy sprawia, że zdecydowanie odechciewa się używać którejkolwiek z nich ;P
Istnieje jednak pewien optymistyczny akcent. Jeśli kiedykolwiek zostanie zniesiony w C++ podział na dwa rodzaje plików z kodem, wówczas i przestrzenie nazw będą musiały się przekształcić w coś podobnego do pakietów Javy czy złożeń z .NET. Podobnie jest w drugą stronę: jeżeli C++ dorobi się mechanizmu pakietów, wtedy pliki nagłówkowe i dyrektywa #include nie będą miały wielkiego sensu. Wypada więc tylko kibicować Komitetowi Standaryzacyjnemu w wykonaniu tego pierwszego kroku, bo drugi prawdopodobnie zrobią siłą rozpędu :)

Tags: ,
Author: Xion, posted under Programming » 3 comments

System GUI #5 – Okno

2007-08-28 17:43

Dawno temu w firmie Xerox wymyślono, że interfejs użytkownika można zapakować w zestaw prostokątnych, nakładających się na siebie okien. Pomysł okazał się niezwykle trafiony i zaowocował nawet wielce kreatywną nazwą pewnego systemu operacyjnego ;) Od tamtej pory trudno sobie wyobrazić zaawansowane UI posługujące się czymś innym niż właśnie zestawem okien.

Nie jest trudno otrzymać na ekranie puste okno. W środowiskach RAD – w rodzaju Visual C# czy Delphi – mamy je najczęściej dane automatycznie, gdy tworzymy nowy projekt. W przypadku programowania niewizualnego (jak np. z użyciem czystego Windows API) sprawa jest nieco bardziej skomplikowana, ale i tak zamyka się w nie więcej niż kilkudziesięciu linijkach.
Takie puste okno wydawać się może mało interesujące czy wręcz zbyt oczywiste, aby zwracać na nie uwagę. Rzadko zwracamy uwagę na to, że ten pozornie trywialny prostokąt sam w sobie potrafi bardzo wiele. Wśród typowych możliwości mamy chociażby:

  • przesuwanie za pomocą przeciągania za pasek tytułu
  • zmianę jednego z wymiarów poprzez przeciąganie brzegów
  • zmianę obu wymiarów poprzez przeciąganie rogów okna
  • Przeciąganie za pasek tytułu okna Zmiana jednego z wymiarów okna Zmiana obu wymiarów okna Przyciski sterujące oknem

  • minimalizacja, maksymalizacja i zamykanie okna przyciskami na pasku tytułu

Uzyskanie podobnej funkcjonalności, zaczynając od zera, jest bardziej pracochłonne niż może się wydawać. W moim przypadku otrzymanie czegoś, co przypomina w pełni funkcjonalne i interaktywne okno, zamknęło się w ok. dwóch tysiącach linijek kodu – nie licząc oczywiście modułu grafiki 2D, potrzebnego do rysowania okien.
To całkiem sporo. Skutkiem ubocznym tej pisaniny jest też to, że obecnie patrzę na stare poczciwe okna systemu Windows z nieco większym respektem :)

Tags: , ,
Author: Xion, posted under Programming » Comments Off on System GUI #5 – Okno

Losowanie tabelkowe

2007-08-26 14:35

W niemal każdym programie chociaż raz zdarza się potrzeba, by “rzucić kośćmi” i pozwolić, by wydarzyło się coś losowego. Oznacza to skorzystanie z generatora liczb pseudolosowych, aby uzyskać ‘przypadkową’ wartość. Nie będzie ona faktycznie losowa, lecz dzięki zastosowaniu matematycznych formuł o dużej nieregularności rezultat może być bardzo zbliżony do ideału. Istnieje oczywiście wiele algorytmów wyznaczania liczb pseudolosowych, różniących się faktyczną przypadkowością uzyskiwanego wyniku.
W różnych językach programowania mamy natomiast odmienne sposoby na uzyskanie liczby ‘losowej’. Zwykle najwygodniejszym jest wartość z zakresu [0..1], bo odpowiada to matematycznemu pojęciu prawdopodobieństwa. Aby w C++ uzyskać taki rezultat, wystarczy napisać proste opakowanie na biblioteczną funkcję rand:

  1. float Random() { return rand() / (float)RAND_MAX; }

Mając taki generator, możemy już łatwo sprawdzić, czy “zdarzyło się” coś, czego prawdopodobieństwo znamy:
// doświadczenie losowe z określonym prawdopodobieństwem
bool RandomOccured(float fProbability) { return Random() <= fProbability; }[/cpp] W ten sposób możemy na przykład rzucać wirtualną monetą. Przykładowa tabelka ataku w World of WarcraftSprawa się jednak komplikuje, jeżeli możliwych wyników doświadczenia jest więcej, a przy okazji mają one różne prawdopodobieństwa zajścia. Tak się dzieje na przykład w grach RPG, w których konieczne jest obliczanie rezultatów zadanych ciosów (trafienie, pudło, unik, blok, itp.). Skuteczność postaci w walce zależy zwykle od jej statystyk, więc szanse poszczególnych wyników nie są stałe i zmieniają się w trakcie gry.
Dobre generatory liczb pseudolosowych są zaś nierzadko względnie kosztowne obliczeniowo. Dlatego zamiast wykonywać po jednym losowaniu dla każdego możliwego rezultatu (zaszedł – nie zaszedł), znacznie lepiej jest załatwić wszystko jednym losowaniem. Nie jest to trudne:
// doświadczenie losowe z prawdopodobieństwem określonym tabelką
int RandomResult(const float* aProbs, int n)
{
float fRand = Random();

float fAccum = 0.0f;
for (int = 0; i < n; ++i) { // sprawdzamy, czy wylosowana liczba mieści się w zakresie p-stwa if (fAccum <= fRand && fRand < fAccum + aProbs[i]) return i; fAccum += aProbs[i]; } // błąd return -1; }[/cpp] Tak naprawdę liczymy tutaj dla każdego możliwego rezultatu wartość tzw. dystrybuanty. Ale chyba nie warto wnikać w takie teoretyczne szczegóły – grunt, że powyższa metoda działa w praktyce :) Trzeba tutaj jednak pamiętać, aby prawdopodobieństwa sumowały się do 1. Jeśli tak nie jest, można przeskalować wylosowaną liczbę.

Tags: ,
Author: Xion, posted under Math, Programming » Comments Off on Losowanie tabelkowe
 


© 2023 Karol Kuczmarski "Xion". Layout by Urszulka. Powered by WordPress with QuickLaTeX.com.