Zapisywanie użytych przedmiotów w grze (np. skrzyń)
Jednym z zagadnień, które często pojawia się w grach, jest zapamiętywanie, czy gracz wykonał już w jakimś miejscu akcje. Kto programuje już chwilę, całkiem szybko wpadnie na to, że najlepiej skorzystać w tym celu z tablic i tam zapisywać jako true/false informację na ten temat.
Problem pojawia się jednak w momencie, gdy trzeba jakoś oznaczyć przedmioty w roomach, żeby było wiadomo, który indeks w tablicy odpowiada któremu przedmiotowi. Prostym sposobem byłaby zmienna, która przechowuje indeks tablicy, ustawiana np. w Creation Code, ale taki system nie jest łatwy do zarządzania, bowiem gdy usuniemy pewien przedmiot, nie możemy nie wiedzieć którego nam brakuje. Poza tym, gdy nagle dodamy nowy etap, możemy nie pamiętać, na jakiej cyfrze skończyliśmy (teoretycznie tabela powinna mieć ten sam rozmiar, ale co jeśli od razu zarezerwowaliśmy np. 100 miejsc na zaś?). Innym sposobem którym widziałem, jest ds_mapa, w której przy otwarciu zapisujemy klucze w postaci:
object_get_name(id) + string(x) + string(y), gdyż raczej nie zdarzy się, że dwa obiekty których stan chcemy zapamiętać stoją w tym samym miejscu (to wręcz przeszkadzałoby np. w ich użyciu i wykryciu kolizji). Niestety, wszystko się zepsuje w momencie, gdy taki obiekt przesuniemy. Warto jednak wiedzieć, że ten sposób wymaga najmniej kodu, więc nie odradzam go całkowicie.
Ja natomiast wybrałem sposób nieco trudniejszy w implementacji (ok 30 linijek kodu), ale za to bardziej praktyczny. Korzysta on z pewnej mało używanej właściwości GameMakera - mianowicie faktu, że każda dodana w roomie instancja dostaje swoją "stałą", której można użyć w kodzie (w formacie inst_8ZNAKOW). Plusem jest to, że można tę nazwę ustawić samemu (ale nie trzeba), można też swobodnie usunąć przedmiot ze środka kolejki (i automatycznie kolorowanie składni przestanie go podświetlać, co ułatwi jego zlokalizowanie), a można go nawet przenieść do innego roomui i nadać starą nazwę, żeby kod nadal działał. Wykorzystuje on dwie tablice, dzięki czemu łatwo go też zapisać do pliku.
Wskazówka:
Nie korzystamy z DS_MAP, gdyż id ukrywające się pod stałą mogą się zmienić, gdy dodamy/usuniemy inne obiekty z danego roomu. Nigdy nie zakładaj, że id zasobów i instancji się nie zmienią - dlatego nie używaj ich jako kluczy struktur/tablic, oraz nie zapisuj do pliku.
Na początek stworzymy sobie dwa obiekty:
obj_player (dowolny sprite):
Kod poruszania w step:
kodx += (-keyboard_check(vk_left) + keyboard_check(vk_right)) * 4;
y += (-keyboard_check(vk_up) + keyboard_check(vk_down)) * 4;
Kod do testów, w Key Pressed R
kodroom_restart();
Teraz stworzymy obj_chest, któremu stworzymy sprite z dwiema klatkami - skrzynią zamkniętą (0) i otwartą (1). Ustawmy też FPS w sprite na 0, dzięki czemu nie będzie się animował i zawsze pokaże zerową klatkę jako domyślną.
obj_chest:
Create:
kodopened = false;
Kod w kolizji z obj_player:
kodif (opened == false) {
opened = true;
image_index = 1;
}
Teraz można w roomie wstawić jednego playera, kilka skrzyń i przetestować, czy działają kolizje i otwieranie skrzyń.
Etap drugi
Utwórzmy teraz nowy skrypt, ja nazwałem go "chest_scripts". Skorzystamy z właściwości GameMakera 2.3+ (2021+), która pozwala na zdefiniowanie zmiennych przed rozpoczęciem gry - wystarczy napisać kod poza function(). Zdefiniujemy sobie tablicę z naszymi skrzyniami (w moim przypadku są cztery), kopiując ich identyfikatory z room editora. Znajdziecie je w inspektorze po zaznaczeniu instancji, lub klikając dwa razy (input tekstowy z inst_XXXXX). Możecie je dowolnie zmienić, ale ja zostawiłem te losowe.
Stworzymy też od razu tablicę z informacją czy skrzynia jest otwarta/zamknięta - o tym samym rozmiarze, domyślnie z wartościami false. Żeby nie musieć jej zawsze modyfikować, skorzystamy z array_create, gdzie rozmiarem tablicy będzie rozmiar tablicy ze skrzyniami, pobrany przy pomocy array_length:
kodglobal.chests = [inst_18134AA6, inst_1C8CC5E6, inst_2B0F70D8, inst_1AFF602];
global.opened = array_create(array_length(global.chests), false);
Wskazówka:
Powyższy kod wykona się tylko gdy gra startuje i nie wykona się ani przy room_restart(), ani nawet przy game_restart();
Teraz, wypadałoby napisać funkcję, która sprawdza, czy dana skrzynia jest otwarta. W tym samym jedynym w naszej grze skrypcie, dodamy kod, który "przelatuje" tablicę i sprawdza, czy ID obecnej skrzyni w ogóle jest na liście i jeśli ją znajdzie, zwróci informację o tym, czy jest otwarta. Dzięki użyciu dwóch tablic, nawet jeśli zmienią się ID skrzyń w grze, nie zmieni się ich pozycja w tablicy. Gdyby skrzynia była kluczem ds_mapy, mimo tej samej "stałej", miałaby ona inną wartość i skrzynie by się losowo zamykały po zmianach w roomie - bowiem tę wartość GM ustawia dopiero w momencie kompilacji.
Funkcja, poza tym, że zwraca, czy skrzynia jest otwarta, od razu zmienia index sprite'a na 1.
kodfunction check_if_opened() {
image_index = 0;
var len = array_length(global.chests);
for(var i = 0; i < len; i++) {
if (global.chests[i] == id) {
if (global.opened[i] == true) {
image_index = 1;
return true;
}
}
}
return false;
}
Została ostatnia funkcja - zapisująca otwarcie skrzyni. Napiszemy ją w bardziej uniwersalny sposób, który pozwoli zapisać też w przyszłości zamknięcie skrzyni. Zmianę sprite'a wywołamy poprzednią funkcją check_if_opened(), dzięki czemu od razu obsłużymy oba przypadki otwarcia i zamknięcia.
kodfunction chest_save() {
var len = array_length(global.chests);
for(var i = 0; i < len; i++) {
if (global.chests[i] == id) {
global.opened[i] = opened;
check_if_opened(); // change sprite
return true; // exit for-loop early
}
}
return false;
}
Pozostaje poprawić kod w obj_chest, aby korzystał z nowych funkcji. Dodajemy/zmieniamy pierwszy skrypt w create, a drugi w step:
Create (po zmianach):
kodopened = false;
check_if_opened(); // nowa linijka
Kolizja:
kodif (opened == false) {
opened = true;
chest_save(); // zmieniona linijka
}
I to tyle. Wszystko działa, a po odpaleniu gry wystarczy wcisnąć "R", aby zobaczyć, że skrzynie są zapamiętane.
Co dalej?
Resetowanie otwarcia skrzyń (w menu, zaczynając/wczytując nową grę) wykonujemy przez ponowne wykonanie:
kodglobal.opened = array_create(array_length(global.chests), false);Warto na pewno utworzyć sobie z tego kawałka kodu funkcję, żeby nie powtarzać go dwa razy.
Natomiast zapis do pliku, pozostawiam już wam - ostatecznie można skorzystać z json_stringify do zapisu i json_parse do odczytu - dodając walidację długości i danych (najlepiej więc wczytać ją do nowej tablicy i porównać pętlą for, o długości najmniejszej (min) z dwóch długości - global.opened i wczytanych danych, rzutując wszystko na true/false).
Gotowy przykład pobierzecie tutaj: gmclan.org/.../saving_used_items.yyz