Jak zaplanować architekturę gry 2D krok po kroku dla początkujących twórców

0
43
5/5 - (1 vote)

Nawigacja:

Krótka historia pierwszej gry, która się rozpadła po tygodniu

Pierwsza gra często zaczyna się niewinnie: prosty pomysł na platformówkę, parę wieczorów dłubania, pikselowy bohater skacze, wrogowie się ruszają, punkty się zliczają. Wszystko działa, dopóki nie pojawi się potrzeba „małej zmiany”: dodać drugi typ przeciwnika, wprowadzić ekran pauzy, dorzucić system żyć. Nagle po tej drobnej modyfikacji kolizje przestają działać, UI się sypie, a każdy kolejny bug rodzi następny.

To typowy scenariusz dla początkujących twórców: kod powstaje spontanicznie, bez planu. Grunt to „żeby działało”, więc logika gracza miesza się z rysowaniem, obsługa klawiatury siedzi w trzech różnych plikach, a zmiany stanu gry są rozsiane po całym projekcie. Jeśli trzeba coś poprawić, najprościej skopiować fragment kodu i wkleić obok z drobną modyfikacją. Do czasu.

Chaos w architekturze gry 2D rodzi się zwykle w kilku miejscach:

  • brak podziału na moduły (wszystko w jednym pliku albo w kilku losowych),
  • mieszanie logiki, renderowania i wejścia w tych samych funkcjach,
  • nadmierne używanie zmiennych globalnych, singletonów „do wszystkiego”,
  • kopiowanie kodu zamiast wydzielania wspólnych funkcji lub klas,
  • brak jasnych granic: nie wiadomo, co jest odpowiedzialne za co.

Architektura gry w praktycznym sensie to nic innego jak sposób, w jaki całość jest poukładana: jakie masz moduły, jak się ze sobą komunikują, gdzie trzymasz dane, gdzie jest logika, a gdzie wyświetlanie. To nie tylko „wzorce projektowe” z książki, ale bardzo konkretna odpowiedź na pytanie: „jeśli jutro będę chciał dodać nowy typ przeciwnika, co będę musiał dotknąć?”.

Wniosek z tej krótkiej historii jest prosty: nawet mała gra 2D potrzebuje choćby bardzo prostego planu technicznego. Inaczej każda drobna zmiana staje się zabiegiem chirurgicznym na otwartym sercu – bez znieczulenia i bez narzędzi. Odrobina myślenia o architekturze na początku oszczędza wiele godzin frustracji później.

Co to znaczy „dobra architektura gry 2D” dla początkującego

Realne potrzeby małej gry vs. ogromne silniki AAA

Gdy szuka się informacji o projektowaniu silnika gry, łatwo trafić na opisy ogromnych systemów: dziesiątki warstw, zaawansowany ECS, moduły sieciowe, edytory poziomów, narzędzia do analityki. Wszystko to brzmi imponująco, ale przy pierwszej, małej grze 2D taka skala jest po prostu zbędna. Nadmierna ambicja architektoniczna potrafi zabić projekt szybciej niż brak planu.

W małym, 2D projekcie solo lub w duecie potrzeba przede wszystkim:

  • jasnego podziału odpowiedzialności (co gdzie jest),
  • prostej pętli gry, którą da się mentalnie ogarnąć,
  • kilku podstawowych modułów (input, logika, render, audio, zasoby),
  • takiej struktury, która umożliwia dopisywanie funkcji bez ruszania wszystkiego.

Nie chodzi o zbudowanie „własnego Unity”, ale o zaplanowanie gry tak, żeby kolejny tydzień pracy nie polegał na przepisywaniu połowy kodu.

Trzy kluczowe cechy dobrej architektury dla początkującego

Dobra architektura gry 2D na start nie musi być „idealna” w sensie akademickim. Ma spełniać trzy praktyczne kryteria:

  1. Prostota implementacji – jesteś w stanie to napisać w rozsądnym czasie i bez doktoratu z inżynierii oprogramowania. Im mniej warstw abstrakcji na początek, tym lepiej. Lepiej mieć „wystarczająco dobre” rozwiązanie, niż genialny wzorzec, którego nie rozumiesz po tygodniu przerwy.
  2. Łatwość zmiany – dodanie nowego przeciwnika, nowej broni czy ekranu menu nie wymaga przekopywania 30 plików. Zmiana reguły gry powinna dotykać głównie jednego modułu (np. logiki), a nie UI, audio i renderer jednocześnie.
  3. Czytelność dla „przyszłego siebie” – po miesiącu przerwy otwierasz projekt i wiesz, gdzie szukać. Nazwy plików, folderów i modułów mówią same za siebie. Nie musisz pamiętać „jak to było zrobione”, bo struktura prowadzi cię za rękę.

Jeśli te trzy punkty są spełnione, architektura jest wystarczająco dobra na start, nawet jeśli nie wykorzystuje najbardziej wyszukanych wzorców.

Ambicja gry a złożoność architektury

Projektowanie architektury gry 2D musi być dopasowane do ambicji projektu. Inaczej będzie wyglądać szkielet prostego endless runnera na telefon, a inaczej taktycznego RPG-a z ekwipunkiem, zadaniami i zapisem stanu. Kuszące jest przygotowanie się „na wszystko”, ale zaawansowany system ekwipunku nie jest potrzebny w grze, która ma jedną broń i brak progresji.

Przeprojektowanie bywa tak samo niebezpieczne jak brak planu. Rozbudowane moduły, które nie są wykorzystywane, pochłaniają czas i energię. Początkujący twórca szybko traci motywację, bo zamiast pracować nad rozgrywką, utknął w projektowaniu abstrakcyjnych systemów.

