RAW Input

Tym razem będzie znów coś związanego z systemem Windows i programowaniem gier, czyli obsługa myszy za pomocą RAW Input. Jest to jeden z trzech popularnych sposobów obsługi tych urządzeń. Pozostałymi są komunikaty procedury okna oraz DirectInput, z tym że ten ostatni jest niezalecany i już nie jest wspierany przez Microsoft na rzecz właśnie RAW Input. Jednak dlaczego nie te dwa?

Komunikaty procedury okna nie są tak naprawdę takie złe, co więcej bardzo dobrze nadają się do małych gier casualowych, najlepiej tych odpalanych w oknie, ponieważ wartościami otrzymywanymi od systemu (w przypadku myszy) jest pozycja kursora, a pozycja jest poprawiana tak, aby znalazł się on w miejscu w którym użytkownik się tego spodziewa. Niestety dane otrzymywane w ten sposób nie są dokładne więc w bardziej wymagających grach mogą okazać się nieprzydatne.
DirectInput jest natomiast sposobem na uzyskanie dokładniejszych danych, jednak zasada jego działania opiera się na utworzeniu dodatkowego wątku, podpięcia się do wskazanego okna i odczytywaniu danych bezpośrednio ze sterownika, czyli surowych danych (RAW). Najprawdopodobniej jest to przyczyną braku wsparcia dla tej metody, dodatkowo niektóre antywirusy mogą ostrzegać że aplikacja którą napisaliśmy jest keyloggerem.

RAW Input umożliwia aplikacji uzyskanie surowych danych bezpośrednio ze sterownika. Zasada działania opiera się tutaj, tak samo jak w pierwszej metodzie, na komunikatach procedury okna. Różnica polega na tym, że chęć otrzymywania tych danych należy wcześniej w systemie zarejestrować. Jest to póki co najlepsza metoda uzyskiwania dokładnych danych z myszy, ponieważ dostarcza informacje na temat przesunięć, a nie pozycji kursora. Otrzymywanie danych z klawiatury również jest możliwe, ale w tym przypadku dokładność nie ma sensu.

Do rejestracji urządzeń, których dane chcielibyśmy otrzymywać tą metodą, służy funkcja:

BOOL WINAPI RegisterRawInputDevices(
  __in  PCRAWINPUTDEVICE pRawInputDevices,
  __in  UINT uiNumDevices,
  __in  UINT cbSize
);


Pierwszym parametrem tej funkcji jest tablica obiektów struktury RAWINPUTDEVICE, która opisuje rejestrowane urządzenie. Pozostałe parametry to ilość urządzeń i rozmiar struktury, czyli sizeof(RAWINPUTDEVICE).

Poniższy przykład pokazuje rejestrację myszy:

1
2
3
4
5
6
7
8
9
10
RAWINPUTDEVICE rawStructure;
rawStructure.usUsagePage = 1;                            // Typ urządzenia (w tym przypadku GENERIC)
rawStructure.hwndTarget = m_hWnd;                    // Uchwyt okna
rawStructure.usUsage = 2;                                   // Rodzaj urządzenia (2 - mysz)
rawStructure.dwFlags = RIDEV_NOLEGACY;        // Więcej info w opisie struktury
 
if(!RegisterRawInputDevices(&rawStructure, 1, sizeof(RAWINPUTDEVICE)))
{
    // Obsługa błędu
}

Wartości usUsage i usUsagePage można znaleźć w tym dokumencie.

