Dźwięki w 3D

Wstęp


Standardowo muzyka i dźwięki są odtwarzane w taki sam sposób dla każdego miejsca w którym stoi gracz. Na pewno spotkałeś się w grach z czymś takim jak to że postacie które się do nas zbliżają robią inny hałas gdy są od nas daleko (dźwięki chodzenia cichsze), a inne kiedy chodzą zaraz obok nas (dźwięki są głośniejsze). W tym poradniku dowiesz się jak można osiągnąć taki efekt w SFML.

Dźwięki mogą zostać uprzestrzennione jedynie tylko wtedy gdy posiadają pojedynczy kanał, czyli są mono. Ten efekt jest wyłączony dla dźwięków, które posiadają więcej kanałów, ponieważ należy wyraźnie zaznaczyć w jaki sposób mają zostać odtworzone.

 

Słuchasz (listener)


Cała muzyka i dźwięki będzie miała swojego odbiorcę, którego nazywamy listener’em. To w jaki sposób on będzie odbierał dźwięki to w ten sposób będzie je słyszał gracz przez swoje głośniki.

W SFML klasa która zajmuje się odbieraniem i przekazywaniem dźwięków w odpowiedni sposób do gracza to sf::Listener. Ta klasa jest unikalna sama w sobie i posiada jedynie funkcje statyczne.

Aby uzyskać efekt dźwięków 3D należy na początku ustawić pozycję słuchacza na scenie:

Jak widzimy podajemy pozycję w 3D (x,y,z), oczywiście w przypadku gry 2D możesz pozycję Y możesz ustawić na 0.

Możesz także dodatkowo ustawić stronę, w którą zwrócony jest gracz:

W tym wypadku słuchacz jest zwrócony w stronę dodatniej osi X, co oznacza że że dźwięk z pozycji (15, 0, 5) będzie słyszalny z prawego głośnika.

Pozostało nam jeszcze ustawić globalną wartość dźwięku na scenie:

Wartość głośności leży w przedziale [0,100], a więc ustawiając na 50 ustawiliśmy na połowę głośności dźwięku.

Oczywiście wszystkie wartości funkcji można pobrać za pomocą przedrostka get.

 

Źródła dźwięku


Każde źródło dźwięku zdefiniowane w SFML posiada te same właściwości dla przestrzenności dźwięków.

Główną z nich jest pozycja źródła dźwięków:

Jest to standardowa pozycja i jeżeli trzeba można ustawić ją aby była słyszalna względem słuchacza.

Może to być użyteczne dla dźwięków wydawanych przez np samego gracza, który zawsze będzie słyszał dźwięki wydawane przez siebie (np wystrzał z pistoletu etc).

Możesz także ustawić słyszalność dźwięków dopiero z pewnej odległości.

Minimalna odległość to odległość, na jakiej będzie słychać dźwięk przy maksymalnej głośności. Na przykład głośniejsze dźwięki jak eksplozje, powinny mieć większą minimalną odległość tak aby dawały wrażenie że są słyszalne z daleka. Minimalna odległość nie powinna przyjmować wartości 0 (jej źródło jest wewnątrz głowy bohatera).

Attenuation to faktor osłabienia dźwięku. Im jest większy tym  słabiej będzie słyszalny z daleka. Aby go nie używać możesz mu ustawić wartość 0. A jeżeli mu ustawisz 100 to dźwięki będą słyszalne jedynie z bardzo bliskiej odległości.

Oryginalny artykuł

Niestandardowe strumieniowanie audio

Trochę teorii


Na początku musimy sobie odpowiedzieć na pytanie czym jest strumieniowanie audio. Strumieniowanie audio (audio stream) jest podobne do muzyki (klasa sf::Music), posiada podobne funkcje, które zachowują się w ten sam sposób. Podstawową różnicą jest to że nie odtwarzamy tutaj pliku audio, lecz odtwarzamy audio bezpośrednio ze źródła, które odtwarzamy. Inaczej rzecz ujmując, możesz wtedy odtworzyć np: audio otrzymane z sieci (komunikator głowowy w twojej grze), muzykę wygenerowaną przez twój program, czy też format audio, którego SFML jeszcze nie wspiera.

