Articles

Unity: jak programowo utworzyć Tilemapę 2D

dla samouczka wideo Kliknij tutaj

Jeśli chcesz utworzyć różne Tilemapy o różnych rozmiarach sprawdź drugą część tego samouczka

komponent Tilemap został wprowadzony w Unity 2017.2 i znacznie ułatwił proces tworzenia gier 2D. Wraz z wersją 2018.3 wprowadzono izometryczną Tilemapę zapewniającą świetne wsparcie dla gier 2.5 D. Ostatnio miałem okazję ściśle współpracować z tym komponentem i postawiłem sobie za zadanie tworzenie kafelków programowo. W mojej ostatniej grze mam wiele poziomów i wszystkie z nich mają tę samą planszę opartą na Tilemapie. Ale sama płyta ma unikalną konfigurację. Oczywiście, będąc profesjonalnym programistą nie chciałem tworzyć 60 scen z gry i malować wszystkich poziomów ręcznie, ale raczej mieć mechanizm, aby wypełnić planszę odpowiednimi elementami w zależności od danego wejścia. Jeśli jesteś ciekawy, jak wygląda efekt końcowy tutaj jest link do gry.

kod źródłowy jest dostępny na GitHub, proszę, znajdź link na końcu tego samouczka.

używam najnowszego dostępnego Unity 2019.2.0.1 f. Aby pracować z tym samouczkiem, powinieneś mieć co najmniej wersję 2018.3. Użyjemy siatki izometrycznej, ale opisana technika ma zastosowanie do każdego typu.

przed rozpoczęciem zdecydowanie sugeruję przeczytanie tego genialnego izometrycznego środowiska 2D z wpisem na blogu Tilemap, aby uzyskać podstawową wiedzę na temat izometrycznej Tilemap.

ponieważ pracujemy w środowisku 2D, powinieneś skonfigurować nowy projekt 2D (patrz 2DAnd3DModeSettings)

do osiągnięcia naszego celu wymagane są 3 główne komponenty: plansza do gry, na której odbędzie się gra, miejsce, w którym można zachować opis poziomu i jakiś kod, który łączy się ze sobą.

Cz.1. Tworzenie tablicy

Zacznijmy od zaimportowania potrzebnych zasobów obrazu. Użyję tych samych obrazów, których użyłem w mojej grze:

Used to create a base tilemap level

Used as a path

Used to mark the start and end points of a ścieżka

obrazy będziemy przechowywać w folderze kafelki, po prostu przeciągnij i upuść je tam.

Konfiguracja płytek

uwaga! Powinieneś mieć ustawiony prawidłowy rozmiar „piksela na jednostkę” na każdy obraz płytki (aby uzyskać więcej informacji, zobacz izometryczne środowiska 2D z Tilemap). Dla tych obrazów wartość wynosi 1096.

konfiguracja obrazu

dobrą praktyką jest rozdzielanie różnych warstw na scenie na dedykowane obiekty z opisowymi nazwami. Wyobraź sobie typowy ekran gry, który może zawierać strefę gry, interfejs użytkownika, miejsca docelowe reklam i tak dalej.

zgodnie z tym podejściem stwórzmy nowy pusty obiekt gry o nazwieGameZone, w którym można umieścić planszę i wszystkie elementy gry.

teraz nadszedł czas, aby utworzyć rzeczywiste płytki z wcześniej zaimportowanych obrazów

naciśnij okno -> 2D -> paleta płytek

paleta kafelków

. Kliknij „Utwórz nową paletę” i utwórz paletę:

New Palette

Drag and drop tile images one by one to create an actual tiles.

Tiles setup

With those tiles we can finally create the game board which will be filled with elements programmatically on a level startup later.

ZnajdźGameZone w widoku hierarchii kliknij prawym przyciskiem myszy → obiekt 2D → Mapa izometryczna. Zostanie utworzony nowy obiekt gry Grid.

rozmiar planszy będzie 11×11 płytek i możemy zacząć malować. Wybierz narzędzie Pędzel skrzynkowy (czwarty element od lewej) w widoku „paleta kafelków”, a następnie wybierz kafelek” czysty”. Namaluj go w widoku sceny.

po zakończeniu malowania należy ręcznie skompresować granice TILEMAP. W tym celu wybierz Tilemap w widoku hierarchii, naciśnij przycisk Ustawienia (bieg) na komponencie Tilemap i wybierz „Compress Tilemap Bounds”