Jeśli rejestracja urządzenia się powiedzie, to aplikacja przestanie otrzymywać standardowe komunikaty związane z tym urządzeniem, a zacznie otrzymywać komunikaty WM_INPUT z danymi RAW tego urządzenia.
Dane można otrzymywać na dwa sposoby: pojedynczo, bezpośrednio w trakcie obsługi komunikatu lub zbuforowane. Poniżej podam przykład na standardową obsługę komunikatu WM_INPUT, więcej info na temat tej i drugiej metody można znaleźć na tej stronie.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
LRESULT WndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
     switch(msg)
     {
     case WM_INPUT:
          {
               RAWINPUT riStruct;
               DWORD dwSize = sizeof(RAWINPUT);
 
               if(GetRawInputData((HRAWINPUT)lParam, RID_INPUT, &riStruct, &dwSize, sizeof(RAWINPUTHEADER)) == (UINT)-1)
               {
                    // Obsługa błędu
               }
 
               if(riStruct.header.dwType == RIM_TYPEMOUSE)
               {
                    riStruct.data.mouse.lLastX;     // Przesuniecie X
                    riStruct.data.mouse.lLastY;     // Przesuniecie Y
 
                    riStruct.data.mouse.ulButtons;     // Przyciski myszy
 
                    riStruct.data.mouse.usButtonData;     // Rolka myszy
               }
          }
          break;
     }
}

Dziwić może to, że funkcja przyjmuje wskaźnik na zmienną do rozmiaru struktury RAWINPUT. Jest to spowodowane tym, że różne urządzenia mają różne struktury i podanie niewłaściwego rozmiaru skutkuje błędem oraz wpisaniem wymaganego rozmiaru pod podanym adresem.

Wyrejestrowanie urządzenia następuje po wywołaniu funkcji RegisterRawInputDevices podając jako w flagę w przekazywanej strukturze RIDEV_REMOVE.

Ogólnie rzecz biorąc RAW Input jest najlepszym sposobem na otrzymanie dokładnych danych z urządzeń wejściowych. Obsługa klawiatury jest trochę bardziej skomplikowana, a szczerze mówiąc nie widzę sensu korzystania z tego sposobu. Lepiej pozostać przy komunikatach. Osobiście uważam, że odpytywanie o stan inputu jest lepsze niż odpowiadanie na komunikaty, ponieważ aplikacja jest wtedy bardziej przewidywalna.

Java 2 Micro Edition

Tym razem będzie trochę o programowaniu aplikacji na telefon komórkowy korzystając z platformy J2ME, co jak się okazuje wcale nie jest takie trudne. Aby w ogóle zacząć pisać aplikację na tę platformę należy zaopatrzyć się w odpowiednie narzędzia. Praktycznie każdy szanujący się edytor do tworzenia aplikacji Javowych, umożliwia tworzenie projektów na urządzenia mobilne. Ja jednak postanowiłem pójść po linii najmniejszego oporu i ściągnąłem pakiet J2ME Platform SDK. Pakiet ten zawiera w sobie wszystko co jest potrzebne do pisania tego typu aplikacji (w tym niewielki edytor oparty na Netbeans). Można go znaleźć pod tym linkiem.

Aplikacje J2ME nazywa się Midletami, ponieważ punktem startowym aplikacji jest klasa dziedzicząca po interfejsie o tej nazwie. Interfejs ten wymusza implementacje 3 podstawowych funkcji:

  • void startApp()
  • void pauseApp()
  • void destroyApp(boolean unconditional)

Pierwsza funkcja wywoływana jest gdy aplikacja rozpoczyna lub wznawia działanie, w celu umożliwienia załadowania, zainicjalizowania odpowiednich danych. Druga funkcja umożliwia aplikacji zwolnienie części zasobów w momencie gdy aplikacja przechodzi w stan wstrzymania. Ostatnia funkcja jest wywoływana w momencie zamykania midletu. Funkcja ta posiada argument typu boolean, który określa, czy aplikacja ma zostać zamknięta bezwarunkowo (w przeciwnym wypadku powinien zostać rzucony odpowiedni wyjątek).

Oprócz tego działanie aplikacji opiera się na kilku innych klasach: Display, Command oraz dziedziczących po klasie abstrakcyjnej DisplayableScreen lub Canvas. Klasa Display odpowiada za to co jest wyświetlane na ekranie, a wyświetlane są obiekty klas pochodnych od klasy Displayable. Dodatkowo te obiekty mogą wysyłać różne zdarzenia, czyli obiekty klasy Command. W celu rejestracji tych zdarzeń potrzebny jest jeszcze obiekt klasy implementującej interfejs CommandListener, który należy przekazać do funkcji Displayable::setCommandListener().

