Як написати свій Тетріс на Java за півгодини?


Дізнайтесь більше про нові кар'єрні можливості в EchoUA. Цікаві проекти, ринкова оплата, гарний колектив. Надсилайте резюме та приєднуйтеся до нас.

У попередніх статтях цієї серії ми вже встигли написати сапера, змійку і десктопний клон гри 2048. Спробуємо тепер написати свій Тетріс.

Нам, як завжди, знадобляться:

  • 30 хв вільного часу;
  • налагоджене робоче середовище, тобто JDK і IDE (наприклад, Eclipse);
  • бібліотека LWJGL (версії 2.x.x) для роботи з графікою (опціонально). Зверніть увагу на те, що для LWJGL версій вище 3-ї потрібно буде написати код, що відрізняється від наведеного в статті;
  • спрайт, тобто картинки клітинок усіх можливих станів (порожня, і з мірами двійки до 2048). Можна намалювати самому, або скачати статті, що використовувалися при написанні.

До того як ставити запитання в коментарях, не забудьте переглянути попередні статті, можливо, там на нього вже давалася відповідь. Вихідний код готового проекту традиційно можна знайти на GitHub.

З чого розпочати?

Починати варто з головного керівного класу, який в нашому проекті розташований вище за інших по рівню абстракції. Взагалі відмінна порада – на початку роботи завжди пишіть код виду if (getKeyPressed()) doSomething (), так Ви швидко визначите фронт робіт.

public static void main (String[] args) { initFields (); while (!endOfGame){ input (); logic (); graphicsModule.draw (gameField); graphicsModule.sync (FPS); }
graphicsModule.destroy ();}

Це наш main (). Він нічим принципово не відрізняється від тих, що ми писали в попередніх статтях – ми так само ініціалізували поля і, поки гра не закінчиться, здійснюємо по черзі: введення користувацьких даних (input ()), основні ігрові дії (logic ()) і виклик методу відмальовки у графічного модуля (graphicsModule.draw ()), в який передаємо поточне ігрове поле (gameField). З нового хіба що метод sync – метод, який повинен буде гарантувати нам певну частоту виконання ітерацій. За його допомогою ми зможемо задати швидкість падіння фігури в клітинках на секунду.

Ви могли помітити, що в коді використана константа FPS. Усі константи зручно визначати в класі з public static final полями. Повний список констант, який нам знадобиться в ході розробки, можна подивитися в класі Constants на GitHub.

Залишимо ініціалізацію полів на потім. Розберемося спочатку з input () і logic ().

Отримання даних від користувача

Код, чесно кажучи, капітанський:

private static void input (){ /// Оновлюємо дані модуля введення keyboardModule.update ();
/// Прочитуємо з модуля введення напрям для зрушення фігурки shiftDirection = keyboardModule, що падає.getShiftDirection (); /// Прочитуємо з модуля введення, чи хоче користувач повернути фігурку isRotateRequested = keyboardModule.wasRotateRequested (); /// Прочитуємо з модуля введення, чи хоче користувач " впустити" фігурку вниз isBoostRequested = keyboardModule.wasBoostRequested (); /// Якщо був натиснутий ESC або " хрестик" вікна, завершуємо гру endOfGame = endOfGame || keyboardModule.wasEscPressed () || graphicsModule.isCloseRequested ();}

Усі дані від введення ми просто зберігаємо у відповідних полях, дії на основі їх виконуватиме метод logic ().

Тепер уже стає зрозумілим, що нам необхідно. По-перше, нам потрібні клавіатурний і графічний модулі. По-друге, треба якось зберігати напрям, який гравець вибрав для зрушення. По-друге, створимо enum з трьома станами: AWAITING, LEFT, RIGHT. Навіщо потрібний AWAITING? Щоб зберігати інформацію про те, що зрушення не потрібно (використання в програмі null слід усіма силами уникати). Перейдемо до інтерфейсів.

Інтерфейси для клавіатурного і графічного модулів

Оскільки багатьом користувачам не подобається, що я пишу ці модулі на LWJGL, то я вирішив у статті приділити час тільки інтерфейсам цих класів. Кожен може написати їх за допомогою GUI-бібліотеки, яка йому подобається (чи взагалі зробити консольний варіант). Я реалізував їх на LWJGL, код можна подивитися тут у теках graphics/lwjglmodule і keyboard/lwjglmodule.

Інтерфейси, після додавання в них усіх згаданих вище методів, виглядатимуть таким чином:

public interface GraphicsModule { /** * Отрисовывает передане ігрове поле * * @param field Ігрове поле, яке необхідно відмалювати */
void draw (GameField field); /** * @return Повертає true, якщо у вікні натиснутий "хрестик" */ boolean isCloseRequested (); /** * Завершальні дії, на випадок, якщо модулю треба підчистити за собою. */ void destroy (); /** * Примушує програму трохи поспати, якщо останній раз метод викликався * менш ніж 1/fps секунд назад */ void sync (int fps);}
public interface KeyboardHandleModule { /** * Прочитування останніх даних зі стека подій, якщо модулю це необхідно */ void update (); /** * @return Повертає інформацію про те, чи був натиснутий ESCAPE за останню ітерацію */ boolean wasEscPressed (); /** * @return Повертає напрям, в якому користувач хоче зрушити фігуру.
* Якщо користувач не намагався зрушити фігуру, повертає ShiftDirection.AWAITING. */ ShiftDirection getShiftDirection (); /** * @return Повертає true, якщо користувач хоче повернути фігуру. */ boolean wasRotateRequested (); /** * @return Повертає true, якщо користувач хоче прискорити падіння фігури. */ boolean wasBoostRequested ();}

Відмінно, ми отримали від користувача дані. Що далі?

А далі ми повинні ці дані обробити і щось зробити з ігровим полем. Якщо користувач сказав зрушити фігуру кудись, то передаємо полю, що треба зрушити фігуру в такому-то напрямі. Якщо користувач сказав, що треба фігуру повернути, то повертаємо, і так далі. Крім цього, не можна забувати, що 1 раз в FRAMES_PER_MOVE (Ви ж відкривали файл з константами?) ітерацій нам необхідно зрушувати фігуру, що падає, вниз.

private static void logic (){ if (shiftDirection != ShiftDirection.AWAITING){ // Якщо є запит на зрушення фігури /* Пробуємо зрушити */ gameField.tryShiftFigure (shiftDirection); /* Чекаємо нового запиту */ shiftDirection = ShiftDirection.AWAITING; } if (isRotateRequested){ // Якщо є запит на поворот фігури /* Пробуємо повернути */
gameField.tryRotateFigure (); /* Чекаємо на новий запит */ isRotateRequested = false; } /* Падіння фігури вниз відбувається якщо loopNumber % FRAMES_PER_MOVE == 0 * Т.е. 1 раз за FRAMES_PER_MOVE ітерацій. */ if ( (loopNumber %  (FRAMES_PER_MOVE /  (isBoostRequested ? BOOST_MULTIPLIER: 1)) ) == 0) gameField.letFallDown (); /* Збільшення номера ітерації (по модулю FPM) */ loopNumber =  (loopNumber+1) %  (FRAMES_PER_MOVE);

Сюди ж додамо перевірку на переповнювання поля (у Тетрисі гра завершується, коли фігурам нікуди падати) :

/* Якщо поле переповнене, гра закінчена */ endOfGame = endOfGame || gameField.isOverfilled (); }

Так, а тепер ми напишемо клас для того магічного gameField, у який ми все це передаємо, так?

Не зовсім. Спочатку ми пропишемо поля класу Main і метод initFields (), щоб закінчити з ним. Ось всі поля, які ми використали:

/** Прапор для завершення основного циклу програми */ private static boolean endOfGame; /** Графічний модуль гри*/ private static GraphicsModule graphicsModule; /** "Клавіатурний" модуль гри, тобто модуль для читання запитів з клавиатуры*/ private static KeyboardHandleModule keyboardModule; /** Ігрове поле. Див. документацію GameField */ private static GameField gameField; /** Напрям для зрушення, отриманий за останню ітерацію */ private static ShiftDirection shiftDirection; /** Чи був за останню ітерацію запрошений поворот фігури */ private static boolean isRotateRequested;
/** Чи було за останню ітерацію запрошено прискорення падіння*/ private static boolean isBoostRequested; /** Номер ігрової ітерації по модулю FRAMES_PER_MOVE. * Падіння фігури вниз відбувається якщо loopNumber % FRAMES_PER_MOVE == 0 * Т.е. 1 раз за FRAMES_PER_MOVE ітерацій. */ private static int loopNumber;

Ініціалізувати ми їх будемо так:

private static void initFields (){ loopNumber = 0; endOfGame = false; shiftDirection = ShiftDirection.AWAITING; isRotateRequested = false; graphicsModule = new LwjglGraphicsModule (); keyboardModule = new LwjglKeyboardHandleModule (); gameField = new GameField ();}

Якщо Ви вирішили не використовувати LWJGL і написали свої класи, що реалізують GraphicsModule і KeyboardHandleModule, то тут треба вказати їх конструктори замість new LwjglGraphicsModule () і new LwjglKeyboardHandleModule ().

Тепер ми переходимо до класу, який відповідає за зберігання інформації про ігрове поле та її оновлення.

Клас GameField

Цей клас повинен, по-перше, зберігати інформацію про поле і про фігуру, що падає, а по-друге, містити методи для їх оновлення, і отримання про них інформації, крім тих, які ми вже використали, необхідно написати метод, що повертає колір осередку за координатами, щоб графічний модуль міг відмалювати поле.

Почнемо за порядком.

Зберігати інформацію про поле

/** Кольори осередків поля. Для порожніх осередків використовується константа EMPTINESS_COLOR */ private TpReadableColor[][] theField; /** Кількість непорожніх осередків рядка. * Можна було б отримувати динамічно з theField, але це довше. */ private int[] countFilledCellsInLine;

…і про фігуру, що падає

 /** Інформація про фігуру */ private Figure figure, що падає в даний момент;

TpReadableColor – простий enum, що містить елементи з промовистими назвами (RED, ORANGE і т. ін.),  і метод, що дозволяє отримати випадковим чином один із цих елементів. Нічого особливого в ньому немає, код можна подивитися тут.

Це все поля, які нам знадобляться. Як відомо, поля люблять бути ініціалізованими.

Зробити це слід в конструкторі.

Конструктор та ініціалізація полів

public GameField (){ spawnNewFigure (); theField = new TpReadableColor[COUNT_CELLS_X][COUNT_CELLS_Y+OFFSET_TOP]; countFilledCellsInLine = new int[COUNT_CELLS_Y+OFFSET_TOP];

“Що це за OFFSET_TOP?” – запитаєте Ви. OFFSET_TOP – це кількість осередків, що не відображаються згори, в яких створюються фігури, що падають. Якщо фігура не зможе “випасти” з цього простору, і хоч один з осередків theField, що виявиться вищим за рівень COUNT_CELLS_Y , буде заповнений, це означатиме, що поле переповнене і користувач програв, тому OFFSET_TOP має бути більше нуля.

Далі в конструкторі варто заповнити масив theField значеннями константи EMPTINESS_COLOR , а countFilledCellsInLine – нулями (друге в Java не вимагається, за ініціалізації масиву все int‘и дорівнюють 0). Чи можна створити декілька шарів вже заповнених осередками? На GitHub Ви можете побачити реалізацію саме другого варіанта.

А що це там за spawnNewFigure ()? Чому ініціалізація фігури винесена в окремий метод?

Ви правильно здогадалися, spawnNewFigure () дійсно ініціалізував поле figure. В окремий метод це винесено, тому що нам доведеться робити ініціалізацію щоразу, коли створюватиметься нова фігура.

/** * Створює нову фігуру в невидимій зоні * X-координата для генерації не має бути розташована ближче до правого краю, * чим максимальна ширина фігури (MAX_FIGURE_WIDTH), щоб влізти в екран */ private void spawnNewFigure (){ int randomX = new Random ().nextInt (COUNT_CELLS_X - MAX_FIGURE_WIDTH); this.figure = new Figure (new Coord (randomX, COUNT_CELLS_Y + OFFSET_TOP - 1)); }

На цьому зі збереженням даних ми закінчили. Переходимо до методів, які віддають інформацію про поле іншим класам.

Методи, що передають інформацію про ігрове поле

Таких методів два. Перший повертає колір осередку (для графічного модуля):

public TpReadableColor getColor (int x, int y) { return theField[x][y];}

А другий повідомляє, чи переповнене поле (як це відбувається, ми розібрали вище):

public boolean isOverfilled (){ for (int i = 0; i < OFFSET_TOP; i++){ if (countFilledCellsInLine[COUNT_CELLS_Y+i] != 0) return true; } return false;}

Методи, які оновлюють фігуру та ігрове поле

Почнемо реалізовувати методи, які ми викликали з Main.logic ().

Зрушення фігури

За це відповідає метод tryShiftFigure (). У коментарях до його виклику з Main було сказано, що він “пробує зрушити фігуру”. Чому пробує? Якщо фігура розташована впритул до стіни, а користувач намагається її зрушити у напрямі цієї стіни, жодного зрушення в реальності відбуватися не повинно. Так само не можна зрушити фігуру в статичні осередки на полі.

public void tryShiftFigure (ShiftDirection shiftDirection) { Coord[] shiftedCoords = figure.getShiftedCoords (shiftDirection); boolean canShift = true; for (Coord coord: shiftedCoords) { if ((coord.y<0 || coord.y>=COUNT_CELLS_Y+OFFSET_TOP) ||(coord.x<0 || coord.x>=COUNT_CELLS_X) || ! isEmpty (coord.x, coord.y)){ canShift = false; } } if (canShift){
figure.shift (shiftDirection); }}

Що ми зробили в цьому методі? Ми запросили у фігури осередки, які б вона зайняла у разі зрушення. Потім для кожного з цих осередків ми перевіряємо, чи не виходить він за межі поля, і чи не знаходиться по її координатах у сітці статичний блок. Якщо хоч один осередок фігури виходить за межі або намагається стати на місце блоку – зрушення не відбувається. Coord тут – клас-оболонка з двома публічними числовими полями (x і y координати).

Повертання фігури

Логіка аналогічна зрушенню:

Coord[] rotatedCoords = figure.getRotatedCoords (); boolean canRotate = true; for (Coord coord: rotatedCoords) { if ((coord.y<0 || coord.y>=COUNT_CELLS_Y+OFFSET_TOP) ||(coord.x<0 || coord.x>=COUNT_CELLS_X)
||! isEmpty (coord.x, coord.y) ){ canRotate = false; } } if (canRotate){ figure.rotate (); }

Падіння фігури

Спочатку код точно повторює попередні два методи:

public void letFallDown () { Coord[] fallenCoords = figure.getFallenCoords (); boolean canFall = true; for (Coord coord: fallenCoords) { if ((coord.y<0 || coord.y>=COUNT_CELLS_Y+OFFSET_TOP) ||(coord.x<0 || coord.x>=COUNT_CELLS_X) ||! isEmpty (coord.x, coord.y)){ canFall = false; } } if (canFall) { figure.fall ();

Проте тепер, у разі якщо фігура далі падати не може, нам необхідно перенести її осередки (“кубики”) theField, тобто в розряд статичних блоків, після чого створити нову фігуру:

 } else { Coord[] figureCoords = figure.getCoords (); /* Прапор, що вказує на те, що необхідно змістити лінії вниз *  (тобто якась лінія була знищена) */ boolean haveToShiftLinesDown = false; for (Coord coord: figureCoords) { theField[coord.x][coord.y] = figure.getColor (); /* Збільшуємо інформацію про кількість статичних блоків в линии*/ countFilledCellsInLine[coord.y]++; /* Перевіряємо, чи повністю заповнений рядок Y * Якщо заповнена повністю, встановлюємо haveToShiftLinesDown в true */
haveToShiftLinesDown = tryDestroyLine (coord.y)  || haveToShiftLinesDown; } /* Якщо це необхідно, зміщуємо лінії на порожнє місце */ if (haveToShiftLinesDown) shiftLinesDown (), що утворилося; /* Створюємо нову фігуру замість тієї, яку ми перенесли*/ spawnNewFigure (); }

Оскільки в результаті перенесення осередків якась лінія може заповнитися повністю, після кожного додавання осередку ми перевіряємо лінію, в яку ми її додали, на повноту:

private boolean tryDestroyLine (int y) { if (countFilledCellsInLine[y] < COUNT_CELLS_X){ return false; } for (int x = 0; x < COUNT_CELLS_X; x++){ theField[x][y] = EMPTINESS_COLOR; } /* Не забуваємо оновити мета-информацию! */ countFilledCellsInLine[y] = 0; return true;}

Цей метод повертає істину, якщо лінію вдалося знищити. Після додавання всіх цеглинок фігури в сітку (і видалення всіх заповнених ліній), ми, за необхідності, запускаємо метод, який зрушує на місце порожніх ліній непорожні:

private void shiftLinesDown (){ /* Номер виявленої порожньої лінії (- 1, якщо не виявлена) */ int fallTo = - 1; /* Перевіряємо лінії знизу вверх*/ for (int y = 0; y < COUNT_CELLS_Y; y++){ if (fallTo == - 1){ //Якщо порожнеч ще не виявлене if (countFilledCellsInLine[y] == 0) fallTo = y; //...намагаємося виявити (._.) } else { //А якщо виявлене if (countFilledCellsInLine[y] != 0){ // І поточну лінію є сенс зрушувати... /* Зрушуємо... */ for (int x = 0; x < COUNT_CELLS_X; x++){
theField[x][fallTo] = theField[x][y]; theField[x][y] = EMPTINESS_COLOR; } /* Не забуваємо оновити мета-информацию*/ countFilledCellsInLine[fallTo] = countFilledCellsInLine[y]; countFilledCellsInLine[y] = 0; /* * У будь-якому випадку лінія згори від попередньої порожнечі порожня. * Якщо раніше вона не була порожньою, то зараз ми її змістили вниз. * Якщо раніше вона була порожньою, то і зараз порожня -- ми її ще не заповнювали. */ fallTo++; } } }}

Тепер GameField реалізований майже повністю, за винятком гетера для фігури. Її графічному модулю також доведеться відмальовувати:

public Figure getFigure (){
return figure;}

Тепер нам треба написати алгоритми, за якими фігура визначає свої координати в різних станах і весь клас фігури.

Клас фігури

Реалізувати це я пропоную таким чином: зберігати для фігури (1) “уявну” координату, таку, що всі реальні блоки знаходяться нижче і правіше її, (2) стан повертання (їх всього 4, після 4 повертань фігура завжди повертається у вихідне положення) і (3) маску, яка за першими двома параметрами визначатиме положення реальних блоків:

/** * Уявна координата фігури. За цією координатою * через маску генеруються координати реальних * блоків фігури. */ private Coord metaPointCoords; /** * Поточний стан повороту фігури. */ private RotationMode currentRotation; /** * Форма фігури.
*/ private FigureForm form;

Rotation мод тут виглядатиме таким чином:

public enum RotationMode { /** Початкове положення */ NORMAL (0), /** Положення, що відповідає повертанню проти годинникової стрелки*/ FLIP_CCW (1), /** Положення, що відповідає дзеркальному отражению*/ INVERT (2), /** Положення, що відповідає повороту за годинниковою стрілкою (чи трьом поворотам проти) */ FLIP_CW (3); /** Кількість поворотів проти годинникової стрілки, необхідне для прийняття положения*/ private int number; /** * Конструктор. * * @param number Кількість поворотів проти годинникової стрілки, необхідне для прийняття положення */ RotationMode (int number){ this.number = number; } /** Зберігає об'єкти enum 'а. Індекс в масиві відповідає полю number.
* Для зручнішої роботи getNextRotationForm (). */ private static RotationMode[] rotationByNumber ={NORMAL, FLIP_CCW, INVERT, FLIP_CW}; /** * Повертає положення, образованое в результаті повороту за годинниковою стрілкою * з положення perviousRotation * * @param perviousRotation Положення з якого був здійснений поворот * @return Положення, утворене в результаті повороту */ public static RotationMode getNextRotationFrom (RotationMode perviousRotation) { int newRotationIndex =  (perviousRotation.number + 1)  % rotationByNumber.length; return rotationByNumber[newRotationIndex]; }}

Відповідно, від самого класу Figure нам потрібний тільки конструктор ініціалізованого поля:

/** * Конструктор.
* Стан повертання за умовчанням: RotationMode.NORMAL * Форма задається випадкова. * * @param metaPointCoords Уявна координата фігури. Див. документацію однойменного поля */ public Figure (Coord metaPointCoords){ this (metaPointCoords, RotationMode.NORMAL, FigureForm.getRandomForm()); } public Figure (Coord metaPointCoords, RotationMode rotation, FigureForm form){ this.metaPointCoords = metaPointCoords; this.currentRotation = rotation; this.form = form; } }