Rozsądna zasada: projektuj pod to, co wiesz, że będziesz miał, i zostaw miejsce na prostą rozbudowę, ale nie buduj katedry dla gry trwającej 3 minuty. Minimalny plan architektury powinien powstać jeszcze przed pierwszą linią kodu, ale ma być krótki i konkretny.

Minimalny plan przed startem kodowania

Przed odpaleniem edytora kodu warto odpowiedzieć sobie na kilka technicznych pytań. To nie musi być 20-stronicowy dokument, wystarczy strona w notatniku lub tablica w narzędziu typu Trello:

  • Jakie moduły będą potrzebne? (np. input, gameplay, rendering, audio, UI, system scen/ekranów, zarządzanie zasobami)
  • Jak wygląda przepływ danych? (skąd moduł logiki dostaje informacje o wejściu, gdzie zapisuje wynik, kto wie o czym)
  • Jakie są granice odpowiedzialności? (kto może dotykać zasobów, kto zmienia stan gry, kto wie o graczach, poziomach, UI)
  • Jak będą ładowane poziomy i zasoby? (z plików, z kodu, z edytora?)
  • Jakie są ekrany gry? (menu główne, rozgrywka, pauza, ekran końca gry, ustawienia)
Dwoje specjalistów IT planuje architekturę oprogramowania przy tablicy
Źródło: Pexels | Autor: ThisIsEngineering

Ustalenie zakresu gry – fundament całej architektury

Gatunek i perspektywa a struktura kodu

Architektura gry 2D nie powstaje w próżni. Kluczowe jest to, jaką grę robisz. Inaczej podejdzie się do:

  • platformówki 2D – dużo fizyki skoku, kolizji z kafelkami, przeszkody, przeciwnicy na poziomach,
  • gry top-down (widok z góry) – ruch w czterech kierunkach, system celowania, często AI przeciwników,
  • puzzle / logicznej – mniej fizyki, więcej operacji na abstrakcyjnych stanach (układy klocków, reguły),
  • endless runnera – głównie generowanie przeszkód, tempo gry, postęp punktowy.

Gatunek wpływa na to, co będzie w sercu architektury. W platformówce ważniejszy będzie system kolizji i zachowanie postaci, w puzzlach – sposób reprezentacji planszy i reguły. W runnerze kluczowe stanie się zarządzanie generowaniem segmentów trasy i rosnącą trudnością.

Już na tym etapie warto zapytać: co w tej grze jest absolutnie centralne? To wokół tego obszaru warto budować najczytelniejszy i najlepiej ułożony moduł.

Lista funkcji „must have” i „miło mieć”

Jedną z pułapek architektury jest projektowanie „pod przyszłe pomysły”, które nigdy nie zostaną zrealizowane. Dobrym remedium jest spisanie funkcji w dwóch kategoriach:

  • Must have – bez tego gra nie spełnia swojej podstawowej wizji,
  • Nice to have – dodatki, które fajnie byłoby mieć, ale nie są krytyczne.

Dla prostej platformówki lista może wyglądać na przykład tak:

Must have:

  • ruch gracza w lewo/prawo i skok,
  • kolizje z kafelkami (ziemia, ściany, kolce),
  • co najmniej 3 poziomy,
  • proste przeciwniki chodzące w prawo/lewo,
  • system żyć / punktów zdrowia,
  • ekran startowy + ekran końca gry.

Nice to have:

  • różne typy przeciwników,
  • zapis postępu między sesjami,
  • zbierane monety i sklep,
  • kilka zestawów grafik (skiny),
  • różne poziomy trudności.

Z architektonicznego punktu widzenia „must have” musi być obsłużone od razu, a „nice to have” powinno być możliwe do dołożenia bez przebudowy. To wpływa na decyzje np. czy od razu wydzielać system ekwipunku, czy wystarczy prosty zestaw parametrów w danych gracza.

Pętla rozgrywki w 10 sekundach

Bardzo praktyczne ćwiczenie: opisać, co gracz robi w typowych 10 sekundach gry. Np. dla endless runnera:

  • gracz patrzy na ekran,
  • postać biegnie automatycznie,
  • co kilka sekund pojawia się przeszkoda,
  • gracz dotyka ekranu, żeby skoczyć lub zsunąć się,
  • system zlicza przebyty dystans,
  • prędkość gry powoli rośnie.

Z powyższego wynika, jakie moduły będą istotne:

  • system wejścia (kliknięcia / dotknięcia ekranu),
  • logika gracza (skok, kolizje, stan „żywy/martwy”),
  • system generowania przeszkód,
  • logika rosnącej trudności,
  • UI z wynikiem na żywo.

Takie „10 sekund” potrafi bardzo wyraźnie pokazać, gdzie będzie gęsto od kodu, a co można zrealizować prosto. Pętla rozgrywki jest zarazem szkicem przepływu w głównej pętli gry.

Endless runner vs gra z poziomami i ekwipunkiem

Porównanie dwóch typów gier pomaga zrozumieć, jak zakres wpływa na architekturę gry 2D.

CechaEndless runner 2DGra 2D z poziomami i ekwipunkiem
Struktura poziomówJedna, nieskończona trasa lub segmentyWiele poziomów, być może różne światy
Stan graczaProsty (pozycja, prędkość, liczba żyć/punktów)Bardziej złożony (ekwipunek, umiejętności, zadania)
Pamiętanie postępuCzęsto tylko najlepszy wynikMapa postępu, zapis ekwipunku, odblokowane elementy
Kluczowe modułyGenerowanie przeszkód, system kolizji, wynikSystem poziomów, ekwipunek, zapis/odczyt stanu

