Poradniki do tworzenia GUI

0

Witam. Poszukuję proadników Tworzenia GUI. Najlepiej jakby obejmowały:
-pole tekstowe, pole tekstowe z scrollbarem
-przycisk, przycisk z obrazem, przycisk z tekstem
-lista przycisków z scrollbarem

Obecnie piszę w C++ oraz SFML więc poradniki w tym języku oraz tej bibliotece byłyby najbardziej pożądane

2

Potrzebujesz poradników na temat tego jak zaprogramować system kontrolek, czy o tym jak zbudować GUI za pomocą istniejących bibliotek do tego celu? Doprecyzuj.

0

Potrzebuję poradników jak zaprogramować system kontrolek GUI przy pomocy biblitoeki graficznej takiej jak np. SFML

2

To jest tak samo jakbyś programował grę.
Musisz się dowiedzieć gdzie na ekranie jest myszka.
Dla wszystkich obiektów GUI sprawdzasz kolizję - czy pozycja myszki znajduje się w ich koliderach.
Kiedy kliknięto myszką, to wywołujesz funkcję przypisaną do obiektu GUI, do którego należy kolider.

Ogólnie to odradzam SFML. To nie jest technologia, w którą warto inwestować swój czas.
Żeby to ładnie działało i było użyteczne, trzeba wykonać znacznie więcej pracy, niż w językach/technologiach wyższego poziomu.
W Unity + C# zrobisz to sprawniej, a może nawet będzie Ci pasowało zastosowanie systemu GUI dostarczanego z silnikiem:

Mi nie pasowało, więc zrobiłem własne. Tak jak to opisałem na początku tego posta.

2

Teoretycznie, stworzenie prostego UI ze wsparciem wymienionych kontrolek nie jest zadaniem trudnym, jeśli potrzebujesz maksymalnie prostego rozwiązania. System kontrolek, który by miał być elastyczny i uniwersalny, zwykle jest bardzo złożony i trzeba sporo kodu naklepać, aby dało się go użyć do budowy dowolnego UI, dowolnie oskórkowanego.

Kilka miechów temu pisałem system kontrolek we własnym silniku. Nie miał być uniwersalny (jak ImGui), więc miałem łatwiejsze zadanie. Mimo wszystko bardzo dużo kodu musiałem napisać, aby kernel UI obsługiwał input (mycha i kursor, klawiatura i dowolne kontrolery), na podstawie inputu generował zdarzenia UI i aktualizował stan kontrolek, a także abym mógł pod taki kernel podłączyć kontrolki dowolnego typu, rozszerzające funkcjonalność bazowego typu kontrolek (bez ingerencji w kod kernela UI). Zajęło mi to kilkaset godzin klepania, jakieś 3-4 tysiące linijek kodu.

Wątpię też, abyś był w stanie zaprogramować jakiś prosty system kontrolek — gdybyś mniej więcej wiedział jak taki system powinien działać i jak wyglądać, to raczej byś pytań o tutoriale nie zadawał. Jeśli faktycznie chcesz coś takiego zaprogramować, to zacznij od czegoś baaardzo prostego, czyli prostych przycisków. Jednak zanim zabierzesz się za klepanie kodu, najpierw dobrze przemyśl architekturę — to jakie funkcje UI będą potrzebne, jakimi urządzeniami wejścia ma być obsługiwane, w jaki sposób stan kontrolek ma być aktualizowany, jakie dane mają reprezentować kontener UI i kontrolki itd.

4

Dobra, skrobnę ci mały tutorial, taki w zajebiście dużym skrócie, bo to temat rzeka, na wiele godzin gadania. Ogólne rzeczy, które musisz przemyśleć, zanim zacznie się klepać kod. Każdy z punktów zawiera opis tego nad czym warto się zastanowić oraz przykład z mojego silnika, czyli jak w moim silniku działa UI.

Oczywiście nie jestem w temacie UI jakiś ekspertem czy autorytetem, więc poniższe traktuj jako jeden z wielu możliwych punktów widzenia. Zaznaczam też, że poniższe dotyczy implementacji UI w trybie Retained, czyli zawartość UI jest reprezentowana przez zestaw obiektów, zamiast być generowana w locie (jak ImGui, działający w trybie Immediate).


Jak ma być reprezentowane UI?