І методи, якими ми користувалися в GameField наступного виду:

/** * @return Координати реальних осередків фігури в поточному стані */ public Coord[] getCoords (){ return form.getMask ().generateFigure (metaPointCoords, currentRotation); } /**
* @return Координати осередків фігури, начебто * вона була повернена проти годинникової стрілки від поточного положення */ public Coord[] getRotatedCoords (){ return form.getMask ().generateFigure (metaPointCoords, RotationMode.getNextRotationFrom (currentRotation)); } /** * Повертає фігуру проти годинникової стрілки */ public void rotate (){ this.currentRotation = RotationMode.getNextRotationFrom (currentRotation); } /** * @param direction Напрям зрушення * @return Координати осередків фігури, начебто * вона була зрушена у вказано напрямі */ public Coord[] getShiftedCoords (ShiftDirection direction){ Coord newFirstCell = null; switch (direction){ case LEFT: newFirstCell = new Coord (metaPointCoords.x - 1, metaPointCoords.y); break; case RIGHT:
newFirstCell = new Coord (metaPointCoords.x + 1, metaPointCoords.y); break; default: ErrorCatcher.wrongParameter ("direction (for getShiftedCoords) ", " Figure"); } return form.getMask ().generateFigure (newFirstCell, currentRotation); } /** * Міняє уявну X- координату фігури * для зрушення в указаном напрямі * * @param direction Напрям зрушення */ public void shift (ShiftDirection direction){ switch (direction){ case LEFT: metaPointCoords.x--; break; case RIGHT: metaPointCoords.x++; break; default: ErrorCatcher.wrongParameter ("direction (for shift) ", " Figure"); } } /** * @return Координати осередків фігури, начебто * вона була зрушена вниз на один осередок
*/ public Coord[] getFallenCoords (){ Coord newFirstCell = new Coord (metaPointCoords.x, metaPointCoords.y - 1); return form.getMask ().generateFigure (newFirstCell, currentRotation); } /** * Міняє уявну Y- координати фігури * для зрушення на один осередок вниз */ public void fall (){ metaPointCoords.y--; }