Endless runner poradzi sobie z jednym modułem poziomów, który generuje segmenty trasy. Gra z wieloma poziomami i ekwipunkiem wymaga wyraźniejszego podziału: osobne moduły do poziomów, zarządzania stanem gracza, zapisu, może nawet systemu dialogów. Architektura gry 2D musi być więc odpowiedzią na zakres – nie odwrotnie.

Architektura jako odpowiedź na wizję

Najważniejszy wniosek: architektura jest narzędziem do realizacji wizji gry. Jeśli wizja jest mętna, struktura kodu też będzie mętna. Dlatego zanim powstanie katalog „src” w projekcie, warto mieć:

Do kompletu polecam jeszcze: Jak działa system warstw dźwiękowych? — znajdziesz tam dodatkowe wskazówki.

  • skrócony opis gry (1–2 akapity),
  • listę „must have” i „nice to have”,
  • spisaną pętlę rozgrywki,
  • decyzję o gatunku i perspektywie (platformówka, top-down itd.).

Tak przygotowany grunt pozwala zaplanować sensowną, prostą architekturę, która nie udaje „silnika na 10 lat”, ale obsługuje realne potrzeby twojej gry.

Podstawowy szkielet techniczny: pętla gry i warstwy

Wyobraź sobie, że po kilku wieczorach coś wreszcie działa: postać się rusza, kamera podąża, przeciwnik nawet spada z platformy. Po godzinie testów klikasz „restart” i nagle połowa obiektów zachowuje się dziwnie, jakby gra pamiętała poprzednią sesję. Szukasz błędu, a tam: wszystko jest „trochę” wszędzie. Brak wyraźnego szkieletu.

Taki szkielet daje właśnie pętla gry i prosty podział na warstwy. Nawet w najmniejszym projekcie dobra organizacja tego poziomu działa jak rusztowanie przy budowie domu – trzyma wszystko w ryzach, kiedy zaczynasz dokładać nowe cegły.

Serce gry: prosta pętla „update–render”

Niezależnie od silnika, sprowadza się to najczęściej do dwóch kroków w kółko:

  • aktualizacja stanu gry (logika, fizyka, AI, wejście gracza),
  • renderowanie (rysowanie wszystkiego na ekranie).

W bardzo uproszczonej formie pseudokod pętli gry może wyglądać tak:

while (gra_dziala):
    dt = czas_od_ostatniej_klatki()
    wejscie.aktualizuj()
    logika.aktualizuj(dt)
    renderuj()

Kluczowy wniosek: logika i renderowanie to dwa różne etapy. Początkujący często mieszają je w jednym miejscu, np. w funkcji, która jednocześnie zmienia pozycję postaci i rysuje ją na ekranie. Na początku to działa, ale później utrudnia:

  • pauzę gry (logika stoi, ale ekran nadal się odświeża),
  • spójność logiki przy spadkach FPS (logika powinna działać w oparciu o czas, nie o liczbę klatek),
  • testy i debugowanie (chcesz móc testować logikę bez renderowania).

Świadome rozdzielenie „update” i „render” to pierwszy krok w stronę sensownej architektury, nawet jeśli w praktyce wszystko i tak siedzi w kilku plikach.

Stały krok logiki kontra zmienny czas klatki

Typowy problem: na twoim komputerze gra działa świetnie, ale na słabszym laptopie kolegi postać skacze inaczej i trudniej jest trafić w platformy. Źródło kłopotów zwykle tkwi w tym, jak używasz czasu w pętli.

Masz dwa popularne podejścia:

  • aktualizacja zależna od FPS – co klatkę dodajesz np. +5 pikseli do pozycji,
  • aktualizacja zależna od czasu – pozycja zmienia się o prędkość × czas_od_ostatniej_klatki.

Pierwsze rozwiązanie jest proste, ale sprawia, że gra zachowuje się inaczej przy różnych FPS-ach. Drugie jest poprawniejsze, bo prędkości są liczone w jednostce „na sekundę”, jednak przy dużych spadkach FPS logika może stać się niestabilna (obiekty „przelatują” przez siebie, kolizje nie łapią).

Na początek dobra równowaga to:

  • korzystać z delta time (dt) dla ruchu i animacji,
  • unikać ekstremalnych wartości dt (np. ograniczyć maksymalny dt, gdy gra była zatrzymana w tle).

Jeśli później projekt urośnie, można przejść na bardziej zaawansowany model stałego kroku logiki. Na starcie ważniejsze jest samo świadome użycie czasu niż perfekcyjna implementacja.

Podział na warstwy: kto z kim gada

W małych grach najczęściej wszystkie moduły „widzą się” nawzajem. Z czasem robi się z tego zupa zależności: UI odwołuje się bezpośrednio do fizyki, przeciwnicy zaglądają w pola prywatne gracza, a system audio nagle zna szczegóły ekwipunku. Trudno cokolwiek zmienić bez ruszania połowy kodu.

Prosty, praktyczny podział na warstwy może wyglądać tak:

  • Warstwa wejścia (Input) – czyta klawiaturę, mysz, dotyk, pada i zamienia je na abstrakcyjne komendy (np. „ruch w lewo”, „skok”).
  • Warstwa logiki gry – interpretuje komendy, aktualizuje stany obiektów, sprawdza zasady gry.
  • Warstwa prezentacji – renderuje świat, UI, odtwarza dźwięki na podstawie informacji z logiki.

Mini-zasada: wejście i prezentacja nie zmieniają bezpośrednio stanu gry. Przekazują sygnały, ale to logika decyduje, co się dzieje. Dzięki temu możesz np. podmienić sterowanie z klawiatury na pada, nie przebudowując logiki gry.

Pętla gry a ekrany i sceny

