Tetris

Gra polega na układaniu spadających bloków (tetrominoes) w rzędy. Celem jest budowanie pełnych poziomych linii bez przerw. Każda kompletna linia jest kasowana, a punkty są naliczane. Gra kończy się, gdy bloki nagromadzą się i dotkną górnej granicy planszy, uniemożliwiając pojawienie się nowej figury.

Written By Coder Matthew

Last updated 6 months ago


1) Grafiki użyte w Tetris

Tak to się prezentuje już w samym unity
  • 1. Border

    • Grafika tworząca ramkę wokół planszy.

    • Używany do wizualnego oddzielenia planszy Tetrisa (Tilemapy) od reszty interfejsu użytkownika lub tła.

  • 2. Grid

    • Grafika tła dla siatki lub pola do wyświetlania wyniku/informacji.

    • Używany do rysowania siatki planszy.

  • 3. Ghost

    • Grafika reprezentująca cień spadającej figury.

  • 4. Blue, Cyan, Green, Orange, Purple, Red, Yellow

    • Grafiki kolorowych kafelków dla poszczególnych Tetromino.

    • Są to płaskie grafiki 2D, używane do rysowania figur Tetromino w miejscach, gdzie nie da się użyć standardowych Tiles (np. w oknie podglądu następnej figury lub jako ikony w UI).


2) Hierarchia w Unity

Wszystkie obiekty
  • Main Camera

    • Odpowiada za widok gry, czyli za wyświetlanie planszy i spadających figur. Ustawia projekcję ortograficzną, idealną dla gier 2D opartych na siatce (Tilemap).

    • Jest statyczna i skupiona na obszarze planszy, aby uchwycić całe pole gry Tetrisa.

  • Grid

    • Obiekt Unity Grid (Siatka), który definiuje globalny system współrzędnych labiryntu. Jest to niezbędna struktura nadrzędna dla komponentów Tilemap.

    • Organizator, który zapewnia, że wszystkie Tilemapy (plansza i cień) używają tego samego układu komórek (np. 1x1 jednostka na kafelek).

  • Board

    • Jest to obiekt, do którego przypięty jest skrypt Board, będący sercem mechaniki gry.

    • Kontroluje generowanie nowych figur (SpawnPiece), logikę Game Over, sprawdzanie kolizji (IsValidPosition) oraz usuwanie pełnych linii (ClearLines).

  • Ghost

    • Reprezentuje obiekt wizualizujący miejsce, w którym figura spadnie.

    • Do tego obiektu przypięty jest skrypt Ghost (cień), który śledzi aktywną figurę.

    • Ułatwia graczowi planowanie, pokazując docelową pozycję opadającej figury.

  • Border

    • Obiekt wizualny tworzący ramkę wokół pola gry. Jest to graficzny element (Sprite), który definiuje granice planszy (np. czarna ramka).

    • Służy celom estetycznym, wizualnie oddzielając planszę gry od reszty ekranu i interfejsu.

  • Canvas

    • Jest kontenerem nadrzędnym dla wszystkich elementów interfejsu użytkownika (UI). Zarządza skalowaniem i rozmieszczeniem tekstu na ekranie.

    • Niezbędny do hostowania tekstu wyniku (ScoreText).

  • ScoreText (wewnątrz Canvas)

    • Element tekstowy (TextMeshPro) wyświetlający aktualny wynik gracza. Pokazuje wynik w formacie czterech cyfr.

    • Skrypt Board (w metodzie UpdateScore) na bieżąco aktualizuje ten obiekt.


3) Skrypty użyte w tej grze