Zastanów się czy wystarczą ci luźne kontrolki, czy chciałbyś mieć kontener do ich grupowania i zarządzania. Możesz mieć po prostu listę kontrolek, ale możesz też mieć obiekt, który będzie mógł reprezentować np. cały ekran lub jego zadany fragment, albo wirtualne okienko. Jeśli chcesz mieć kontener, to zastanów się też nad tym, czy w danym momencie tylko jeden może być aktywny, czy może ich być wiele.

Sam skorzystałem z prostego kontenerka, który posiada kilka właściwości oraz listę kontrolek. Kontener ten służy wyłącznie do grupowania kontrolek i ich aktualizowania — nie zawiera danych na temat swojego wyglądu, nie zajmuje się renderowaniem ani siebie, ani zawartych w sobie kontrolek.


Jakiego typu kontrolki mają być obsługiwane?

Pasuje się najpierw zastanowić nad tym, czy chcesz mieć tylko mały, hardkodowany zestaw nieelastycznych kontrolek, czy może mieć możliwość łatwego tworzenia kontrolek nowego typu i ich podpinania pod UI. Jeśli nie wiesz czy w przyszłości nie będziesz potrzebował nowych kontrolek lub dla niektórych przypadków redefiniować zachowanie wybranych instancji, idź w kierunku rozwiązania łatwego do rozszerzania, abyś nie musiał hakować własnego kodu i produkować spaghetti.

U siebie zrobiłem w ten sposób, że stworzyłem typ bazowy kontrolki, który jest jedynym, jaki zna kontener UI. W dowolnym momencie mogę wziąć taki typ bazowy i na jego podstawie stworzyć nowy typ kontrolki, a następnie dodawać jej instancje do kontenerków. Polimorfizm robi tutaj robotę. Aby kontener UI mógł aktualizować kontrolki, których typów nie zna, ten bazowy typ kontrolki posiada funkcje umożliwiające komunikację z kontrolką oraz jej aktualizowanie. Głównie mam tutaj na myśli funkcję Respond, służącą komunikowania się aktualizatora z kontrolką oraz o funkcję Update, która popycha animację kontrolki. Rozszerzone (końcowe) typy kontrolek nadpisują te funkcje i w ten sposób mogą instruować aktualizator UI jak z tej kontrolki korzystać.


Kto może obsługiwać UI?

Jeśli twoja gra ma zawierać UI, które może być kontrolowane z poziomu dowolnych urządzeń inputu, to zastanów się czy wprowadzić tutaj ograniczenia. Jeśli np. przeznaczona jest do grania przez kilku graczy jednocześnie, to czy UI może być kontrolowane przez wszystkich graczy i wszystkie urządzenia, czy tylko przez wybranego gracza i/lub wybrane urządzenia. Dodatkowo, jeśli w danym momencie, na ekranie może być więcej niż jeden kontener, należy określić kto może który kontener obsługiwać i czy w ogóle (np. HUD zwykle nie jest interaktywny).

U mnie kontener UI posiada informacje o tym, czy jest przeznaczony do obsługi urządzeniami wejścia, czy jest kontenerem statycznym (np. HUD, który zwykle nie jest interaktywny). Jesli kontener jest interaktywny, to dodatkowo posiada informacje o tym, czy może być sterowany przez dowolnego gracza, czy tylko przez wybranego. Np. wszystkie kontenery służące do budowy głównego menu gry mogą być sterowane przez dowolnego gracza. Zasobnik z przedmiotami może być obsługiwany tylko przez konkretnego gracza — w przypadku lokalnego co-opa, dwóch graczy może jednocześnie dłubać w swoich zasobnikach i jeden drugiemu nic nie poprzestawia. HUD natomiast nie jest interaktywny, więc w ogóle nie sprawdza inputu.


W jaki sposób obsługiwać UI?

UI może być sterowane za pomocą predefiniowanego zestawu klawiszy, przycisków myszy oraz triggerów gamepadów, ale może też być sterowane na podstawie mappingu urządzeń użytwanych przez graczy. Jeśli twoja gra ma dawać graczowi możliwość zmiany ustawień sterowania (a powinna), to musisz się zastanowić nad tym, czy ten sposób rozróżniać i jeśli tak, kontener UI powinien zawierać takie informacje oraz na ich podstawie przeprowadzać aktualizację kontrolek.