Oto przykładowy kod Hello World:

package hello;
 
import javax.microedition.midlet.*;
import javax.microedition.lcdui.*;
 
public class HelloMIDlet extends MIDlet implements CommandListener
{
    private Command exitCommand; // The exit command
    private Display display;     // The display for this MIDlet
 
    public HelloMIDlet()
    {
        display = Display.getDisplay(this);
        exitCommand = new Command("Exit", Command.EXIT, 0);
    }
 
    public void startApp()
    {
        TextBox t = new TextBox("Hello", "Hello, World!", 256, 0);
 
        t.addCommand(exitCommand);
        t.setCommandListener(this);
 
        display.setCurrent(t);
    }
 
    public void pauseApp()
    {
    }
 
    public void destroyApp(boolean unconditional)
    {
    }
 
    public void commandAction(Command c, Displayable s)
    {
        if (c == exitCommand)
        {
            destroyApp(false);
            notifyDestroyed();
        }
    }
}

Jak widać te kilka klas wystarcza, aby utworzyć prostą aplikację na telefon komórkowy. Bardziej złożone korzystają dodatkowo z RecordStore do zapisywania informacji między różnymi uruchomieniami aplikacji oraz kilku innych.

Nowa strona – Linki

Postanowiłem dodać nową stronę na bloga – Linki. Będę na niej zamieszczał odnośniki, które według mnie są warte kliknięcia. Lista linków będzie podzielona na dwie części, z których pierwsza będzie zawierać te, które już widziałem, a do pozostałych po prostu zajrzałem. Postaram się również, w miarę możliwości, dodawać do nich opisy.

MSBuild, czyli windowsowy odpowiednik make

Korzystając z MS Visual Studio, czasami przydałaby się możliwość skompilowania całej solucji korzystając z wiersza poleceń (cmd/PowerShell), tak jak ma to miejsce w przypadku Unixowego make. Okazuje się, że można to zrobić, a służy do tego konsolowy programik MSBuild. Znajduje się on w jednym z katalogów:

C:\Windows\Microsoft.NET\Framework\[wersja] lub
C:\Windows\Microsoft.NET\Framework64\[wersja] (wersja x64).

W każdym z katalogów powyżej wersji 1.1 (czyli od 2.0) znajdują się kompilatory wszystkich języków zarządzanych oraz kilka dodatkowych narzędzi w tym właśnie MSBuild (dla ciekawych, kompilator cl.exe języka C++ znajduje się pod ścieżką [katalog Visual Studio]\VC\bin).

Pytanie: dlaczego porównałem MSBuild do make? Głównie ze względu na sposób użycia, czyli „odpal i idź na kawę” :). Aby zbudować solucję wystarczy, „być” w katalogu z plikiem .sln i uruchomić MSBuild, wtedy program skorzysta z domyślnych ustawień zapisanych w pliku solucji. Można dodatkowo podać jako argumenty

-p:”Configuration=Release;Architecture=x64″

wtedy cała solucja zostanie zbudowana zgodnie z ustawieniami Release pod platformę 64-bitową. Można również budować solucje z innego katalogu, aby to zrobić trzeba podać ścieżkę do pliku .sln.

Cały wyciąg budowania aplikacji opatrzony jest kolorami, więc nie trzeba specjalnie doszukiwać się poszczególnych etapów. Więcej informacji można znaleźć po następującymi linkami:

http://msdn.microsoft.com/en-us/library/ee662426.aspx
http://msdn.microsoft.com/en-us/library/ms164311.aspx

Nowy hosting, [Netrix]#website up

Wreszcie udało mi się zakupić nowy, tym razem płatny, hosting wraz z odpowiednią domeną (jak widać :)). Głównym czynnikiem, który przyczynił się do tych zmian był pad bazy danych na starym (darmowym) hostingu. W związku z tym, postanowiłem dać sobie z nim spokój, btw. może przy okazji uda mi się powąchać nieco PHP, kto wie :D. A teraz co się działo w międzyczasie, czyli IGK.