Nawet w niewielkiej produkcji zwykle pojawia się więcej niż jeden ekran: menu główne, sama rozgrywka, może proste ustawienia. Zamiast upychać wszystko w jednym „Game” dobrze jest wprowadzić pojęcie sceny/ekranu.

Prosty model scen może wyglądać tak:

  • każda scena ma swoje metody update() i render(),
  • istnieje menedżer scen, który wie, która scena jest aktualnie aktywna,
  • pętla gry wywołuje update i render wyłącznie na aktywnej scenie.

Pseudokod:

while (gra_dziala):
    dt = czas_od_ostatniej_klatki()
    aktywna_scena.update(dt)
    aktywna_scena.render()

Zmiana ekranu polega wtedy na podmianie aktywnej sceny w jednym miejscu. Menu nie musi znać szczegółów rozgrywki, a rozgrywka nie musi wiedzieć, jak działa ekran opcji. Ta izolacja mocno zmniejsza ryzyko, że jedna zmiana „wysadzi” coś w innym ekranie.

Zbliżenie kolorowej planszy z pionkami i kostką do gry
Źródło: Pexels | Autor: Pixabay

Struktura projektu: foldery, moduły i nazewnictwo

W pewnym momencie projekt zaczyna przypominać szufladę, do której przez pół roku wrzucasz wszystko „na później”. Na początku jest czysto, po kilku tygodniach szukasz tego jednego pliku przez 10 minut, bo nie pamiętasz, czy nazwałeś go enemy2_new_final.cs, czy jakoś inaczej. To nie lenistwo, tylko brak prostych reguł od startu.

Na szczęście nie trzeba wymyślać wielkiej architektury katalogów. Wystarczy kilka sensownych szuflad, które od początku porządkują myślenie o grze.

Prosty układ folderów dla małej gry 2D

Przykładowy, praktyczny podział (niezależnie od silnika) może wyglądać następująco:

  • src/ – cały kod źródłowy gry,
    • core/ – pętla gry, definicja scen, pomocnicze klasy bazowe,
    • gameplay/ – logika rozgrywki (gracz, przeciwnicy, pociski, system kolizji),
    • ui/ – interfejs użytkownika (menu, HUD, napisy),
    • systems/ – moduły ogólne (audio, input, zarządzanie zasobami, zapis),
    • data/ – definicje poziomów, konfiguracje, skrypty danych.
  • assets/ – surowe zasoby i gotowe pliki do gry,
    • sprites/, audio/, fonts/, tilesets/ itd.
  • tools/ – małe narzędzia, skrypty do eksportu, generatory.

Nie chodzi o to, by sztywno trzymać się dokładnie takiej struktury, tylko o konsekwencję. Jeśli wszystkie rzeczy od UI lądują w ui/, po miesiącu nie musisz zgadywać, gdzie jest logika przycisku start.

Nazwy klas i plików – mini-kontrakty z przyszłym sobą

Po kilku tygodniach przerwy wrócisz do projektu jako „obca osoba”. Dobre nazwy klas i plików działają wtedy jak drogowskazy. Zamiast NewPlayer2.cs lepiej od razu zdecydować się na:

  • PlayerController – klasa sterująca zachowaniem gracza,
  • PlayerData – struktura/przechowalnia danych o graczu,
  • PlayerView – warstwa prezentacji postaci (opcjonalnie, jeśli rozdzielasz logikę od grafiki).

Podobnie z plikami scen i poziomów:

  • Level01_Forest.json zamiast mapa_test.json,
  • MainMenuScene.cs zamiast menu1.cs.

Prosta zasada: po samej nazwie pliku powinieneś wiedzieć, gdzie w architekturze gry on należy. Jeśli nie wiesz, nazwa jest za ogólna albo struktura projektu wymaga doprecyzowania.

Oddzielenie kodu gry od kodu silnika

Nawet jeśli korzystasz z gotowego silnika (Unity, Godot, MonoGame, czysty SFML), spróbuj mentalnie oddzielić dwie rzeczy:

  • kod, który integruje się z silnikiem (sceny, komponenty, skrypty podpinane do obiektów),
  • kod czystej logiki gry (ruch, reguły, ekwipunek, zasady punktacji).

Przykład: w Unity skrypt przypięty do obiektu może zbierać wejście z Input i przekazywać je do klasy PlayerController, która nie ma żadnych bezpośrednich odwołań do Unity API poza kilkoma prostymi adapterami. Takie rozdzielenie sprawia, że:

  • łatwiej testować logikę niezależnie od sceny,
  • łatwiej przenieść część kodu do innego projektu,
  • zmiana sposobu sterowania nie wymaga kopiowania logiki postaci.

Nie zawsze da się to zrobić idealnie, ale im mniej silnikowych zależności w „mózgu gry”, tym architektura czytelniejsza.

Pliki konfiguracyjne i dane zamiast magii w kodzie

Na początku wszystkie wartości lądują w kodzie: prędkość gracza, obrażenia, punkty za monetę. Gdy nagle chcesz szybko podbić prędkość skoku, przeszukujesz cały projekt za 7.5f, bo nie pamiętasz, gdzie to wpisałeś. Lepiej od razu wydzielić dane do prostych struktur lub plików.

Praktyczne opcje:

  • prosta klasa Config lub BalanceData z polami,
  • pliki JSON/CSV/YAML w folderze data/ wczytywane przy starcie gry,
  • edytor wbudowany w silnik (np. ScriptableObjects w Unity, zasoby w Godot).

Najważniejsze, by logika gry pobierała wartości z jednego miejsca. Wtedy zmiana balansu albo poziomu trudności polega na korekcie danych, nie na przekopywaniu kodu.