Tak właśnie wyglądają gotowe skrypty w unity
  • Board

    • Jest to centralny menedżer i kontroler całej planszy gry. Zarządza logiką czyszczenia i ustawiania bloków na Tilemap, generowaniem nowych figur (SpawnPiece) oraz sprawdzaniem warunków Game Over.

    • Kontroluje kluczowe mechaniki: usuwanie pełnych linii (ClearLines) oraz walidację pozycji (IsValidPosition) każdej figury, zanim ta się przesunie lub obróci.

  • Data

    • Statyczny skrypt (klasa) przechowujący wszystkie stałe dane niezbędne do działania mechaniki Tetrisa.

    • Zawiera definicje kształtów wszystkich bloków (Cells).

    • Zapewnia spójność i zgodność z oficjalnymi regułami Tetrisa, dostarczając geometryczne zasady, których używają skrypty TetrominoData i Piece.

  • Ghost

    • Skrypt tworzący wizualny cień (preview) spadającej figury, wskazujący, gdzie nastąpi Hard Drop.

    • Śledzi aktywną figurę (Piece) i oblicza jej najniższą możliwą pozycję na planszy.

    • Używa oddzielnej warstwy Tilemap do rysowania cienia. Logika jest uruchamiana w LateUpdate, aby cień był zawsze aktualny po wszelkich ruchach figury.

  • Piece

    • Skrypt sterujący pojedynczą, aktywną, spadającą figurą (Tetromino).

    • Zarządza inputem gracza (ruch boczny, Soft/Hard Drop, rotacja) oraz kontroluje timery spadania i blokady.

    • Implementuje zaawansowaną logikę rotacji oraz TestWallKicks, aby umożliwić obrót figury, nawet gdy jest ona zablokowana przy ścianie lub innym bloku.

  • TetrominoData

    • Używa struktury (struct w C++, pozdrawiamy mgr Tomasza Kożaka) używana do definiowania konfiguracji każdego typu bloku.

    • Łączy w sobie wygląd (Tile), typ Tetromino (np. 'I', 'J', 'L') oraz odpowiednie dane geometryczne z klasy Data.

    • Jest używany przez skrypt Board do losowania nowej figury i przekazywania wszystkich jej cech geometrycznych do aktywnego obiektu Piece.


4) Całe skrypty

Tutaj znajdziesz całe skrypty bez owijania w bawełne 😊, każdy blok kodu jest starannie zakomentowany tak żebyś w miarę mógł ogarnąć o co chodzi nawet jak nie znasz unity i C#.