Otóż w dniach 9-11 kwietnia odbyła się konferencja Inżynieria Gier Komputerowych (IGK 2010) na której oczywiście byłem (trzeci raz z rzędu). Tym razem udało mi się nawet wziąć udział w Compo. Razem Dabem, tanatosem i Wyszo w drużynie „Klapek z prawej nogi” zajęliśmy szóste miejsce. Wynikiem naszej pracy jest ta oto aplikacja. Gierka została napisana w języku C# przy użyciu Tao Framework do grafiki oraz irrKlang do dźwięku. Wymagania: Tao Framework + .NET Framework 2.0.  Oto kilka screenów:

W tym roku nie zamierzam pisać relacji, tak więc aby dowiedzieć się czegoś więcej, polecam udać się na strony Rega i Xiona.

Karta WiFi jako Access-Point

Niedawno kupiłem sobie w końcu router z AP, ale zanim jeszcze to zrobiłem chciałem podłączyć PS3 do internetu za pomocą sieci WLAN. Niestety konsola nie obsługuje sieci Ad-hoc, więc potrzebny był AP.
Poszukując rozwiązania natrafiłem w końcu na tę stronę. W skrócie trik polega na wykorzystaniu wirtualnego adaptera WiFi, który jest domyślnie zainstalowany w Windows 7 do utworzenia sieci bezprzewodowej w trybie AP. Oczywiście nie wszystkie karty sieciowe obsługują ten tryb. Lista obsługiwanych chipsetów również znajduje się na pod tym linkiem. Dodatkowo dodam od siebie, że karta Atheros AR9285 z Eee PC obsługuje ten tryb, natomiast Atheros AR5006EG już nie.

Nowy nabytek – Playstation 3 + Uncharted

Jakiś czas temu postanowiłem w końcu kupić maszynkę do grania, ponieważ do tej pory najmocniejszym sprzętem do tego celu był komputer z P4 2.8 GHz + GeForce 6200, więc niezbyt mocny zestaw. Mój wybór padł na Playstation 3 w starszej wersji, czyli sprzęt do grania + dostęp do potężnego procesora Cell z poziomu linuksa (którego na nowszych wersjach zainstalować nie można). Wraz z konsolką kupiłem również grę Uncharted, oczywiście do testów :D. Jakie są moje wrażenia ?

Muszę przyznać, że to jest to czego mi brakowało. Konsolka radzi sobie świetnie z najnowszymi grami. Nie ma problemów z wymaganiami sprzętowymi, które są udręką pecetów, a zestaw TV Full HD + kino domowe komponuje się z nią idealnie. Dodatkowo bezprzewodowy kontroler SIXAXIS z DualShock sprawia że wrażenia z gry są jeszcze większe. Oczywiście dalej pozostaję w przekonaniu, że w strzelanki i strategie na konsolach grać się nie da, bo do tego potrzebne jest biurko, myszka, klawiatura i monitor, ale w platformówki i przygodówki takie jak np. Uncharted gra się naprawdę świetnie.

Dodatkową zaletą PS3 w porównaniu do Xbox360 jest całkowicie darmowe PSNetwork, do którego można się zarejestrować bez żadnego oszustwa związanego z lokalizacją. W samej sieci PSN dostępne jest wiele gier do kupienia oraz wersji demonstracyjnych. Tych ostatnich (po wpisaniu w wyszukiwarkę frazy „demo”) jest ponad 150, więc postanowiłem sobie ich trochę naściągać.

Jeśli chodzi o grę Uncharted, to jest to produkcja typu AAA i przy okazji exclusive na PS3. Gra jest przygodówką z elementami strzelanki o poszukiwaniu skarbu El Dorado. Głównym bohaterem jest Nathan Drake, który jest swoistą kopią filmowego Indiany Jonesa. Grafika w Uncharted jest naprawdę piękna. Oświetlenie genialnie oddaje klimat, a świetnie dobrane materiały i normal-mapping sprawiają, że gra jest niezwykle szczegółowa. Dopracowany jest praktycznie każdy detal jak np. mokre ubranie po wyjściu z wody. W samej grze nie ma również żadnego ładowania między poziomami, a AI zachowuje się w całkiem inteligentnie. Fabuła jest wciągająca i muszę przyznać, że kilka razy mnie zaskoczyła.