Projektowanie modułów gry: od ekranów po systemy

Pierwsza minimalistyczna wersja gry 2D często jest „jednoekranowa”: startujesz i od razu grasz. W pewnym momencie dodajesz menu, później pauzę, może prosty sklep. Jeśli każdy z tych ekranów powstaje jako osobny „eksperyment”, po kilku tygodniach masz kolekcję osobnych wysp kodu bez wspólnego języka. Trudno je wtedy połączyć w całość.

Łatwiej zacząć od myślenia modułami: ekrany i systemy. Ekran to miejsce, w którym gracz coś robi. System to zestaw funkcji, które działają niezależnie od konkretnego ekranu.

Mapa ekranów gry jako szkic modułów

Zanim zaczniesz kodować, możesz dosłownie narysować prostą mapę prostokątów: „Menu główne → Rozgrywka → Ekran końca gry → Menu główne”. Każdy prostokąt to potencjalny moduł–scena, a strzałki pokazują przejścia między nimi.

Typowy zestaw ekranów w prostej grze 2D:

Taki szkic jest fundamentem, na którym buduje się architekturę gry 2D. Bez niego łatwo popaść w tryb „dodajmy jeszcze to” bez mówienia, gdzie to właściwie ma trafić w strukturze projektu. Jeśli korzystasz z blogów typu Robię Gry, szybko zauważysz, że większość dojrzałych projektów – nawet małych – ma jasny podział na moduły i cykl życia gry.

  • Menu główne – start gry, wyjście, ewentualnie szybkie opcje.
  • Rozgrywka – właściwa gra.
  • Pauza – opcjonalnie, może być warstwą nad sceną rozgrywki.
  • Ekran końca gry – podsumowanie, wynik, przycisk „Zagraj ponownie”.
  • Ekran opcji – głośność, sterowanie, rozdzielczość.

Każdy z tych ekranów to dobra kandydatura na osobną scenę/moduł. Dzięki temu:

  • menu nie musi znać szczegółów logiki poziomów,
  • rozgrywka nie przejmuje się tym, jak rysowane jest menu,
  • ekran końca gry może używać wspólnego systemu UI bez kopiowania kodu z menu.

Systemy przekrojowe: co obsługuje wiele ekranów

Obok ekranów pojawiają się systemy, które działają „w tle”, niezależnie od tego, gdzie jest gracz. Najczęstsze przykłady:

  • System audio – odtwarza dźwięki i muzykę na żądanie innych modułów.
  • System zapisu – zapisuje i odczytuje dane gry.
  • Warstwa logiki a warstwa prezentacji

    Typowy scenariusz: animacja skoku jeszcze nie gotowa, ale ty już klepiesz logikę podwójnego skoku. Po godzinie zaczyna się plątanina: warunek skoku jest w tej samej funkcji, która odpala animację, dźwięk i trzęsie kamerą. Próbujesz zmienić jedną rzecz i coś innego się sypie.

    Łatwiej żyje się, gdy podzielisz postać (i inne obiekty) na minimum dwie warstwy:

  • logika – co obiekt ma zrobić (prędkości, stany, punkty życia),
  • prezentacja – jak to wygląda i brzmi (sprite, animacje, efekty, dźwięki).

W praktyce oznacza to dwie klasy lub dwa zbiory funkcji. Przykładowy podział:

  • PlayerController – liczy ruch, pilnuje stanów (stoi, skacze, spada), przyjmuje wejście, decyduje o zadaniu obrażeń,
  • PlayerView – słucha zmian stanu gracza i włącza odpowiednią animację, zmienia kolor sprite’a przy trafieniu, odpala efekt cząsteczkowy.

Logika może powiedzieć: „gracz wszedł w stan Jumping” lub „gracz dostał 10 obrażeń”. Prezentacja reaguje: „odpal animację skoku”, „pokaż miganie na czerwono”. Dzięki temu:

  • możesz prototypować logikę bez gotowej grafiki,
  • zamiana assetów (inne sprite’y, nowe animacje) nie rusza logiki,
  • łatwiej testować – da się uruchomić symulację bez renderowania sceny.

Dobrym sygnałem ostrzegawczym jest sytuacja, gdy wewnątrz klasy od logiki pojawiają się bezpośrednie wywołania konkretnej animacji, nazw dźwięków czy ścieżek do sprite’ów. To znak, że warto coś wydzielić.

Projektowanie modułu rozgrywki: od świata do jednostek

Moment, w którym pojawia się pierwszy przeciwnik, często kończy się skopiowaniem kodu gracza i podmienieniem kilku linijek. Działa? Działa. Problem przychodzi przy trzecim typie przeciwnika, kiedy w projekcie krąży już pięć lekko różnych wersji tej samej logiki kolizji.

Dużo czytelniej jest potraktować „świat gry” i „jednostki w świecie” jako osobne elementy:

  • świat/poziom – mapa, kafelki, platformy, spadanie w przepaść, spawn punktów,
  • jednostki – gracz, wrogowie, pociski, interaktywne obiekty (skrzynie, przełączniki),
  • reguły – co się dzieje, gdy obiekt uderzy w ścianę, przepaść, przeciwnika, monetę.

Prosty układ modułu rozgrywki dla gry 2D może wyglądać następująco:

  • World – zna layout poziomu, potrafi odpowiedzieć: „tu jest ściana”, „tu jest przepaść”,
  • Entity (lub Actor) – bazowa klasa dla wszystkich rzeczy, które mają pozycję i mogą się ruszać,
  • Player, Enemy, Bullet – konkretne jednostki dziedziczące po Entity,
  • CollisionSystem – moduł, który zbiera listę jednostek i sprawdza ich zderzenia ze światem oraz między sobą,
  • Rules lub GameplayLogic – decyduje, co się dzieje przy konkretnych kolizjach (gracz + wróg, pocisk + ściana, gracz + moneta).