Klasa sf::Music jest wyspecjalizowana w strumieniowaniu audio poprzez pobieranie małych fragmentów (samples) z pliku.

Z racji że mówimy o strumieniowaniu audio będziemy mówili o audio, które nie może być załadowane w całości do pamięci, lecz będziemy je wczytywali fragmentami i dopiero odtwarzali. Jeżeli twoje dźwięki mogą być załadowane  w całości do pamięci to strumieniowanie audio nie pomoże ci, skorzystaj wtedy z sf::SoundBuffer oraz odtwórz swój dźwięk za pomocą sf::Sound.

sf::SoundStream


Aby utworzyć swoją klasę do strumieniowania audio musisz dziedziczyć po klasie abstrakcyjnej sf::SoundStream. Posiada ona 2 wirtualne klasy, które musisz napisać: onGetData oraz onSeek.

Funkcja onGetData jest wywoływana za każdym razem gdy zostanie odtworzony obecnie pobrany fragment muzyki i będzie potrzebny kolejny. Przez tą funkcję musisz dostarczyć kolejnego fragmentu używając argumentu data.

Musisz zwrócić true jeżeli wszystko udało się wczytać, w przeciwnym razie (lub gdy nie ma więcej danych do odtworzenia) zwróć false, wtedy odtwarzanie audio zostanie zatrzymane.

SFML tworzy kopie sample’i w momencie gdy zostanie zwrócona prawda w onGetData, więc nie musisz trzymać tych danych, możesz spokojnie je usunąć.

Metoda onSeek jest wywoływana w momencie wywołania funkcji setPlayingOffset. Służy ona do zmiany wskaźnika odtwarzanych danych na odpowiedni. Jako argument przyjmuje czas utworu, który ma być odtworzony (czas jest podawany licząc od początku utworu nie od obecnej pozycji). Ta funkcja czasami jest niemożliwa do zaimplementowania, wtedy możesz zostawić ją pustą i powiedzieć użytkownikowi, że nie da się przewinąć utworu.

Twoja klasa jest już prawie gotowa do pracy. Klasa SoundStream potrzebuje jeszcze wiedzieć o numerze kanału (channel number) oraz o częstotliwości próbkowania (sample rate). Aby przekazać te parametry musisz użyć funkcji protected initialize najczęściej zaraz po wczytaniu/zainicjalizowaniu strumieniowania.

 

Kwestia wątkowania (thread)


Strumieniowanie audio zawsze odbywa się w oddzielnym wątku, jednak powinieneś wiedzieć co się tam dzieje i gdzie.

onSeek jest wywoływane bezpośrednio z funkcji setPlayingOffset więc jest zawsze wykonywana w wątku z którego została wezwana. Jednakże onGetData jest wzywane dopóki jest odtwarzane audio w oddzielny wątku stworzonym przez SFML. Więc jeżeli twój strumień audio używa danych może zostać przydzielony jednocześnie do obu wątków: tego w którym został uruchomiony oraz w wątku odtwarzającym, który musisz chronić za pomocą np. mutex’ów tak aby uniknąć równoczesnego dostępu do tych samych danych. W przeciwnym razie będzie mogło dojść do nieprzewidywalnych zachowań (zniszczenie odtwarzanych danych, crash, itd).

Jeżeli nie czujesz się pewnie w kategorii wątków zapraszam do poradnika o wątkach.

 

Użycie własnego strumieniowania audio


Skoro już posiadamy zdefiniowaną klasę, przyjrzyjmy się jak należy ją poprawnie użyć. Jest to bardzo proste, wręcz podobne do tego co pokazywałem w poradniku o sf::Music. Możesz kontrolować audio za pomocą: play, pause, stop i setPlayeingOffset. Oczywiście możesz je także odtworzyć z takimi właściwościami jak np. głośność. Możesz się także odnieść do dokumentacji API lub do innych poradników o audio.

 

Przykład