Postaram się napisać kiedyś co nieco o linuksie na PS3, jeśli będę miał okazję i czas się tym zająć :).

Pobieranie kodu HTML stron w C++ za pomocą WinSock2

Dzisiejsza notka będzie o tym jak pobrać kod HTML strony WWW w aplikacji C++ korzystając z WinSock2.

Pierwszą rzeczą, którą trzeba wiedzieć jest to w jaki sposób robi to przeglądarka internetowa. Zacznę od tego jak wygląda typowy adres strony WWW na przykładzie adresu do działu artykułów na stronie http://www.gamedev.pl/:

http://www.gamedev.pl/articles.php

Powyższy adres składa się z kilku części:

  • „http://” – protokół reprezentujący sposób transmisji danych, dzięki niemu przeglądarka wie w jaki sposób komunikować się z serwerem oraz na jaki port wysyłać żądania
  • „www.gamedev.pl” – domena na którą będzie wysłane zapytanie – podany adres jest tłumaczony na adres IP przez serwer DNS
  • „/articles.php” – adres żądanego plik lub żądanie dla serwera WWW, które aplikacja wykorzystuje do stworzenia nagłówka

Przeglądarka mając adres strony tworzy odpowiedni nagłówek, który zostaje wysłany do serwera WWW. Jak już wspomniałem adresem tego serwera jest domena zawarta w adresie WWW natomiast port jest określany na podstawie protokołu, czyli w przypadku „http://” jest to port 80. Nagłówek HTTP powinien zawierać następujące elementy:

  • rodzaj zapytania – w przypadku pobrania strony jest to „GET”
  • adres żądanego pliku lub żądanie – to co chcemy od serwera otrzymać, najczęściej jest to adres pliku na serwerze, czyli „/articles.php” w tym przykładzie
  • wersja protokołu – typowo HTTP/1.1
  • domena hosta

Dodatkowo, jeśli jest to konieczne, można wysłać informacje o tym, jakiej przeglądarki używamy (UserAgent), informacje o akceptowanych plikach, kodowaniu, ciasteczkach oraz czasie trwania połączenia, więcej można dowiedzieć się z tej strony wiki oraz samych nagłówków, które wysyła przeglądarka. Jako ciekawostkę mogę dodać, że istnieje fajna wtyczka dla Firefoxa, która pokazuje nagłówki wysyłane przez przeglądarkę – Live HTTP headers.

Oto nagłówek uzyskany z pomocą tej wtyczki podczas łączenia się do przykładowej strony:

1
2
3
4
5
6
7
8
9
GET /articles.php HTTP/1.1
Host: www.gamedev.pl
User-Agent: Mozilla/5.0 (Windows; U; Windows NT 6.1; pl; rv:1.9.1.7) Gecko/20091221 Firefox/3.5.7
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: pl,en-us;q=0.7,en;q=0.3
Accept-Encoding: gzip,deflate
Accept-Charset: ISO-8859-2,utf-8;q=0.7,*;q=0.7
Keep-Alive: 300
Connection: keep-alive

Jak widać jest on trochę długi (usunąłem informacje o ciasteczkach, nie są tutaj potrzebne). Aby pobrać kod strony wystarczą tak naprawdę dwie pierwsze linijki. Jednak ważną rzeczą jest, aby po każdej linijce występowała para znaków \r\n a koniec nagłówka reprezentowany był przez dwie pary tych znaków. W przypadku, gdy nagłówek nie będzie zawierał tak skonstruowanego nagłówka, serwer po prostu udrzuci zapytanie.

Teraz pora na kod C++ z WinSock2, oto on:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
// Usuwa zbędne definicje z nagłówka
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
 