Taki podział sprawia, że zmiana fizyki (np. inna grawitacja) czy dodanie nowego typu przeciwnika nie wymaga grzebania w starej logice na chybił trafił. Nowy wróg po prostu wchodzi w ten sam system kolizji i reguł.

Moduł wejścia: sterowanie bez twardego wiązania

Typowe potknięcie: przycisk skoku jest wciśnięty, więc wprost w kodzie gracza sprawdzasz if (IsKeyDown(SPACE)) .... Działa, dopóki nie chcesz dodać pada albo zmiany klawiszy.

Moduł wejścia można potraktować jak tłumacza między urządzeniem a językiem gry. Zamiast:

if (IsKeyDown(SPACE)):
    player.jump()

lepiej wprowadzić warstwę pośrednią:

  • InputSystem – zbiera surowe dane z klawiatury/pada/myszki,
  • InputActions – tłumaczy je na akcje gry: MoveLeft, MoveRight, JumpPressed, PausePressed.

Wtedy logika gracza korzysta z abstrakcji:

if (inputActions.JumpPressed):
    player.requestJump()

Zmiana klawisza, dodanie wsparcia dla pada czy sterowania dotykowego odbywa się w jednym miejscu – w konfiguracji wejścia. Gracz i reszta gry nie muszą o tym nic wiedzieć.

Moduł audio: dźwięki bez chaosu

Na początku każdy wywołuje dźwięki „na dziko”: PlaySound("hit.wav") w losowych miejscach kodu. Po kilku tygodniach połowa dźwięków jest za głośna, część się nie odtwarza, bo ktoś pomylił ścieżkę, a globalne wyciszanie muzyki to polowanie na wszystkie wywołania.

Prostsze i czytelniejsze podejście to centralny moduł audio:

  • AudioSystem – jedna klasa lub zestaw funkcji zarządzających dźwiękami i muzyką,
  • zamiast ścieżek w kodzie – identyfikatory logiczne: "player_jump", "enemy_hit", "ui_click".

Wywołanie w logice wygląda wtedy tak:

audioSystem.playSfx("player_jump")

A mapowanie "player_jump" → konkretny plik siedzi w jednym miejscu (np. plik konfiguracyjny lub tabela w AudioSystem). Dzięki temu:

  • zmiana głośności wszystkich efektów albo tylko UI to jedna funkcja,
  • łatwo podmienić dźwięk bez ruszania kodu rozgrywki,
  • unikasz literówek w ścieżkach porozrzucanych po projekcie.

Zarządzanie zasobami: jeden magazyn zamiast setek odczytów

Loader sprite’a w pętli gry to klasyk. Za każdym razem, gdy pojawia się nowy pocisk, kod ładuje plik PNG z dysku. Na krótkich testach tego nie widać, ale przy większej liczbie obiektów zaczyna się przycinać, a pamięć puchnie.

Wprowadzenie prostego menedżera zasobów zmienia sytuację. Jego zadaniem jest:

  • wczytać zasób tylko raz,
  • trzymać odniesienie/pointer/identyfikator do niego,
  • oddać ten zasób każdemu, kto o niego poprosi.

Struktura może być naprawdę prosta:

  • ResourceManager – posiada słownik id → zasób,
  • przy pierwszym wywołaniu getTexture("player") wczytuje plik i zapamiętuje,
  • kolejne wywołania zwracają już tylko gotowy obiekt.

Identyfikatory znów możesz trzymać w jednym pliku, np.:

{
  "textures": {
    "player": "assets/sprites/player.png",
    "enemy_basic": "assets/sprites/enemy_basic.png"
  }
}

W architekturze gry widać wtedy jasny przepływ: scena rozgrywki prosi ResourceManager o tekstury i dźwięki, a obiekty, które ich używają, dostają już tylko referencję. Nie martwią się o wczytywanie czy zwalnianie pamięci.

Zapis i stan gry: oddzielenie od logiki chwili

Wiele prototypów nie ma na początku zapisu. Problemy startują, gdy nagle trzeba dodać „kontynuuj od ostatniego poziomu” przed deadlinem. Najszybsza droga – wrzucić wszystko do jednego pliku – działa tylko do pierwszego większego refaktoringu.

Bezpieczniej jest potraktować zapis jako osobny moduł od samego początku, nawet jeśli na start zapisuje tylko jedną liczbę:

W tym miejscu przyda się jeszcze jeden praktyczny punkt odniesienia: Jak i kiedy rozpocząć testy wewnętrzne gry?.

  • SaveData – struktura/klasa opisująca, co gra chce zapamiętać (odblokowane poziomy, najlepszy wynik, ustawienia dźwięku),
  • SaveSystem – kod, który potrafi:
    • zamienić SaveData na plik (np. JSON),
    • wczytać plik i odtworzyć SaveData,
    • obsłużyć błąd, gdy pliku nie ma albo jest uszkodzony.

Logika gry nie musi znać szczegółów formatu pliku. Mówi tylko: „chcę zapisać obecny stan” albo „daj mi aktualne wartości z zapisu”. Przykład przepływu:

  • gracz kończy poziom,
  • moduł rozgrywki aktualizuje SaveData.levelUnlocked = 2,
  • woła SaveSystem.save(SaveData),
  • menu główne przy starcie pyta SaveSystem.load() i na tej podstawie odblokowuje przyciski „Kontynuuj”.

Przy okazji takie rozdzielenie ułatwia testy: można wstrzyknąć „fałszywy” SaveSystem w czasie developmentu, który niczego nie zapisuje na dysk, tylko drukuje dane w konsoli.