На додаток, у фігури має бути колір, щоб графічний модуль міг її відобразити. У Тетрісі кожній фігурі відповідає свій колір, тому колір ми будемо запитувати у форми:

public TpReadableColor getColor (){ return form.getColor ();}

Форма фігури і маски координат

Приведу реалізацію тільки для двох форм: I-подібної та J-подібної. Код для інших фігур принципово не відрізняється і викладений на GitHub.

Зберігаємо для кожної фігури маску координат (яка визначає, наскільки кожен реальний блок повинен відстояти від “уявної” координати фігури) і колір:

public enum FigureForm { I_FORM (CoordMask.I_FORM, TpReadableColor.BLUE), J_FORM (CoordMask.J_FORM, TpReadableColor.ORANGE);/** Маска координат (задає геометричну форму) */ private CoordMask mask; /** Колір, характерний для цієї форми */ private TpReadableColor color; FigureForm (CoordMask mask, TpReadableColor color){ this.mask = mask; this.color = color; }

Реалізуємо методи, які використали вище:

/** * Масив з усіма об'єктами цього enum 'а (для зручної реалізації getRandomForm() ) */
private static final FigureForm[] formByNumber ={I_FORM, J_FORM, L_FORM, O_FORM, S_FORM, Z_FORM, T_FORM,}; /** * @return Маска координат цієї форми */ public CoordMask getMask (){ return this.mask; } /** * @return Колір, специфічний для цієї форми */ public TpReadableColor getColor (){ return this.color; } /** * @return Випадковий об'єкт цього enum 'а, тобто випадкова форма */ public static FigureForm getRandomForm (){ int formNumber = new Random ().nextInt (formByNumber.length); return formByNumber[formNumber]; }

Маски координат я пропоную просто захардкодити:

/** * Кожна маска -- шаблон, який по уявній координаті фігури і * стану її повертання повертає 4 координати реальних блоків
* фігури, які повинні відображатися. * тобто маска задає геометричну форму фігури. * * @author DoKel * @version 1.0 */public enum CoordMask { I_FORM ( new GenerationDelegate (){ @Override public Coord[] generateFigure (Coord initialCoord, RotationMode rotation){ Coord[] ret = new Coord[4]; switch (rotation){ case NORMAL: case INVERT: ret[0] = initialCoord; ret[1] = new Coord (initialCoord.x, initialCoord.y - 1); ret[2] = new Coord (initialCoord.x, initialCoord.y - 2); ret[3] = new Coord (initialCoord.x, initialCoord.y - 3); break; case FLIP _CCW: case FLIP _CW:
ret[0] = initialCoord; ret[1] = new Coord (initialCoord.x + 1, initialCoord.y); ret[2] = new Coord (initialCoord.x + 2, initialCoord.y); ret[3] = new Coord (initialCoord.x + 3, initialCoord.y); break; } return ret; } } ), J_FORM ( new GenerationDelegate (){ @Override public Coord[] generateFigure (Coord initialCoord, RotationMode rotation){ Coord[] ret = new Coord[4]; switch (rotation){ case NORMAL: ret[0] = new Coord (initialCoord.x + 1, initialCoord.y); ret[1] = new Coord (initialCoord.x + 1, initialCoord.y - 1);
ret[2] = new Coord (initialCoord.x + 1, initialCoord.y - 2); ret[3] = new Coord (initialCoord.x, initialCoord.y - 2); break; case INVERT: ret[0] = new Coord (initialCoord.x + 1, initialCoord.y); ret[1] = initialCoord; ret[2] = new Coord (initialCoord.x, initialCoord.y - 1); ret[3] = new Coord (initialCoord.x, initialCoord.y - 2); break; case FLIP _CCW: ret[0] = initialCoord; ret[1] = new Coord (initialCoord.x + 1, initialCoord.y); ret[2] = new Coord (initialCoord.x + 2, initialCoord.y);
ret[3] = new Coord (initialCoord.x + 2, initialCoord.y - 1); break; case FLIP _CW: ret[0] = initialCoord; ret[1] = new Coord (initialCoord.x, initialCoord.y - 1); ret[2] = new Coord (initialCoord.x + 1, initialCoord.y - 1); ret[3] = new Coord (initialCoord.x + 2, initialCoord.y - 1); break; } return ret; } } );/** * Делегат, що містить метод, * який повинен визначати алгоритм для generateFigure () */ private interface GenerationDelegate{ /** * По уявній координаті фігури і стану її повороту * повертає 4 координати реальних блоків фігури, які повинні відображатися
* * @param initialCoord Уявна координата * @param rotation Стан повороту * @return 4 реальних координати */ Coord[] generateFigure (Coord initialCoord, RotationMode rotation); } private GenerationDelegate forms; CoordMask (GenerationDelegate forms){ this.forms = forms; } /** * По уявній координаті фігури і стану її повороту * повертає 4 координати реальних блоків фігури, які повинні відображатися. * * Запит передається делегатові, спецефичному для кожного об'єкту enum 'а. * * @param initialCoord Уявна координата * @param rotation Стан повороту * @return 4 реальних координати */ public Coord[] generateFigure (Coord initialCoord, RotationMode rotation){ return this.forms.generateFigure (initialCoord, rotation); }}

Для кожного об’єкта enum‘а ми передаємо за допомогою імпровізованих (інших в Java немає) делегатів метод, в якому залежно від переданого стану повертання повертаємо різні реальні координати блоків. Загалом, можна обійтися і без делегатів, якщо зберігати в кожному елементі відступи для кожного з режимів повертання.

Насолоджуємося результатом

Робоча програма

P.S. Нагадаю, що первинники готового проекту доступні на GitHub.

Київ, Харків, Одеса, Дніпро, Запоріжжя, Кривий Ріг, Вінниця, Херсон, Черкаси, Житомир, Хмельницький, Чернівці, Рівне, Івано-Франківськ, Кременчук, Тернопіль, Луцьк, Ужгород, Кам'янець-Подільський, Стрий - за статистикою саме з цих міст програмісти найбільше переїжджають працювати до Львова. А Ви розглядаєте relocate?


Trends: скачати гру тетрис

Залишити відповідь

Ваша e-mail адреса не оприлюднюватиметься. Обов’язкові поля позначені *