U mnie dany kontener UI rozróżnia sposób sterowania i w trakcie aktualizacji albo testuje konkretne klawisze, stan myszy oraz uniwersalne triggery wszystkich dostępnych kontrolerów (D-Pad, analogi i konkretne przyciski), albo za pośrednictwem mappera sprawdza czy konkretna funkcja jest użyta (np. atak czy skok) i ją tłumaczy na akcję UI. W ten sposób kontenery UI używane do budowy menu mogą być wygodnie obsługiwane dowolnymi urządzeniami i intuicyjnymi klawiszami/triggerami (np. strzałki, Enter, Esc, D-Pad, analogi). Natomiast kontenery używane podczas rozgrywki — np. z zasobnikiem przedmiotów — mogą być kontrolowane tymi klawiszami/triggerami, których faktycznie gracz używa do grania (nieważne do którego urządzenia i którego triggera są przypisane).


Obsługiwać mysz?

Są gry, w których podczas rozgrywki używa się myszy do sterowania i kursor jest cały czas widoczny (np. jakieś strzelanki czy strategie), ale są i takie, w których myszy się nie używa i kursor myszy nigdy nie jest widoczny (np. jakieś retro gierki). Jeśli mysz nie jest przeznaczona do grania w twoją grę, to jej obsługi nie musisz implementować, choć nic nie stoi na przeszkodzie, aby dodać jej wsparcie i móc za jej pomocą obsługiwać wszystkie menu gry (poza rozgrywką). Jeśli chcesz dodać wsparcie myszy, to pamiętaj, aby aktualizować UI najpierw na podstawie inputu myszy — ta powinna mieć najwyższy priorytet.

W moim silniku jest wsparcie myszy i można grać myszą, mimo że rozgrywka nie opiera się o pozycję kursora — to nie jest point-and-click. Mysz używana jest jako wirtualna gałka analogowa. Dlatego też dodałem jej wsparcie w kontekście UI, można za jej pomocą sterować UI, zarówno menu główne, jak i kontenerki dostępne w trakcie rozgrywki. Aktualizator UI najpierw bierze pod uwagę input myszy, a dopiero później sprawdza klawiaturę i gamepady (bezpośrednio lub pośrednio przez mapper). Niektóre akcje aktualizacji UI za pomocą myszy nie mogą być przerwane, więc w takich przypadkach input klawiatury i gamepadów jest ignorowany, ale o tym w kolejnym punkcie.


Jakie funkcje myszy należy uwzględnić?

Przede wszystkim ruch kursora, który powinien służyć do przenoszenia fokusu (aktywować kontrolkę pod kursorem) i jego usuwania (kursor nad pustym miejscem to dezaktywacja kontrolek). Po drugie, lewy przycisk myszy, który powinien służyć do wybierania opcji (akceptacji kontrolki) oraz opcjonalnie prawy przycisk myszy, służący do powrotu do poprzedniego menu (wygodny zamiennik klawisza Esc). Jeśli przewidujesz menu, które mogą się nie mieścić na ekranie, to podstawowa rolka myszy musi umożliwiać wygodne scrollowanie. To są podstawy. Możesz też dodać funkcję automatycznego pokazywania i ukrywania kursora — jeśli gracz wciśnie coś na klawiaturze, to kursor się chowa, a jeśli ruszy myszą lub kliknie, to kursor się pojawia. Nie jest to trudne w implementacji.

Z rzeczy bardziej zaawansowanych, pasuje obsłużyć tzw. mouse capture, czyli zawłaszczanie inputu myszy przez aktywną (sfokusowaną) kontrolkę, nawet jeśli kursor wyjedzie poza jej obszar. Załóżmy, że chcesz mieć suwak do ustawiania poziomu głośności — jeśli gracz umieści kursor nad kontrolką, wciśnie lewy przycisk myszy i trzymając go, wyjedzie kursorem poza obszar kontrolki, kontener UI cały czas powinien dostarczać tej kontrolce dane i jej nie dezaktywować. Dopiero kiedy gracz puści lewy przycisk, mouse capture powinien być dezaktywowany, a zmiany w kontrolce zaakceptowane.