#include <winsock2.h>
#include <ws2tcpip.h>
#include <string>
 
#define BUFFER_SIZE 2048
#pragma comment(lib, "ws2_32.lib")	// Niezbędna biblioteka
using namespace std;
 
int main()
{
	//************************************************
	// Inicjalizacja WinSock2
	//************************************************
	WSADATA wsaData;
	int error;
	string answer;
	ZeroMemory(&wsaData, sizeof(wsaData));
 
	if(FAILED(WSAStartup(MAKEWORD(2,2), &wsaData)))	// MAKEWORD(2,2) - Wersja WinSock
	{
		return 1;
	}
 
	char recvBuffer[BUFFER_SIZE];
	addrinfo hint;					// Struktura przechowująca dane o połączeniu
	addrinfo* wsResult;				// Wskaźnik na rezultat
	SOCKET pSocket;					// Właściwy pSocket
 
	ZeroMemory(recvBuffer, sizeof(recvBuffer));
	ZeroMemory(&hint, sizeof(hint));
	hint.ai_family = AF_UNSPEC;		// Rodzaj transmisji - nieokreślony
	hint.ai_socktype = SOCK_STREAM;	// Typ gniazda - strumień
	wsResult = NULL;
	pSocket = INVALID_SOCKET;
 
	//************************************************
	// Tworzenie zapytania	
	//************************************************
 
	// Wyciąganie informacji z adresu
	string httpAddress = "http://www.gamedev.pl/articles.php";
	string temp = httpAddress.substr(httpAddress.find("http://") + sizeof("http://") - 1);	// Tylko http:// więc można wyciąć
	string domain = temp.substr(0, temp.find_first_of('/'));								// Domena
	string addressTail = temp.substr(temp.find_first_of('/'));								// Żądanie pliku
 
	// Tworzenie nagłówka
	temp = string("GET ") + addressTail + " HTTP/1.1\r\n"
			   + "Host: " + domain + "\r\n"
			   //+ "User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; pl; rv:1.9.1.5) Gecko/20091102 Firefox/3.5.5\r\n"
			   //+ "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\n"
			   //+ "Accept-Language: pl,en-us;q=0.7,en;q=0.3\r\n"
			   //+ "Accept-Encoding: gzip,deflate\r\n"
			   //+ "Accept-Charset: ISO-8859-2,utf-8;q=0.7,*;q=0.7\r\n"
			   //+ "Keep-Alive: 300\r\n"
			   //+ "Connection: keep-alive\r\n"
			   + "\r\n\r\n";
 
	// Pobieranie IP serwera z serwera DNS
	if(FAILED(getaddrinfo(domain.c_str(), TEXT("80"), &hint, &wsResult)))
	{
		printf("Nie udalo sie pobrac IP\n");
		return 1;
	}
 
	// Wskaźnik na zwrócone adresy
	addrinfo* ptr = wsResult;
 
	// Tworzenie socketu
	if(FAILED(pSocket = socket(wsResult->ai_family, wsResult->ai_socktype, wsResult->ai_protocol))) 
	{
		freeaddrinfo(wsResult);
		printf("Nie udało się utworzyc socketu\n");
		return 1;
	}
 
	//************************************************
	// Wysyłanie zapytania	
	//************************************************
 
	// Łączenie się do serwera
	if(FAILED(connect(pSocket, ptr->ai_addr, (int)ptr->ai_addrlen))) 
	{
		printf("Nie udalo sie polaczyc do serwera\n");
		goto Error;
	}
 
	// Wysyłanie nagłówka
	if(FAILED(send(pSocket, temp.c_str(), temp.size(), 0)))
	{
		printf("Nie udalo sie wyslac naglowka\n");
		goto Error;
	}
 
	// Kończenie wysyłania
	if(FAILED(shutdown(pSocket, SD_SEND)))
	{
		printf("Nie udalo sie zamknac polaczenia\n");
		goto Error;
	}
 
	//************************************************
	// Odbieranie danych
	//************************************************
 
	// Odbieranie danych
	do
	{
		ZeroMemory(recvBuffer, sizeof(recvBuffer));
		error = recv(pSocket, recvBuffer, sizeof(recvBuffer), 0);
 
		if(error > 0)
		{
			answer += string(recvBuffer);
		}
	}
	while(error > 0);
 
	closesocket(pSocket);		// Zamknięcie socketu
	freeaddrinfo(wsResult);		// oraz zwolnienie struktury addrinfo
 
	printf("Kod HTML strony \"http://www.gamedev.pl/articles.php\":\n\n%s", answer.c_str());
	return 0;
 
Error:
	closesocket(pSocket);
	freeaddrinfo(wsResult);
	return 1;
}