Sklejanie modułów: prosta warstwa orkiestrująca

Po kilku tygodniach powstaje zestaw osobnych „klocków”: system wejścia, audio, zasoby, zapis, rozgrywka, UI. Wiele projektów zaczyna wtedy tonąć w zależnościach, bo każdy moduł odwołuje się do każdego.

Znacznie czytelniej jest mieć jedno miejsce, które spaja całość – prosty „kompozytor” gry. Jego obowiązki mogą być niewielkie:

  • utworzyć wszystkie główne systemy przy starcie (audio, wejście, zasoby, zapis),
  • przekazać te systemy do scen (np. przez konstruktor lub kontekst gry),
  • obsłużyć przełączanie scen: menu ↔ rozgrywka ↔ ekran końca gry.

Taki kompozytor może być prostą klasą, np. GameApp albo GameRoot. Dzięki temu:

  • moduły nie tworzą się nawzajem „po cichu”,
  • łatwo zobaczyć, co istnieje globalnie i jakie są powiązania,
  • sceny stają się lżejsze – dostają dostęp do potrzebnych systemów z zewnątrz.

Dobrym nawykiem jest też ograniczenie liczby rzeczy globalnych. Jeśli każdy moduł może w dowolnej chwili złapać „singleton audio”, „singleton zasobów” i „singleton zapisu”, regresje i niechciane zależności pojawiają się szybciej, niż się spodziewasz. Podawanie zależności jawnie (przez parametry, konstruktory) zmusza do przemyślenia powiązań.

Małe refaktoryzacje zamiast wielkiej rewolucji

Bez względu na to, jak dobrze zaplanujesz architekturę gry 2D, po paru tygodniach coś i tak przestanie pasować. To normalne. Najgorsze, co można wtedy zrobić, to czekać na „idealny moment”, żeby napisać wszystko od zera.

Dużo zdrowiej działa rytm małych, częstych zmian:

  • po dodaniu nowej funkcji zastanów się, które dwie–trzy klasy zyskały za dużo odpowiedzialności,
  • wydziel jeden mini–moduł (np. osobny HealthComponent albo ScoreManager),
  • przenoś kod krokami, nie w jeden weekend bez snu.

Przykład z praktyki: masz klasę GameScene, która obsługuje:
logikę fal przeciwników, liczenie punktów, zapis najlepszych wyników, UI wyniku i pauzę. Zamiast rozcinać wszystko, wydziel najpierw sam licznik punktów do ScoreSystem, a po kilku dniach przenieś obsługę pauzy do osobnego kontrolera. Małe operacje są mniej ryzykowne, a architektura rośnie równolegle z grą, zamiast ją blokować.

Najczęściej zadawane pytania (FAQ)

Od czego zacząć planowanie architektury prostej gry 2D?

Najczęściej wygląda to tak: masz pomysł na platformówkę, odpalasz silnik, „jakoś to będzie” i po tygodniu toniesz w chaosie plików. Dużo rozsądniej jest poświęcić jeden wieczór na kartkę papieru lub prostą notatkę, zanim napiszesz pierwszą linię kodu.

Na start odpowiedz sobie konkretnie: jaki to gatunek (platformówka, top-down, puzzle, runner), jakie ekrany będą w grze (menu, rozgrywka, pauza, koniec gry) oraz jakie moduły są potrzebne: input, logika, renderowanie, audio, UI, zarządzanie zasobami i scenami. Potem narysuj prosty schemat: skąd logika dostaje dane wejściowe, co aktualizuje, a co tylko wyświetla stan gry. Taki mini‑plan już sam w sobie porządkuje myślenie i ogranicza późniejsze „gaszenie pożarów”.

Jak podzielić kod gry 2D na moduły, żeby nie powstał bałagan?

Typowy problem początkujących: wszystko ląduje w jednym pliku „Game.cs” albo „Main.gd” i rośnie jak kula śniegowa. Gdy trzeba dodać drugi typ przeciwnika, nagle dotykasz inputu, rysowania i UI w tym samym miejscu, a każde poprawienie buga generuje dwa kolejne.

Bezpieczny podział to kilka prostych modułów, które każdy „robią swoje”:

  • Input – tylko czyta klawiaturę/pada/mysz i zamienia to na komendy typu „ruch w lewo”, „skok”.
  • Gameplay / logika – decyduje, co się dzieje w grze: ruch gracza, kolizje, punkty, życie, AI.
  • Rendering – na podstawie stanu gry tylko rysuje: sprite’y, kafelki, UI.
  • Audio – odtwarza dźwięki i muzykę na podstawie zdarzeń z logiki.
  • System scen/ekranów – przełącza między menu, rozgrywką, pauzą itd.

Jeśli nowa funkcja dotyka głównie jednego modułu, znaczy, że podział jest sensowny. Jeśli wszędzie musisz „pomacać po trochu” – czas wrócić do granic odpowiedzialności.

Jak zaplanować architekturę gry 2D, żeby łatwo dodawać nowe funkcje?

Częsty scenariusz: pierwsza wersja gry działa, ale przy pierwszym „małym” rozszerzeniu (np. drugi typ przeciwnika) trzeba przekopać pół projektu. Problem zwykle nie leży w samej funkcji, tylko w tym, że logika, rysowanie i wejście są ze sobą posklejane na sztywno.

Dobra praktyka to planowanie pod „łatwość zmiany”: jedna decyzja projektowa powinna dotykać jednego głównego miejsca. Nowy przeciwnik? Głównie moduł logiki + ewentualnie konfiguracja w danych. Nowy ekran pauzy? Głównie system scen + osobny UI. Pomaga też prosta zasada: najpierw wypisz „must have” (bez czego gra nie ma sensu), a „nice to have” traktuj jako dodatki, które da się dołożyć bez rozbierania fundamentów.