Poniżej mamy prosty przykład strumieniowania audio, gdzie jest odtwarzanie audio z bufora audio. Całość w sama sobie jest totalnie bezużyteczna, ale chodzi tutaj o pokazanie w jaki sposób dane (data) są strumieniowanie przez klasę, nie ważne z jakiego źródła.

Oryginalny artykuł

 

 

 

Lista stron z zasobami do gier

Poniżej chciałbym wam przedstawić listę z darmowymi (w większości) zasobami do gier, typu: grafika, muzyka, dźwięki. Strony które tutaj wypiszę raczej cechują się dużym wyborem i faktycznie są warte uwagi. Jeżeli znacie strony z fajnymi zasobami piszcie w komentarzach to je dopiszę.

Grafika

Muzyka i dźwięki

Nagrywanie dźwięku

Nagrywanie dźwięku do bufora


W większości przypadków do zapisywanie przechwytywanych dźwięków używa się klasy sf::SoundBuffer, która pozwala na odtwarzanie i zapisywanie dźwięków.

Jednak do samego nagrywania używa się sf::SoundBufferRecorder.

SoundBufferRecorder::isAvailable sprawdza czy nagrywanie jest możliwe na tym systemie, jeżeli zwróci false możesz sobie darować nagrywanie i nic na to nie poradzimy.

Nagrywanie odbywa się w oddzielnym wątku więc nie musisz kombinować aby nagrać cokolwiek, po prostu używasz play i zaczyna się nagrywanie niezależnie od tego co robi twoja gra.

Co możesz zrobić z nagranymi danymi?

– zapisać do pliku

– odtworzyć

– uzyskać dostęp do surowych danych, przekształcić je i cokolwiek chciałbyś z nimi zrobić

 

Nagrywanie niestandardowe


Jeżeli przechowywanie i zapisywanie przechwyconych dźwięków to dla ciebie za mało, to możesz utworzyć swoją własną nagrywarkę. W ten sposób będziesz mógł np streamować przechwycona audio do sieci itp.

Aby tego dokonać musisz utworzyć klasę, która dziedziczy po SoundRecorder. Posiada ona w sobie klasę wirtualną onProcessSamples podczas gdy każdy chunk został przechwycony, więc to jest miejsce gdzie musisz zaimplementować własne elementy.

Są także dwie opcjonalne metody do nadpisania: onStart onStop które są wywoływane podczas rozpoczęcia/zatrzymania nagrywania.

Poniżej masz przykładowy szkielet takiej klasy:

Funkcje isAvailable/start/stop zostały zdefiniowane w sf::SoundRecorder i są dziedziczone w każdej klasie potomnej tej klasy, co oznacza że ich użycie wygląda tak samo jak w klasie sf::SoundBufferRecorder.

Oryginalny artykuł

Odtwarzanie muzyki i dźwięków

sf::Sounds vs sf::Music


W SFML istnieją dwie klasy do odtwarzania audio: sf::Music oraz sf::Sound. Nie róźnią się one zbytnio w tym co oferują, ale bardziej sposobem w jaki działają.

sf::Sound jest lżejszym obiektem, który do odtwarzania audio potrzebuje sf::SoundBuffer. Ta klasa jest odtwarzana w całości w pamięci i powinna być używana do odtwarzania krótkich utworów audio (czyli dźwięków), czyli np. do odgłosów strzałów, chodzenia itp.

sf::Music nie jest od razu wczytywana w całości w pamięci, jest wczytywana w „locie” tzn. w trakcie działania programu w miarę potrzeb. Jest obiektem cięższym oraz może odtwarzać dłuższe utwory, które trwają dużo dłużej niż krótkie dźwięki.

 

Wczytywanie i odtwarzanie dźwięków


Tak jak wspomniałem powyżej nie można odtworzyć dźwięków bezpośrednio z sf::Sound tylko trzeba je odpowiednio przygotować, czyli musimy je na początku wczytać przez sf::SoundBuffer.

Oczywiście można także do wczytania skorzystać z loadFromMemory, czy też własnego strumienia danych (loadFromMemory). Pełna lista wspieranych rozszerzeń plików audio jest dostępna w dokumentacji API.