Mouse capture z reguły przewiduje też możliwość anulowania zmian w kontrolce — jeśli w trakcie trzymania lewego przycisku i przeciągania, użytkownik wciśnie prawy przycisk myszy lub klawisz Esc, mouse capture również powinien być dezaktywowany, ale zmiany wprowadzone w kontrolce powinny być odrzucone. Oznacza to, że anulowanie capturingu cofa zmiany w kontrolce (np. pozycję suwaka do tej sprzed rozpoczęcia capturingu) i kontrolka nie powinna sygnalizować zmian (czyli np. emitować zdarzenia zmiany pozycji suwaka). No i oczywiście w trakcie capturingu, input z klawiatury i gamepadów powinien być ignorowany, tak aby aktualizator nie gryzł się sam ze sobą.

Wszystko powyższe zaimplementowałem w swoim silniku i mogę powiedzieć tyle, że zaprogramowanie tego wszystkie jest czasochłonne, skomplikowane, a przypadków brzegowych jest od zajebania, więc skuś się na taką implementację tylko jeśli masz dużo czasu i anielską cierpliwość. 😉


Jak aktualizować stan kontenera i kontrolek na podstawie inputu?

Mając zestaw zdarzeń dotyczących inputu w danej klatce gry, aktualizator powinien przekładać je na konkretne akcje UI. Przy czym jeśli chcesz mieć obsługę myszy, to aktualizator najpierw powinien sprawdzać input myszy, zanim zabierze się za klawiaturę i gamepady. Zdarzenia inputu powinny być wyłapywane przez aktualizator UI, a następnie na podstawie ich typu, powinien aktualizować stan kontrolek oraz swoje własne informacje — np. która kontrolka jest aktualnie aktywna. Aktualizacja UI powinna owocować nie tylko aktualizacją kontrolek, ale też produkować listę zdarzeń na temat tego, co zostało zrobione w UI — np. kliknięto w przycisk CANCEL — i ta lista zdarzeń UI powinna być dostarczona do głównej funkcji aktualizującej logikę gry. Te rzeczy powinny być odseparowane, aby były wygodne w obsłudze.

Aktualizator może pchać te zdarzenia do aktywnej kontrolki, ale może też komunikować się samodzielnie z kontrolkami, przekazując im własne zdarzenia (po prostu dedykowane paczki danych). Te zdarzenia nie tylko powinny informować daną kontrolkę o tym co się stało (np. kursor zmienił pozycję), ale też mogą służyć do odpytywania kontrolki i uzyskiwanie od niej odpowiedzi. Taka forma dwustronnej komunikacji pozwala instruować aktualizator UI z poziomu kodu kontrolki, bez ingerencji w kod samego aktualizatora — genialna sprawa. Dodatkowo, kontrolka po otrzymaniu zdarzenia od aktualizatora UI, sama może dorzucić własne zdarzenia do kolejki, które później zostaną przekazane logice.

Natomiast po przetworzeniu wszystkich zdarzeń inputu i zaktualizowaniu na ich podstawie kontenera oraz kontrolek, wszystkim kontrolkom należy popchnąć animacje. To musi być wykonane na koniec aktualizacji UI i tylko raz w danej klatce gry. Finalnie, wszystkie akcje wykonane w UI powinny być dostarczone logice gry.

W moim silniku, wszystkie zdarzenia są przetwarzane przed aktualizacją logiki — dane inputu są dostarczane do obiektów reprezentujących mysz, klawiaturę i gamepady. W trakcie aktualizacji logiki gry, każdemu kontenerowi UI wywołuje się metodę aktualizującą ich stan. Aktualizator najpierw sprawdza stan myszy i na podstawie zmian generuje własne zdarzenia i przesyła je do kontrolek. W ten sposób po pierwsze mogą one zaktualizować swój stan (np. bo otrzymała fokus, więc odpala sobie animację), a po drugie, mogą odpowiedzieć i poinstruować aktualizator UI co może z tą kontrolką zrobić. Po obsłudze myszy, to samo robi się z inputem klawiatury i gamepadów — tworzy zdarzenia UI, dostarcza je odpowiednim kontrolkom i ewentualnie sprawdza ich odpowiedzi. Kontrolki w trakcie otrzymywania zdarzeń od aktualizatora UI mogły dorzucać własne zdarzenia do kolejki zdarzeń UI. Po przetworzeniu inputu, każdej kontrolce wywołuje się metodę popychającą ich animacje. Na koniec, jeśli kolejka zdarzeń UI nie jest pusta, wszystkie te zdarzenia po kolei przekazywane są do callbacku, dzięki czemu logika gry widzi jakie akcje zostały wykonane w UI i może je obsłużyć. Wcześniej pisałem o swoich funkcjach Respond oraz Update — pierwsza służy do komunikacji z kontrolkami, przekazywania im zdarzeń i opcjonalnie uzyskiwania od nich odpowiedzi, a ta druga służy do pchania animacji kontrolek.