dobra robota, plansza jest gotowa do użycia! Wrócimy do tego w części 3.

Cz.2. Dane poziomu

ponieważ chcemy ponownie użyć tej samej sceny i tej samej planszy, dane poziomu powinny być gdzieś przechowywane. Prostym rozwiązaniem jest prosty plik json, który opisuje, jak każdy poziom powinien być zbudowany.

ważne jest, aby zrozumieć, co staramy się osiągnąć, dlatego muszę powiedzieć kilka słów o mechanice. W grze znajdują się obiekty, które poruszają się wzdłuż ścieżki od początku do końca (podobnie jak w Zuma), a celem gracza jest zniszczenie ich wszystkich. W tym samouczku stworzymy tę ścieżkę, która będzie unikalna dla każdego poziomu.

ok, wracając do projektu.

istnieje wiele sposobów dostępu do zewnętrznych danych w środowisku wykonawczym ze skryptu. Tutaj będziemy używać folderów zasobów

stwórzmy nowy folder-pliki – i podkatalog Resources. To jest miejsce, w którym chcemy przechowywać dane, więc utwórz nowe poziomy plików.json i umieść go tam.

dla celów tutorialu będziemy mieli tylko dwa pola do opisania każdego poziomu:

  • number — int do identyfikacji poziomu
  • path — ścieżka oparta na kafelkach, którą chcemy utworzyć programowo. Tablica wartości, gdzie pierwsza wartość jest punktem początkowym, a ostatnia jest punktem końcowym.

To jest plik, którego będę używał. Nie martw się o te wartości w path, do tego dojdziemy później.

{
"levels":
},
{
"number": 2,
"path":
},
{
"number": 3,
"path":
}
]
}

stwórzmy kolejny folder — skrypty — i teraz zaczyna się kodowanie.

chcemy uzyskać dostęp do danych z pliku w kodzie, dlatego potrzebujemy do tego klasy modelu. Czas stworzyć nasz pierwszy skrypt –LevelsData. Nie jest przeznaczony do tworzenia instancji przez Unity, więc MonoBehaviour I StartUpdate metody powinny zostać usunięte. Z powyższego pliku możemy zobaczyć, że głównym elementem jest array poziomów, gdzie każdy poziom powinien mieć jeden int numer pola i jeden int array ścieżka pola. Nie zapomnij również umieścić adnotacji.


public class LevelsData
{
public LevelData levels;
public class LevelData
{
public int number;
public int path;
}
}

miło, teraz mamy plik i model. Następnym krokiem jest przekształcenie jednego w drugiego. Stwórzmy kolejny skrypt — GameZone — I dołączmy go do obiektu GameZone na scenie. Ten skrypt zostanie później użyty do Ustawienia całej planszy.

zgodnie z zasadą jednej odpowiedzialności stwórzmy jeszcze jeden skrypt — LevelsDataLoader — który wykona całą transformację. Dołącz go również do obiektuGameZone.

Ta klasa załaduje Dane i zwróci je jako słownik, gdzie kluczem jest numer poziomu, a danymi same dane poziomu.

teraz powinniśmy mieć dostęp do danych w skrypcie GameZone.

public class GameZone : MonoBehaviour
{
private Dictionary<int, LevelsData.LevelData> _levelsData;
private LevelsDataLoader _dataLoader;private void Awake()
{
_dataLoader = GetComponent<LevelsDataLoader>();
}private void Start()
{
_levelsData = _dataLoader.ReadLevelsData();Debug.Log(_levelsData.Count + " levels have been stored in the dictionary!");
}
}

Przełącz z powrotem na Unity, naciśnij przycisk Odtwórz i sprawdź konsolę — powinieneś zobaczyć komunikat „3 poziomy zostały zapisane w słowniku!”

Cz.3. Łączenie płyty i danych

Gratulacje, dotarłeś do ostatniej i najciekawszej części tego poradnika. Jak faktycznie połączymy tablicę z danymi? Czytaj dalej, aby dowiedzieć się!

przede wszystkim umieśćmyhorizontal Istart_stop płytki utworzone w pierwszej części samouczka w folderze zasoby w folderze kafelki. Następnie dodaj nowy skrypt –TilesResourcesLoader — statyczną klasę pomocniczą do ładowania kafelków z tego folderu w środowisku wykonawczym.