Czy potrzebuję skomplikowanych wzorców projektowych (ECS, MVC) w pierwszej grze 2D?

Wielu początkujących wpada w pułapkę: ogląda prezentacje o silnikach AAA, czyta o złożonych ECS-ach, warstwach abstrakcji i próbuje to odtworzyć w małej platformówce z trzema poziomami. Kończy się na tym, że więcej czasu idzie na walkę z architekturą niż na robienie gry.

Na pierwsze projekty wystarczy prosta, „ludzka” struktura: kilka modułów, klarowna pętla gry, minimum globalnych zmiennych i wyraźny podział na logikę oraz wyświetlanie. Wzorce mają sens wtedy, gdy faktycznie rozwiązują realny problem w twojej grze, a nie tylko „brzmią profesjonalnie”. Lepiej mieć prosty kod, który rozumiesz po miesiącu przerwy, niż „idealny” wzorzec, którego nie jesteś w stanie utrzymać.

Jak dopasować złożoność architektury do wielkości projektu?

Typowy błąd: projekt, który ma być prostym runnerem na 3 minuty rozgrywki, dostaje plan architektury jak duże RPG z ekwipunkiem, zadaniami i systemem zapisu świata. Po kilku tygodniach zamiast gry masz niedokończony „silnik”, a motywacja topnieje.

Zdrowe podejście to odpowiadać tylko na te potrzeby, o których wiesz, że będą: jeśli nie planujesz rozbudowanego ekwipunku, nie projektuj go „na wszelki wypadek”. Architektura powinna być:

  • wystarczająca dla listy „must have”,
  • otwarta na proste rozszerzenia w miejscach „nice to have”,
  • bez katedr – żadnych ogromnych systemów, których gra realnie nie użyje.
  • Taki pragmatyczny minimalizm sprawia, że projekt idzie do przodu, a nie kręci się w miejscu wokół teoretycznych problemów.

Jak uniknąć bałaganu z globalnymi zmiennymi i singletonami w grze 2D?

Na początku wszystko kusi, żeby zrobić jako singleton: „GameManager”, „AudioManager”, „UIManager”, a do tego kilka globalnych zmiennych „na szybko”. Po tygodniu każdy fragment kodu zna się ze wszystkimi, a zmiana w jednym miejscu rozsypuje trzy inne rzeczy – klasyczny „spaghetti kod”.

Zamiast tego lepiej jasno określić, kto ma prawo wiedzieć o czym. Stan gry (np. poziomy, punkty, życie) powinien być trzymany w jednym, jasno zdefiniowanym miejscu, do którego inne moduły odwołują się przez proste interfejsy lub przekazywanie referencji. Singletony można ograniczyć do faktycznie „systemowych” rzeczy (np. dostęp do zasobów), a nie używać ich jako skrótu „bo tak szybciej”. Im mniej globalnego stanu, tym łatwiej utrzymać porządek i debugować problemy.

Jak zaplanować ekrany (sceny) w grze 2D, żeby nie mieszać logiki i UI?

Częsty obrazek: logika gry, menu, pauza i ekran końca gry siedzą w jednym „update”, oplątane if-ami typu „if (isPaused) { … }”. Działa to do czasu, aż trzeba dodać ustawienia, osobne menu dla gamepada albo ekran wyboru poziomu.

Prostsze i czytelniejsze podejście to wydzielenie systemu scen/ekranów. Każdy ekran (menu główne, rozgrywka, pauza, koniec gry) to osobny moduł z własnym UI i logiką, a „główny” kod tylko decyduje, która scena jest aktywna i przekazuje jej input oraz czas. Dzięki temu rozgrywka nie musi wiedzieć, jak działa menu, a menu nie musi dotykać fizyki i kolizji – ekran odpowiada za swój kawałek gry i nic więcej.

Najważniejsze punkty

  • Spontaniczne „żeby tylko działało” kończy się szybko: bez planu nawet mała platformówka po kilku zmianach zamienia się w chaos, gdzie każda drobna poprawka psuje trzy inne rzeczy.
  • Źródłem bałaganu są zwykle te same grzechy: brak modułów, mieszanie logiki z rysowaniem i wejściem, globalne zmienne do wszystkiego oraz kopiowanie kodu zamiast wspólnych funkcji czy klas.
  • Dobra architektura w małej grze 2D oznacza prosty, jasny układ: wiesz, gdzie jest logika, gdzie render, gdzie input, jak moduły się komunikują i które fragmenty trzeba zmienić przy dodaniu np. nowego przeciwnika.
  • Początkujący nie potrzebuje silnika na poziomie AAA – ważniejsza jest prostota implementacji, łatwość wprowadzania zmian i czytelność projektu dla „przyszłego siebie”, niż wyrafinowane wzorce projektowe.
  • Zbyt ambitne, rozbudowane systemy są tak samo szkodliwe jak ich brak: projekt ugrzęźnie w budowaniu abstrakcyjnej katedry, zamiast dowieźć prostą, działającą rozgrywkę.
  • Minimalny plan architektury powinien powstać przed pierwszą linią kodu i obejmować: listę modułów (input, gameplay, render, audio, UI, sceny, zasoby), przepływ danych oraz granice odpowiedzialności między częściami gry.
  • Architekturę trzeba zawsze dopasować do realnego zakresu gry: projektujesz pod to, co na pewno będzie (np. kilka typów wrogów, parę ekranów), zostawiając tylko rozsądny margines na rozwój, zamiast przygotowywać systemy pod „może kiedyś”.