Wynikiem tego kodu jest wyświetlony kod HTML przykładowej strony.

Environmental mapping + skybox

Tym razem będzie mały update dotyczący silnika. Niby nic, ale można się pochwalić.

etuymcrypPo pierwsze Environmental mapping, czyli proste przybliżenie zjawiska odbicia światła na powierzchni ciała oraz załamania światła na granicy dwóch ośrodków. Wymaga dostarczenia tylko dodatkowej tekstury sześciennej (cube-mapa) oraz skorzystania z dwóch dodatkowych funkcji HLSL: refract i reflect. Można go wzbogacić dodatkowo o efekt Fresnela i rozszczepienie chromatyczne. Całość wygląda jak na screenie obok. Efekt ten nie jest trudny do zaimplementowania, więc szczegółów implementacyjnych nie będzie, zresztą są one w książce CG Tutorial.

Kolejną rzeczą jest skybox. Jak nazwa wskazuje, jest to pudełko z tłem nieba, które powoduje, że scena z obiektami nie wygląda po prostu pusto. Poniżej przedstawiam trzy kroki rysowania skyboxa:

  1. Potrzebne jest pudełko wraz z teksturą, którą jest już wspomniana wcześniej cube-mapa. Biblioteka D3DX posiada do tego odpowiednią funkcję: D3DXCreateBox(), która jako parametry przyjmuje wymiary pudełka. Najlepiej jest stworzyć sześcian 1:1:1.
  2. Pudełko powinno obracać się razem z kamerą, ale nie może zmieniać swojej pozycji. Aby to osiągnąć należy użyć tylko macierzy widoku i projekcji. przy czym ostatni wiersz tej pierwszej powinien mieć takie wartości: (0, 0, 0, 1).
  3. Skybox powinien być pierwszym elementem rysowanym na scenie, a dodatkowym warunkiem koniecznym by był on tłem całej sceny jest wyłączenie zapisu do ZBuffera. Dzięki temu każdy kolejny obiekt sceny będzie rysowany zawsze przed skyboxem. Aby odczytać kolor tekstury zamiast funkcji tex2D należy wykorzystać funkcję texCUBE, do której jako współrzędne tekstury przekazuje się pozycję danego pixela.

Jak widać nie jest to skomplikowane, a dzięki temu scena wygląda o wiele lepiej.

Swing czyli taniec z Javą

2009-10-26(3)

Tym razem postawiłem napisać o czymś, czym się aktualnie zajmuję na uczelni czyli programowaniem w Javie, a konkretniej programowanie interfejsu graficznego w Swingu. Java Swing jest w pełni obiektową (jak sam język) biblioteką graficzną służącą do majstrowania okienkowego GUI (podobnie jak QT, czy też Windows Forms). Korzystanie z tej biblioteki jest bardzo proste, niestety ma ona jedną podstawową wadę (oprócz bycia w Javie) – domyślnie okienko posiada własny style, więc nie wykorzystuje systemowego, dlatego może się wyróżniać.

Tworzenie okienek w Swingu jest bardzo proste, przykładowo wyświetlenie pustego okienka wygląda następująco:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import javax.swing.SwingUtilities;
import javax.swing.JFrame;
 
public class Okienko
{
   public static void main(String[] args)
   {
      SwingUtilities.invokeLater(new Runnable()
      {
         public void run()
         {
            JFrame f = new JFrame();
            f.setTitle("Hello World!");
            f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
            f.setSize(290, 250);
            f.setVisible(true);
         }
      });
   }
}

