Piszę sobie dalej kod mappera inputu w swojej grze. Dobrze idzie, coraz bliżej ukończenia tego modułu. Rozgryzłem to w jaki sposób stworzyć wygodne API do sterowania ruchem bohatera. Założenie jest takie, że z jednej strony mamy kupkę urządzeń wejściowych (mysz, klawiatura i bug wie jakie kontrolery USB), a z drugiej strony mamy bohatera, który ma się ruszać zgodnie z tym co użytkownik wciśnie w przypisanym do akcji ruchu urządzeniu.
Pierwszy problem — do ruchu można wykorzystać dowolny ”manipulator” na dowolnym urządzeniu. Może to być ruch myszą; mogą to być cztery przyciski, np. WASD
na klawiaturze, D-Pad lub jakiekolwiek inne przyciski na kontrolerze; może to też być gałka analogowa. Tutaj jest pewne podobieństwo — ruch myszy można reprezentować tak jak gałkę analogową. Zmodyfikowałem więc kod wirtualnej gałki analogowej, tak aby użyć jej zarówno do reprezentacji myszy, jak i gałek analogowych dowolnych kontrolerów USB. Oczywiście w przypadku gałek kontrolerów, są one wirtualnymi manipulatorami, bo API systemowe i SDL dostarcza dane poszczególnych osi, a nie ich par (i dobrze).
Jak wyżej pisałem, bez względu na sposób zmapowania inputu przez gracza, jego akcje muszą być odczytane z urządzeń wejścia i przetłumaczone na kilka danych — kąt ruchu (w radianach), dystans (z przedziału <0.0,1.0>
, jako mnożnik) oraz dodatkowo też jeden z ośmiu głównych kierunków (na potrzeby menu). Zaprogramowałem wszystko, dodałem sobie jeszcze możliwość ustawiania rozmiaru martwej strefy — do grania myszą potrzeba dużej martwej strefy, do grania gałką analogową malutkiej. Przyszedł czas na dodanie opcji ustawiania precyzji ruchu kursora na potrzeby grania myszą — i tu problem.
Zmienna precyzja ruchu kursora to nic innego jak mnożenie relatywnego ruchu kursora o jakąś wartość zmiennoprzecinkową. Aby kursor poruszał się wolniej, mnożnik musi być mniejszy od 1.0
, jeśli szybciej, to większy od 1.0
— np. 0.5
da ruch dwa razy wolniejszy, 2.0
da dwa razy szybszy. Nie jest to jakimś wielkim kłopot, bo SDL w zdarzeniach SDL_MOUSEMOTION
dostarcza zarówno nowe współrzędne kursora, jak i relatywne zmiany pozycji kursora, jednak nie da się ich wykorzystać. Wszystko dlatego, że jeśli kursor jest trzymany w obrębie okna dzięki SDL_SetWindowGrab
, to gdy ten dotyka krawędzi okna i rusza się myszą nadal w tym kierunku, to kursor nie zmienia pozycji, a więc zdarzenie SDL_MOUSEMOTION
dostarcza wartość 0
w polu xrel
/yrel
.
Skorzystałem więc z relatywnego trybu myszy — słuzy do tego funkcja SDL_SetRelativeMouseMode
. Jej włączenie powoduje, że kursor jest ukryty i porusza się wyłącznie w obrębie klienta okna (wołanie SDL_SetWindowGrab
i ukrywanie kursora nie są już potrzebne). Istotną różnicą jest to, że bez względu na to gdzie jest kursor w oknie i w którą stronę się go przesuwa, pola xrel
i yrel
w zdarzeniach SDL_MOUSEMOTION
dostarczają niezerowe wartości kroków w sposób ciągły — win. Zmodyfikowałem więc kod wirtualnej gałki, aby jej pozycja była zmieniana o zadany krok i dodałem obsługę zmiany precyzji. Działa elegancko, ale jest nowy problem.
Jeśli włączę relatywny tryb myszy, to przy mnożniku równym 1.0
(a więc precyzja domyślna), kursor zapierdziela wielokrotnie szybciej niż w trybie podstawowym. Nie wiedziałem dlaczego tak się dzieje. Sprawdziłem kod czy może gdzieś obliczenia są popsute, ale nie. W Google też nic na ten temat nie znalazłem. Dodałem więc zgłoszenie w repo SDL-a pytając co jest z tym trybem nie tak — może bug gdzieś się wkradł? Powód okazał się trywialny.
Otóż w podstawowym trybie obsługi myszy, prędkość kursora uzależniona jest od ustawień w OS-ie użytkownika, a w relatywnym trybie myszy, zdarzenia dostarczają dane surowe. W panelu sterowania mam ustawioną bardzo niską prędkość (wartość 2
), co jest około trzy razy wolniejsze od prędkości domyślnej — dlatego mój wirtualny analog tak popierdziela.
Teoretycznie nie jest to bug, ale za to spore ograniczenie, dlatego do SDL-a zostanie dodany nowy hint — SDL_HINT_MOUSE_RELATIVE_SYSTEM_SCALE
— dzięki któremu będzie można odblokować skalowanie ruchu myszą, zgodne z ustawieniami systemowymi użytkownika. Czyli w tym momencie wszystko gra, prawda? Nie — jest kolejny problem. :D
Jeśli ustawi się niską prędkość w mojej wirtualnej gałce analogowej, to powolny ruch myszą w ogóle nie rejestruje ruchu i kursor się nie rusza. Nie jest to dziwne. Jeśli mnożnik precyzji ma wartość np. 0.2
, to przy powolnym ruchu myszą, zdarzenie dostarcza wartość 1
lub 2
w polach xrel
/yrel
— po przemnożeniu dostajemy wartości mniejsze niż piksel, a po zaokrągleniu wychodzi 0
(brak zmiany położenia kursora). Aby więc nie tracić mikroruchów myszą, dodałem sobie zmiennoprzecinkowe akumulatory do struktury analogu:
type
TGame_Stick = record private
SizeStick: TGame_SInt32;
SizeDeadZone: TGame_Float;
Precision: TGame_Float;
AccumulatorX: TGame_Float; // ło
AccumulatorY: TGame_Float; // tu
RadiusStick: TGame_Float;
RadiusDeadZone: TGame_Float;
RadiusXY: TGame_Float;
X: PGame_State;
Y: PGame_State;
Angle: PGame_State;
Distance: PGame_State;
Direction: PGame_State;
Active: TGame_Bool;
end;
i wykorzystałem do gromadzenia subpikselowych ruchów:
procedure Game_StickMoveBy(AStick: PGame_Stick; AX, AY: TGame_SInt32);
var
NewAccumulatorX: TGame_Float;
NewAccumulatorY: TGame_Float;
NewX: TGame_SInt32;
NewY: TGame_SInt32;
begin
// dolicz kroki w formie subpikselowej do wartości akumulatorów
NewAccumulatorX := AStick^.AccumulatorX + AX * AStick^.Precision;
NewAccumulatorY := AStick^.AccumulatorY + AY * AStick^.Precision;
// użyj części całkowitej kroków do ukreślenia nowej pozycji gałki analogowej
NewX := PGame_SInt32(Game_StateGetCurrent(AStick^.X))^ + Trunc(NewAccumulatorX);
NewY := PGame_SInt32(Game_StateGetCurrent(AStick^.Y))^ + Trunc(NewAccumulatorY);
// zachowaj jedyne część dziesiętną w akumulatorach
AStick^.AccumulatorX := Frac(NewAccumulatorX);
AStick^.AccumulatorY := Frac(NewAccumulatorY);
// ustaw gałkę w zadanej pozycji
Game_StickMoveTo(AStick, NewX, NewY);
end;
I teraz wszystko działa poprawnie.
Skoro mam już zaprogramowaną precyzję na podstawie relatywnego trybu myszy oraz mnożnika precyzji, kod odpowiedzialny za zmianę rozmiaru obszaru analogu mogłem już usunąć. Obszar ten zawsze jest stały (i nieduży), dzięki czemu zmiana rozmiaru okna na malutki nie wpłynie na sterowanie myszą.
Po tym wszystkim, skorzystałem z implementacji wirtualnej gałki do reprezentacji par osi w kontrolerach. Różnica jest taka, że dla myszy rozmiar obszaru wynosi 224px
(wysokość tylnego bufora), natomiast dla gałek kontrolerów jest to rozmiar 0xFFFF
— dlatego, że pozycja osi w kontrolerach to wartość 16-bitowa (co prawda ze znakiem, ale tę mapuję sobie na przedział bez znaku).
Koniec końców, jedna implementacja wirtualnej gałki analogowej została z powodzeniem użyta dla myszy oraz kontrolerów USB i we wszystkich przypadkach zapewnia to czego będę potrzebować do sterowania bohaterem, czyli kąta ruchu, dystansu i opcjonalnie kierunku. Niczego nie będę musiał obliczać ani w mapperze, ani tym bardziej w obsłudze logiki gry, bo wszystkie dane dostarcza strukturka wirtualnej gałki.
@furious programming: prawda, usmiealem sie ze to tyle trwa :) ale jeszcze nie jestes gotowy na moje opinie przyjacielu w Chrystusie
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. ;)
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
@chomikowski: cieszę się Twoim szczęściem. ;)
Mapper z obsługą wymaganych urzadzeń wejścia, nad którym obecnie pracuję, to dość skomplikowany moduł, którego implementacja wymaga dużo czasu i wysiłku. Pamiętaj, że mapper nie jest jedyną rzeczą, którą teraz programuję, i też o tym, że nie poświęcam na ten projekt 10h dziennie, aby robota szła sprawnie. Wiele rzeczy muszę najpierw rozgryźć i przetestować, zanim napiszę właściwy kod we właściwym projekcie. Szczególnie, że wiele z nich jest rzadko spotykanych w grach, ze względu właśnie na stopień skomplikowania i czasochłonność implementacji.
Programowanie to nie rąbanie drewna — nie wystarczy machać rękami szybciej, aby szybciej skończyć zadanie.
Widząc słowa z tego komentarza — „Jeszcze piszesz ten kod? ja juz zdazylem wypuscic 5 projektow w swiat a ty ciagle te guziki robisz hahahaha? w ogole skonczysz to do 2025?” — spokojnie można uznać, że naśmiewasz się ze mnie i z tempa mojej pracy nad tym projektem. Ja tam się nie obrażam, ale wyluzuj trochę, bo celuję nie w ilość, a w jakość. ;)