To co opisałem wyżej jest mniej więcej tym, w jaki sposób działa Win32 API — system wysyła komunikaty do aplikacji, ta je przetwarza, opcjonalnie odpowiada lub generuje własne komunikaty. W przypadku gry jest podobnie, tyle że zamiast kernela Windowsa jest aktualizator UI, a zamiast aplikacji jest kontrolka. Komunikacja pomiędzy nimi jest dwustronna, bazuje na dostarczaniu danych i opcjonalnym uzyskiwaniu odpowiedzi.


A co z renderowaniem?

Możesz to zrobić jak chcesz — wyposażyć kontener oraz kontrolki w metody Render, albo zaimplementować renderowanie kontrolek gdzieś indziej. Sam jestem zwolennikiem odseparowywania renderowania od właściwej logiki kontrolek, aby mieć wolność, elastyczność oraz zachowaną pojedynczą odpowiedzialność. Kontrolka powinna posiadać dane dotyczące jej typu, stylu oraz stanu, a także logikę umożliwiającą zmianę jej stanu — i nic więcej. Jednocześnie kontrolka nie powinna zajmować się renderowaniem samej siebie, bo nie ma bladego pojęcia o tym, do czego aktualnie służy i jak ma finalnie wyglądać na ekranie.

Malowaniem powinien zajmować się renderer zaimplementowany poza logiką kontenera UI i kontrolki, który wie jak ma wyglądać interfejs na ekranie. Powinien odczytywać dane z kontrolki (jej pozycję, rozmiar, kolor, tekst, typ fontu, ID tekstury itp.), a następnie na podstawie tych danych renderować ją na ekranie lub tylnym buforze. To renderer decyduje o tym jak finalnie będzie ona wyglądać, a nie kontrolka sama w sobie. Jeśli np. trwa animacja zanikania obrazu, to renderer klatki wie o tym, a kontrolka wiedzieć o tym nie powinna, bo ta wiedza nie jest jej do niczego potrzeba.

W moim silniku, cały kernel UI nie ma nic wspólnego z renderowaniem — zawiera informacje o tym gdzie się znajduje i jaki ma rozmiar, czy jest odblokowany na input, dane na temat stanu animacji kontenera (widoczność) oraz listę kontrolek, a także zestaw funkcji do aktualizacji jego i kontrolek. Nic więcej. Taki kontener mogę wyrenderować w dowolny sposób, w zależności od określonego końcowego efektu, a kontrolki i sam kontener nic o tym nie wiedzą i nie ingerują w ten proces.


Jeśli jesteś ciekaw jakiego typu zdarzenia emitowane są przez aktualizator UI w moim silniku i jakie dane przesyłane są do kontrolek podczas komunikacji aktualizatora UI z nimi, to tutaj jest deklaracja struktury zdarzenia UI — https://pastebin.com/Ga7SVkUj

0

SFML jest dosyć stary i może być trudno napisać współczesny interfejs z użyciem tej biblioteki.

Współczesny interfejs raczej napiszesz już w HTML + CSS, najlepiej z użyciem JS. Całość jednak musi być dobrze zoptymalizowana i np. zająć tylko 1 ms na klatkę (klatka przy 60 fpsach ma 16 ms).

Niektórzy używają chromium z renderowaniem do tekstury, ale to dużo pracy aby coś zacząć sensownego robić. Ale w chromium trudno jest zmieścić się w limicie czasu wykonania, ewentualnie są duże opóźnienia w interfejsie.

Duże studia zaczynają korzystać z Coherent Gameface, który działa z JS i React: https://coherent-labs.com/products/coherent-gameface/ ale jest drogi.

Całkiem nieźle zoptymalizowany i open source jest projekt RmlUi: https://github.com/mikke89/RmlUi
Z jego pomocą jesteś w stanie robić na prawdę dobre interfejsy użytkownika, ale niestety nie ma wsparcia JS - zamiast tego jest Lua.
Z plusów to od razu dostępny jest data binding, co jest bardzo zbliżone do tworzenia interfejsu w React.

screenshot-20240403174301.png

screenshot-20240403174249.png

1 użytkowników online, w tym zalogowanych: 0, gości: 1