2009-10-26(4)

Ten krótki kod tworzy prostą formatkę JFrame, a następnie ustawia jej tytuł, rozmiar, akcje zamnięcia i na końcu pokazuje okienko. Wszystko to jest wykonywane w funkcji run() anonimowej klasy wewnętrznej po interfejsie Runnable (swoją drogą najpiękniejszy mechanizm w Javie :)), która działa w osobnym wątku biblioteki Swing. Jak widać jest to bardzo proste.

Co jednak gdy chcemy stworzyć coś bardziej wyszukanego? Wtedy najlepiej rozszerzyć (odziedziczyć) klasę JFrame. Mając własną klasę o wiele łatwiej wszystkim zarządzać wszystkimi komponentami, które znajdują się na formatce. Poniższy kod tworzy formatkę i wyświetla na niej przycisk i pole tekstowe:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import java.awt.FlowLayout;
import javax.swing.*;
 
public class Okienko extends JFrame
{
   private JButton bPisz = new JButton("Pisz");
   private JTextArea textArea = new JTextArea(10, 20);
 
   public Okienko()
   {
      setTitle("Hello World!");
      setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
      setSize(290, 250);
 
      setLayout(new FlowLayout());
      add(new JScrollPane(textArea));
      add(bPisz);
   }
 
   public static void main(String[] args)
   {
      SwingUtilities.invokeLater(new Runnable()
      {
         public void run()
         {
            new Okienko().setVisible(true);
         }
      });
   }
}

Powyższy kod jest już troszkę dłuższy, a jego zadanie polega na wyświetleniu na formatce przycisku oraz pola tekstowego. Pojawiły się tutaj dodatkowe komponenty takie jak: JButton, JTextArea, JScrollPane oraz funkcja setLayout(). Dwa pierwsze są oczywiste, bo jest to wspomniany przycisk i pole tekstowe, natomiast JScrollPane jest dodatkowym komponentem dodającym do JTextArea paski przesuwania (to wszystko przez proste opakowanie go). Funkcja setLayout() ustawia sposób rozmieszczenia kontrolek. Domyślnym układem jest BorderLayout, który kontrolki układa jedna na drugiej, natomiast FlowLayout ustawia je w taki sposób aby wszystkie były widoczne (po szczegóły zapraszam do dokumentacji :)).

2009-10-26(5)

No cóż mamy przycisk, ale teraz co zrobić, aby po jego naciśnięciu coś się stało. Obsługa akcji na kontrolkach opiera się na zdarzeniach, czyli w przypadku naciśnięcia przycisku wywoływana jest odpowiednia funkcja, która to zdarzenie potrafi obsłużyć. W takim razie jak obsłużyć akcję wciśnięcia przycisku? W tym celu należy dodać do przycisku odbiorcę zdarzenia za pomocą funkcji addActionListener(). Funkcja ta przyjmuje referencję do interfejsu ActionListener, z którego należy zaimplementować funkcję actionPerformed(). Właśnie ta funkcja zostanie wywołana w momencie wystąpienia zdarzenia.

Wynikowy kod:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import java.awt.FlowLayout;
import javax.swing.*;
import java.awt.event.*;
 
public class Okienko extends JFrame
{
   private JButton bPisz = new JButton("Pisz");
   private JTextArea textArea = new JTextArea(10, 20);
 
   public Okienko()
   {
      setTitle("Hello World!");
      setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
      setSize(290, 250); 
 
      bPisz.addActionListener(new ActionListener()
      {
         public void actionPerformed(ActionEvent e)
         {
            textArea.append("Hello World ");
         }
      });
 
      setLayout(new FlowLayout());
      add(new JScrollPane(textArea));
      add(bPisz);
   }
 
   public static void main(String[] args)
   {
      SwingUtilities.invokeLater(new Runnable()
      {
         public void run()
         {
            new Okienko().setVisible(true);
         }
      });
   }
}