Example
using UnityEngine; using UnityEngine.Tilemaps; using UnityEngine.UI; using TMPro; using UnityEngine.Networking; using System.Collections; [DefaultExecutionOrder(-1)] public class Board : MonoBehaviour { // --- Komponenty Planszy i Aktywne Elementy --- public Tilemap tilemap { get; private set; } public Piece activePiece { get; private set; } // Aktualnie spadająca figura. // --- Konfiguracja i Ustawienia --- public TetrominoData[] tetrominoes; // Definicje wszystkich możliwych bloków. public Vector2Int boardSize = new Vector2Int(10, 20); // Standardowy rozmiar planszy Tetrisa. public Vector3Int spawnPosition = new Vector3Int(-1, 8, 0); // Pozycja, z której spadają nowe figury. // --- Punktacja i UI --- public int score { get; private set; } = 0; public TextMeshProUGUI scoreText; public RectInt Bounds { // [BLOK KOMENTARZY: Właściwość Określająca Granice Planszy] // Oblicza granice (min/max X i Y) planszy, co jest kluczowe do sprawdzania, // czy figura nie wyszła poza obszar gry. get { Vector2Int position = new Vector2Int(-boardSize.x / 2, -boardSize.y / 2); return new RectInt(position, boardSize); } } private void Awake() { // [BLOK KOMENTARZY: Inicjalizacja] // Pobiera komponenty (Tilemap, Piece) i inicjalizuje dane wszystkich Tetromino. tilemap = GetComponentInChildren<Tilemap>(); activePiece = GetComponentInChildren<Piece>(); for (int i = 0; i < tetrominoes.Length; i++) { tetrominoes[i].Initialize(); } } private void Start() { // [BLOK KOMENTARZY: Uruchomienie Gry] // Zeruje wynik i rozpoczyna generowanie pierwszej figury. UpdateScore(0); SpawnPiece(); } // --- Logika Spawnowania i Zakończenia Gry --- public void SpawnPiece() { // [BLOK KOMENTARZY: Generowanie Nowej Figury] // 1. Losuje następny blok (Tetromino) z dostępnych typów. int random = Random.Range(0, tetrominoes.Length); TetrominoData data = tetrominoes[random]; // 2. Inicjalizuje aktywną figurę na pozycji startowej. activePiece.Initialize(this, spawnPosition, data); // 3. Sprawdza, czy figura może w ogóle się pojawić (czy górne pole jest puste). if (IsValidPosition(activePiece, spawnPosition)) { Set(activePiece); // Jeśli tak, rysuje ją. } else { GameOver(); // Jeśli nie, koniec gry. } } public void GameOver() { // [BLOK KOMENTARZY: Koniec Gry] // 1. Wysyła wynik gracza na zewnętrzny serwer. StartCoroutine( SendScore( 20, // id score // score ) ); // 2. Czyści całą planszę (usuwa wszystkie bloki). tilemap.ClearAllTiles(); } // --- Rysowanie i Usuwanie Bloków --- public void Set(Piece piece) { // [BLOK KOMENTARZY: Rysowanie Figury na Tilemapie] // Rysuje wszystkie 4 komórki aktywnej figury na planszy. for (int i = 0; i < piece.cells.Length; i++) { Vector3Int tilePosition = piece.cells[i] + piece.position; tilemap.SetTile(tilePosition, piece.data.tile); } } public void Clear(Piece piece) { // [BLOK KOMENTARZY: Usuwanie Figury z Tilemapy] // Czyści wszystkie 4 komórki aktywnej figury. Zawsze wywoływane przed jej przesunięciem. for (int i = 0; i < piece.cells.Length; i++) { Vector3Int tilePosition = piece.cells[i] + piece.position; tilemap.SetTile(tilePosition, null); } } // --- Logika Kolizji --- public bool IsValidPosition(Piece piece, Vector3Int position) { // [BLOK KOMENTARZY: Sprawdzanie Ważności Pozycji] RectInt bounds = Bounds; // Sprawdza każdą komórkę figury: for (int i = 0; i < piece.cells.Length; i++) { Vector3Int tilePosition = piece.cells[i] + position; // 1. Sprawdzenie granic: Czy figura nie wyszła poza krawędź planszy. if (!bounds.Contains((Vector2Int)tilePosition)) { return false; } // 2. Sprawdzenie zajętości: Czy w docelowej pozycji nie ma już innego bloku. if (tilemap.HasTile(tilePosition)) { return false; } } return true; } // --- Logika Usuwania Linii --- public void ClearLines() { // [BLOK KOMENTARZY: Wykrywanie i Usuwanie Pełnych Linii] RectInt bounds = Bounds; int row = bounds.yMin; int linesCleared = 0; // Iteruje od najniższego rzędu do najwyższego. while (row < bounds.yMax) { if (IsLineFull(row)) { // Jeśli linia pełna, usuwa ją i nie przechodzi do następnego rzędu // (ponieważ nowa linia spadła w to samo miejsce). LineClear(row); linesCleared++; } else { // Jeśli linia niepełna, przechodzi do następnego rzędu. row++; } } // Nalicza punkty za usunięte linie. if (linesCleared > 0) { UpdateScore(linesCleared * 100); } } public bool IsLineFull(int row) { // [BLOK KOMENTARZY: Sprawdzenie Pełnej Linii] // Iteruje przez wszystkie kolumny w danym rzędzie. Jeśli znajdzie choć jedną pustą komórkę, // oznacza to, że linia nie jest pełna. RectInt bounds = Bounds; for (int col = bounds.xMin; col < bounds.xMax; col++) { Vector3Int position = new Vector3Int(col, row, 0); if (!tilemap.HasTile(position)) { return false; } } return true; } public void LineClear(int row) { // [BLOK KOMENTARZY: Usuwanie Linii i Przesuwanie Bloków] RectInt bounds = Bounds; // 1. Czyści usuwaną linię. for (int col = bounds.xMin; col < bounds.xMax; col++) { Vector3Int position = new Vector3Int(col, row, 0); tilemap.SetTile(position, null); } // 2. Przesuwa wszystkie bloki znajdujące się powyżej usuniętej linii w dół o jeden rząd. while (row < bounds.yMax) { for (int col = bounds.xMin; col < bounds.xMax; col++) { // Kopiuje Tile z rzędu wyżej (row + 1). Vector3Int position = new Vector3Int(col, row + 1, 0); TileBase above = tilemap.GetTile(position); // Wkleja Tile do aktualnego rzędu (row). position = new Vector3Int(col, row, 0); tilemap.SetTile(position, above); } row++; } } // --- Aktualizacja Wyniku i Komunikacja Sieciowa --- public void UpdateScore(int points) { // [BLOK KOMENTARZY: Aktualizacja Wyniku] // Dodaje punkty do wyniku i aktualizuje tekst w UI (formatuje na 4 cyfry). score += points; if (scoreText != null) { scoreText.text = score.ToString("D4"); } } public IEnumerator SendScore(int gameId, int score) { // [BLOK KOMENTARZY: Procedura HTTP] // Standardowa korutyna do wysyłania wyniku na zewnętrzny serwer. string url = "https://bit.elita.fun/execs/api/set_score.php?game_id=" + gameId + "&score=" + score; WWWForm f = new WWWForm(); f.AddField("pass", System.DateTime.Now.ToString("mm") + "EcS9YcvMtWNnW0N8yi1mVK"); using (UnityWebRequest r = UnityWebRequest.Post(url, f)) { yield return r.SendWebRequest(); Debug.Log(r.result == UnityWebRequest.Result.Success ? "Wynik przesłany: " + r.downloadHandler.text : "BŁĄD WYSYŁANIA WYNIKU: " + r.error); } } }