Wczoraj zacząłem pisać tutaj wpis na temat postępów nad implementacją systemu mapowania inputu własnego pomysłu, a dokładniej o zrobieniu dodatkowej gałki analogowej z myszy. Niestety wpis wyszedł za długi zbyt skomplikowany, a do tego edytor się snów zesrał, więc piszę jeszcze raz.
Główna mechanika gry będzie (za pięć lat…) przystosowana do grania na kontrolerze lub klawiaturze — albo analog, albo zestaw czterech przycisków na kontrolerze (np. D-Pad), albo zestaw klawiszy na klawiaturze. Czyli typowo, tak jak to zwykle wygląda obecnie i tak jak to wyglądało w erze SNES-a. Jednak mnóstwo graczy gra w różne produkcje myszą (np. w MMORPG-i), więc postanowiłem dodać możliwość wykorzystania myszy do chodzenia bohaterem.
I tu jest pewien problem — gra będzie typu ”action adventure”, a nie ”point & click”, więc systemowy kursor podczas grania nie będzie widoczny. A nie będzie widoczny, bo grając nie będzie się klikało w obiekty znajdujące się na mapie. Jak więc wykorzystać mysz do sterowania? Cóż — zrobić z niej wirtualną gałkę analogową. :]
Skoro mysz ma działać jak gałka analogowa, musi dostarczać pozycję gałki — w formie wartości liczbowych (współrzędne, tak jak zwykle w obsłudze gałek analogowych), ale też przyda się kąt wychylenia (w radianach) oraz liczbowa wartość kierunku (jak w D-Padach). Dzięki temu gałka będzie mogła działać zarówno jako analog jak i D-Pad. W zależności od potrzeb, będzie można przesunąć postać bohatera albo płynnie o zadany kąt, albo w jednym z ośmiu kierunków (to będzie mógł sobie gracz skonfigurować).
Gałka na fizycznym kontrolerze dostarcza swoją pozycję w formie dwóch liczb X
i Y
jako SInt16
— to samo trzeba zrobić w przypadku wirtualnej gałki. Żeby to było możliwe, gałka musi posiadać określony, kwadratowy obszar, w którym punkt centralny oznacza gałkę wycentrowaną. Rozmiar obszaru musi być na tyle mały, aby mieścił się w oknie. Do tego przydadzą się pola CursorX
i CursorY
do przechowywania aktualnej pozycji gałki — na ich podstawie można łatwo policzyć kierunek wychylenia oraz kąt.
Gałka ta ma działać w ten sposób, że przesunięcie myszy w którąś stronę będzie przesuwać wirtualny kursor w obszarze gałki. Odpowiednio dalekie przesunięcie kursorsa od punktu centralnego spowoduje, że bohater zacznie się w tym kierunku poruszać po mapie i ten ruch będzie automatyczny. Aby dało się ten ruch przerwać, obszar musi posiadać martwą strefę w centralnej części (w formie koła) — jeśli kursor znajdzie się w tej strefie, to kierunek zostanie określony jako none
(odpowiednik puszczenia D-Pada/analogu na kontrolerze).
Struktura gałki wygląda tak:
type
TGame_WindowStick = record private
SizeStick: TGame_SInt32;
SizeDeadZone: TGame_SInt32;
RadiusStick: TGame_SInt32;
RadiusDeadZone: TGame_SInt32;
Center: TGame_SInt32;
CursorX: TGame_SInt32;
CursorY: TGame_SInt32;
Angle: TGame_Float;
Direction: TGame_SInt32;
Active: TGame_Bool;
end;
Oprócz danych wymienionych wyżej, jest jeszcze flaga Active
— jeśli gracz nie używa myszy do grania, to jej stan nie jest aktualizowany. Jak widać wyżej, struktura gałki nie zawiera informacji na temat okna, do którego przynależy — i nie powinna, bo to okno ma używać gałki, a nie na odwrót.
Każde okno w grze posiada wskaźnik na strukturę gałki oraz współrzędne jej obszaru względem okna-rodzica. Aby móc aktualizować stan gałki, należy wiedzieć w które miejsce jej obszaru przesunąć wirtualny kursor. Jak to zrobić? Wystarczy obsłużyć zdarzenie SDL_MOUSEMOTION
— po otrzymaniu informacji o ruchu myszy w obrębie okna, współrzędne systemowego kursora należy przeliczyć na relatywne współrzędne w obszarze gałki. Te współrzędne wpisuje się do pól CursorX
i CursorY
(przycięte do rozmiaru obszaru), a następnie zaktualizować kąt i kierunek gałki:
SDL_MOUSEMOTION:
if Game_WindowStickIsActive(Window^.Stick) then
begin
NewStickX := AEvent^.Motion.X - Window^.BoundsStick.X;
NewStickY := AEvent^.Motion.Y - Window^.BoundsStick.Y;
Game_WindowStickSetPosition(Window^.Stick, NewStickX, NewStickY);
end;
Obszar BoundsStick
posiada współrzędne obszaru gałki w oknie. U mnie jest to nieco bardziej skomplikowane, bo obszar klienta okna może być zgodny z rozmiarem okna, a może też być mniejszy. Wszystko dlatego, że klatka gry zawsze jest renderowana z zachowaniem proporcji 16:10
(zgodnych z tylnym buforem) — jeśli systemowe okno ma inne proporcje, to pojawiają się pasy po bokach lub na górze i dole:
Skoro systemowy kursor podczas grania jest niewidoczny, to gracz nie widzi gdzie on jest. To powoduje, że łatwo jest ruszyć wyjechać kursorem poza okno i kliknąć np. w pulpit lub okno innego programu — a to dezaktywuje okno gry i zdenerwuje gracza. Dlatego też trzeba kontrolować pozycję systemowego kursora w oknie.
Aby ograniczyć ruch kursora do konkretnego okna lub jego obszaru, SDL posiada funkcję SDL_SetWindowGrab
— podanie wartości SDL_ENABLE
powoduje, że nie będzie się dało wyjechać kursorem poza wewnętrzny obszar okna. Działa to bez względu na styl okna, a więc jeśli jest malutkie, ale też w trybie pełnoekranowym oraz ekskluzywnym trybie wideo. Jeśli wirtualny analog jest nieaktywny (czyli jeśli gracz obsługuje zasobnik z przedmiotami), to kursor jest utrzymywany w obszarze klienta okna.
A co jeśli analog jest włączony? Wtedy systemowy kursor jest ukrywany i obszar po którym kursor może się przemieszczać jest zmniejszany do rozmiaru obszaru wirtualnej gałki, czyli niewielkiego kwadratu po środku okna. Aby móc ustalić własny obszar po którym może się poruszać kursor w oknie, należy wykorzystać funkcję SDL_SetWindowMouseRect
.
Podczas aktywacji gałki, systemowy kursor jest ukrywany i przenoszony w takie miejsce w oknie, aby było zgodne z współrzędnymi wirutalnego kursora w obszarze gałki. Jeśli gracz włączy zasobnik z przedmiotami, a tym samym dezaktywuje wirtualną gałkę, systemowy kursor jest pokazywany w centralnym punkcie okna — dzięki temu gracz nie będzie musiał pamiętać gdzie był kursor podczas obsługi zasobnika.
Centrowanie kursora podczas wyłączania gałki jest tymczasowe — w przyszłości to zachowanie może być zmienione, aby współpracowało z kanapowym trybem kooperacji. No bo nie może być tak, że dwóch graczy gra na jednym ekranie i ten który gra myszą, jeździ kursorem po całym oknie — to będzie drugiemu graczowi przeszkadzać.
A co jeśli chcemy mieć możliwość zmiany czułości gałki? A to akurat proste — im wyższa czułość, tym mniejszy rozmiar obszaru gałki. I to samo w drugą stronę — im większy rozmiar obszaru gałki, tym dłuższą drogę musi pokonać kursor, a więc tym niższa czułość.
Tu jest pewien problem, bo okno gry można zmniejszyć do rozmiaru tylnego bufora (czyli do 358×224
), a rozmiar gałki może być znacznie większy. Aby zapobiec problemom ze sterowaniem, okno przechowuje aktualny rozmiar gałki (ustalony przez gracza), ale jeśli jest on większy niż obszar klienta okna, to go przycina — ten przycięty rozmiar przekazuje gałce.
Niżej krótki gif prezentujący działanie gałki oraz kontrolę ruchu i widoczności systemowego kursora:
Różnokolorowe obszary widoczne na animacji służą jedyne do debugowania kodu okna i analogu.
To co mam obecnie na razie wystarczy, ale w przyszłości dodam jeszcze jedną rzecz. Aby łatwiej było kontrolować w końcu niewidoczny kursor w obszarze gałki, dodam automatyczne przyciąganie wirtualnego kursora do konkretnych punktów wewnąrz obszaru gałki — punktów ośmiu głównych kierunków oraz do centrum. Dzięki temu mikro-ruchy myszą (wynikające np. z mocniejszego klikania) nie będą powodować przypadkowej zmiany kierunku bohatera. Dopiero mocniejszy ruch myszą będzie powodował zmianę kierunku.
Na razie tego nie implementuję — nie ma jeszcze silnika, więc nie mam za bardzo jak tego ficzera testować i kalibrować.
Obecnie cisnę fundamenty gry, aby zapewnić sobie jak najwięcej możliwości, jeśli chodzi o tryby okna oraz mapowanie inputu. Na razie nie ogłaszam się z tym za bardzo, ale po przygotowaniu wstępnej wersji silnika gry (co zajmie mi jeszcze kilka miesięcy), chciałbym zająć się tworzeniem tej gry na pełny etat. Planuję więc w niedalekiej przyszłości skorzystać z Patronite i prowadzić vlog na YouTube i nie tylko pierdzielić o postępach, ale też omawiać bardziej szczegółowo ”flaki”, mówiąc co i dlaczego robię (bez owijania w bawełnę), jak to ma wpływać na gameplay i rozrysowywać to wszystko w jakimś programie (podobnie jak to robi np. One Lone Coder). No i oczywiście prowadzić swojego Discorda, coby mieć stały kontakt z patronami i społecznością.
Przy okazji, tytuł okna zawiera docelową nazwę gry — ta będzie się nazywać Project: Kids, a pierwsza odsłona nosić będzie slogan When the Nightmare Comes. Nie chcę spoilerować o fabule za wczasu (skoro g**no jeszcze mam), ale w grze będzie się sterowało dzieciakiem (lub kilkoma, w trybie kooperacji, bez split screenu) i robiło rozpierdziel na półotwartej mapie, walcząc z kosmitami-najeźdźcami w post-apokaliptycznym świecie, będącym efektem koszmaru głównego bohatera, w jedną z burzliwych nocy. Mechanika ma zapewniać sporo możliwości, zapewniając bogaty wachlarz funkcji dotyczących sterowania. Całość oczywiście w formie pikselartowej, ale ze wsparciem obracania kamery.
Więcej informacji w mam nadzieję niedalekiej przyszłości. ;)
@cerrato: kilka miesięcy jeszcze minie.
Po ukończeniu systemu mapowania inputu, zajmę się programowaniem UI (mini-LCL), czyli systemem kontrolek i ich obsługą z poziomu urzadzeń wejścia. Potem zabiorę się za obsługę fontów, bo ta istniejąca w SDL-u posysa (renderowanie tekstu jest niemiłosiernie powolne), dlatego skorzystam z tekstur zamiast plików .ttf
, bo mogę. Potem zajmę się obsługą poszczególnych scen i zbuduję tymczasowe główne menu gry, aby zobaczyć efekty działania UI oraz aby podłączyć menu pod mapowanie inputu.
Kiedy już będę miał powyższe, będę mógł zabrać się za silnik gry, a więc będzie co w końcu pokazać w oknie. Wbrew pozorom, wszystko co wymieniłem nie będzie problematyczne w zaprogramowaniu — ani fonty, ani UI, ani sceny, ani menu. Silnik sam w sobie też nie będzie nieskomplikowany — w przeciwieństwie do jego renderera, bo zamierzam wykorzystać software'owy ray tracing. Choć sam ray tracing też będzie znacznie łatwiejszy w implementacji, niż standardowy renderer rasteryzujący — taki paradoks. :D
@furious programming: ogólnie to stworzenie większości rzeczy nie jest problematyczne, tylko pracochłonne. Poza tym - problematyczne jest spięcie tych wszystkich drobnych kawałeczków w jedną, sensowną i działającą całość.
dlatego skorzystam z tekstur zamiast plików .ttf, bo mogę
Od dawna gierki sobie generowały tekstury czcionek z *.ttf ;)
https://nehe.gamedev.net/tutorial/freetype_fonts_in_opengl/24001/
@Spine: tutaj bardziej mi chodziło o to, że mogę sobie stworzyć fonty jako zestawy tekstur, dlatego że rozdzielczość tylnego bufora jest zawsze stała — dokładnie 358×224
piksele (rozdzielczość pionowa zgodna z NES/SNES). Każdy znak podstawowego fontu będzie miał 8×8
pikseli, co elegancko pasuje do pikselartowej oprawy i bardzo łatwo się takie fonty obsługuje (brak kerningu itp.).
Po drugie, fonty nie będą jednokolorowe — nie tylko będą posiadać czarną otoczkę (kontrastującą z dowolnym tłem), ale również będą mogły posiadać zawartość gradientową czy jakąkolwiek inną. Aby renderować takie literki w dowolnym kolorze, wystarczy ustawić kolor modulacji renderera (funkcją SDL_SetTextureColorMod
) i gotowe. Niektóre fonty (głównie ozdobne, np. na potrzeby menu) będa posiadały stałą zawartość i nie będą modulowane podczas renderowania.
Natomiast powód nieużywania fontów typu .ttf
jest taki, że SDL nie wspiera ich renderowania bezpośrednio na teksturach. Aby wyrenderować tekst, SDL najpierw tworzy powierzchnię software'ową (surface
), a więc alokuje pamięć (a to ssie), następnie trzeba powierzchnię przekonwertować na teksturę (co znów wymaga alokacji pamięci i transferu danych do pamięci GPU) i dopiero wtedy można ją gdzieś wyrenderować. A potem trzeba zwolnić pamięć pomocniczej powierzchni i tekstury — kolejne marnowanie mocy obliczeniowej.
Lekko licząc, renderowanie znaków w formie osobnych tekstur jest setki razy szybsze od funkcji z modułu SDL_ttf
. :D
Kiedy będą pierwsze screeny z jakąś grafiką? Żeby mieć jakąkolwiek wizję jak to będzie wyglądać?