Można także wczytać dźwięk z tzw. sample’i, jednak tego tutaj nie pokażę (zapraszam do oryginalnego poradnika).

Aby odtworzyć audio należy należy użyć klasy Sound.

Co jest fajnego to fakt, że możesz przypisać do jednego bufora wiele dźwięków i odtworzyć je jednocześnie. Co jest ważne dźwięki oraz muzyka są odtwarzane w osobnym wątku, co oznacza że po uruchomieniu play możesz robić co chcesz (nie licząc usunięcia muzyki).

 

Odtwarzanie muzyki


W przeciwieństwie do dźwięków sf::Music nie potrzebuje klasy pomocniczej do odtworzenia audio, tzn otwieranie pliku i jego odtworzenie jest robione przez tą jedną klasę.

Nie bez powodu funkcja nazywa się openFromFile, a nie loadFromFile, jest to dlatego że plik jest wczytywany na bieżąco, a nie kopiowany w całości do pamięci. Można także użyć: openFromMemory oraz openFromStream.

 

Co jeszcze można robić?


Poniżej pokazane funkcje wyglądają tak samo dla Sound jak i Music.

  • play uruchamia lub kontynuuje odtwarzanie audio
  • pause wstrzymuje odtwarzanie muzyki
  • stop zatrzymuje odtwarzanie dźwięku oraz przewija go
  • setPlayingOffset zmienia obecnie odtwarzaną pozycję

getStatus zwraca nam obecny status, czyli mówi nam czy audio jest w trakcie odtwarzania, spauzowane itp. Muzyka oraz dźwięki posiadają kilka artrybutów które można zmieniać w dowolnej chwili:

Można ustawić pitch, które wpływa na ton dźwięku a co za tym idzie na prędkość jego odtwarzania. Ustawiamy tutaj faktor, który gdy jest > 1 to sprawia, że dźwięk jest niższy, < 1 wyższy, == 1 normalny.

Można ustawić także volume, czyli głośność dźwięku w przedziale [0,100].

loop decyduje o tym czy utwór ma być zapętlany, czyli odtwarzany w nieskończoność.

Jest dostępnych także więcej atrybutów, jednak o nich porozmawiamy w poradniku o dźwiękach 3D.

 

Najczęściej spotykane błędy


Zniszczenie bufora

Jest wtedy gdy deklarujesz go w sposób podobny jak poniżej, pamiętaj że musi on być cały czas dostępny w pamięci programu gdy go używasz.

 

Za dużo dźwięków

SFML posiada limit ilości dźwięków, czyli 256, jeżeli chcesz używać ich więcej to musisz pozbyć się z pamięci tych których nie używasz i wczytać jedynie te których potrzebujesz.

 

Usunięcie źródła muzyki z pamięci

Pamiętaj, że podczas odtwarzania muzyki plik cały czas musi być dostępny. Zwykle w przypadku gdy jest na dysku nie ma z tym problemu, al może się zdarzyć, że otworzysz go np  z pamięci, wtedy łatwo przez przypadek go usunąć.

 

sf::Music nie można kopiować

Czyli nie można zrobić czegoś takiego:

Oryginalny artykuł

 

Kontrolowanie kamery przy użyciu View

Czym jest view?


View jak nazwa wskazuje jest podglądem poziomu, a konkretnie jakiegoś mniejszego obszaru. Stosuje się go w grach gdzie poziom gry jest większy niż ekran, dzięki czemu możemy decydować, który kawałek poziomu ma być wyświetlany.

W SFML istnieją 2 podstawowe założenia odnośnie view: możesz decydować jaki kawałek poziomu będzie wyświetlany, a także gdzie jest wyświetlany oraz w jaki sposób (czy jest powiększony itd).

Podsumowując view pozwoli ci powiększać, obracać i przewijać poziom. Jest on także przydatny przy robieniu split screenów oraz mini mapach.

 

Definiowanie podglądu view


Klasa służąca do podglądu w SFML została zhermetyzowana w sf::View. Możemy jeszcze w konstruktorze zdecydować co ma wyświetlać:

Obie definicje powyżej tworzą podgląd na obszar mapy o środku na pozycji (350,300) oraz wielkości 300×200.

Możesz także edytować je poza konstruktorem:

Teraz kiedy zdefiniowałeś już swój podgląd (view) to możesz go transformować w dowolny sposób.

 

Poruszanie podglądem (przewijanie/scrolling)

W przeciwieństwie to obiektów Drawable, których pozycja standardowo była umieszczona w lewym górnym rogu to w klasie View umieszczana jest ona zawsze w centrum obiektu. To jest także powodem dla którego ustawianie pozycji w tej klasie brzmi setCenter, a nie setPosition.

Obracanie view

Czyli przechylanie naszej kamery. Jako argument podajemy wartości w stopniach.

 

Powiększanie (zoomowanie/skalowanie)

Zoomowanie jest odpowiednikiem zmiany rozmiaru, a więc używamy tutaj setSize.

 

Definiowanie gdzie view ma być wyświetlane


Skoro już wiemy jak ustawić co view ma wyświetlać, czas zająć się tym jak ustawić gdzie ma być wyświetlane. Standardowo view zajmuje cały ekran, jeżeli podgląd jest tej samej wielkości co okno wszystko jest renderowane w stosunku 1:1. Jezeli jest innych wymiarów wszystkie obiekty są odpowiednio skalowane.

Standardowe zachowanie tej klasy jest odpowiednie dla większości przypadków jednak czasami chcemy aby było inne, np w przypadku gry multiplayer gdzie chcemy użyć dzielenia ekranu (split screen) poprzez użycie dwóch viewów, które będą zajmowały jedynie po połowie ekranu. Możesz także tego użyć to wyświetlania mapy, która będzie pomniejszoną wersją wyświetlanego obszaru. Do zrobienia czegoś takiego używamy viewPort.

Aby zdefiniować viewPort musisz użyć funkcji setViewPort.

Jak możesz zauważyć viewport nie jest definiowany w pikselach lecz za pomocą faktora size. Takie zastosowanie ma swoje powody, dzięki temu nie musisz pilnować czy nastąpił event resize, aby zaktualizować jego wielkość.

Użycie viewport dla split screena w większości gier wygląda podobnie.

lub dla minimapy:

 

Użycie View


Aby narysować coś za pomocą View, musisz wcześniej ustawić View w obiekcie, który może renderować inne obiekty (RenderWindow, RenderTexture)

Jeżeli nie chcesz używać już ustawionego przez siebie View, lecz chcesz skorzystać z normalnego poglądu okna możesz to zrobić w ten sposób:

Podczas uruchamiania setView jest tworzona kopia view, a nie wskaźnik do niego co oznacza, że po każdej zmianie na View (np skalowanie) musisz wywołać ponownie setView.

 

Zmiana zachowania view


Standardowo po zmianie rozmiaru okna wszystkie obiekty są obiekty są rysowane na tych samych pozycjach tylko odpowiednio przeskalowane do nowej wielkości okna.

Możliwe że będziesz chciał zmienić ten sposób zachowania i np. przy powiększeniu okna chciałbyś aby był widoczny większy obszar mapy. Wszystko co musisz w tym wypadku zrobić to zsynchronizować view do wielkości okna.

 

Konwersja współrzędnych


Podczas gdy używasz view piksele będą mogły nie pasować do pozycji swojej pozycji, ponieważ ich pozycja będzie zależna od przeskalowania. Np klikając na pozycję (10,40) będzie mogło się okazać, że kliknęliśmy rzekomo na pozycję (30, -40) w świecie gry. Aby to poprawić musisz skonwertować pozycję na ekranie do pozycji w świecie gry: mapPixelToCoords.

Standardowo mapPixelToCoords używa obecnie używanego view, jeżeli chcesz użyć view, które nie jest obecnie aktywne, możesz to zrobić poprzez podanie go jako argument tej funkcji.

Możesz także przeprowadzić konwersję w drugą stronę: mapCoordsToPixel.

Oryginalny artykuł