public static class TilesResourcesLoader
{
private const string PathHorizontal = "horizontal";
private const string StartStop = "start_stop"; public static Tile GetPathHorizontalTile()
{
return GetTileByName(PathHorizontal);
} public static Tile GetStartStopTile()
{
return GetTileByName(StartStop);
} private static Tile GetTileByName(string name)
{
return (Tile) Resources.Load(name, typeof(Tile));
}
}

jako ostatni krok powinniśmy umieścić te płytki na planszy podczas uruchamiania sceny. Wróćmy do skryptuGameZone. Przede wszystkim musimy symulować wybór poziomu, w prawdziwej grze zwykle dzieje się to, gdy użytkownik naciśnie przycisk poziomu. Dla uproszczenia dodajmy publiczny poziom pola do GameZone I zmienimy jego wartość na 1 dla start. Najpierw pokażę wam ostatni skrypt:

public class GameZone : MonoBehaviour
{
public int Level; private const int FieldLineSize = 11;
private const int FieldTotalTiles = FieldLineSize * FieldLineSize;
private Dictionary<int, LevelsData.LevelData> _levelsData; private void Start()
{
_levelsData = GetComponent<LevelsDataLoader>().ReadLevelsData();
SetupTiles();
} private void SetupTiles()
{
var baseLevel = GetComponentsInChildren<Tilemap>(); var localTilesPositions = new List<Vector3Int>(FieldTotalTiles);
foreach (var pos in baseLevel.cellBounds.allPositionsWithin)
{
Vector3Int localPlace = new Vector3Int(pos.x, pos.y, pos.z);
localTilesPositions.Add(localPlace);
} SetupPath(localTilesPositions, baseLevel);
} private void SetupPath(List<Vector3Int> localTilesPositions, Tilemap baseLevel)
{
var path = _levelsData.path;
var pathHorizontalTile = TilesResourcesLoader.GetPathHorizontalTile();
var first = path.First();
var last = path.Last();
foreach (var localPosition in localTilesPositions.GetRange(first, Math.Abs(first - last)))
{
baseLevel.SetTile(localPosition, pathHorizontalTile);
} var startStopTile = TilesResourcesLoader.GetStartStopTile();
baseLevel.SetTile(localTilesPositions, startStopTile);
baseLevel.SetTile(localTilesPositions, startStopTile);
}
}

Wow, to dużo akcji! Pozwól, że cię przez to poprowadzę.

w metodzieSetupTiles powinniśmy najpierw uzyskać samą tilemapę, ponieważ musimy znać położenie kafelków, aby ją zmienić. Aby to osiągnąć, używamy metody tilemap.cellBounds.allPositionsWithin, która zwraca wszystkie pozycje kafelków począwszy od pierwszego — w danej konfiguracji jest to kafelek najbardziej w dół.

patrz na poniższy obrazek, gdzie każda liczba reprezentuje indeks w liścielocalTilesPositions.

numbered tilemap

czy pamiętasz wartości, których używamy w ścieżce w Levels.json? Jak można się już domyślić, wartości te są indeksami płytek w tablicy. Wszystko, co musisz teraz zrobić, to mieć obraz cheatsheet, który pomoże Ci budować poziomy. Tego używam podczas rozwoju:

Moja brzydka numerowana tilemap

w tym przykładzie ustawiamy poziomą linię w ścieżce, proszę odnieść się do metody SetupPath. Kluczową częścią jest następująca pętla:

foreach (var localPosition in localTilesPositions.GetRange(first, Math.Abs(first - last)))
{
baseLevel.SetTile(localPosition, pathHorizontalTile);
}

tutaj iteraczymy nadlocalTilesPositions, aby znaleźć te, które mają ustawić żądany kafelek — poziomy w tym przypadku.

Uwaga! GetRange metoda ma dwa parametry — indeks i licznik.

do oznaczania pozycji początku i końca ścieżki służy płytka start_stop.

oto wynik naszej ciężkiej pracy:

wynik końcowy

teraz spróbuj zmienić pole numer poziomu skryptuGameZone z 1 na 2 lub 3, a zobaczysz, że ścieżka jest ładowana poprawnie dla danego poziomu.

potem