Як написати свою змійку на Java за 15 хвилин?


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

У попередній статті ми писали сапера за 15 хв, тепер займемося класичною змійкою.

Цього разу нам знову знадобляться:

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

Підключення бібліотек

Попереднього разу в багатьох користувачів виникли із цим питанням проблеми, тому мені здалося доречним присвятити цьому трохи часу. По-перше, вище я дав посилання на скачування архіву з бібліотеками, які використовую я, щоб не було плутанини з версіями і запитання: “Де знайти?”. Теку з архіву необхідно помістити в теку проекту і підключити через Вашу IDE.

По-друге, користувачі InteliJ IDEA мали проблеми з їх підключенням. Я знайшов у мережі наступний відеогайд:

Після того, як я зробив все точно за ним, у мене бібліотеки підключилися коректно і все запрацювало.

Робота з графікою

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

  • клас виконуватиме ініціалізацію OpenGL:
///Class GUIprivate static void initializeOpenGL (){ try { //Задаємо розмір майбутнього вікна Display.setDisplayMode (new DisplayMode (SCREEN_WIDTH, SCREEN_HEIGHT)); //Задаємо ім'я майбутнього вікна Display.setTitle (SCREEN_NAME); //Створюємо вікно Display.create (); } catch (LWJGLException e) { e.printStackTrace (); } glMatrixMode (GL_PROJECTION); glLoadIdentity ();
glOrtho (0, SCREEN_WIDTH, 0, SCREEN_HEIGHT, 1, - 1); glMatrixMode (GL_MODELVIEW); /* * Для підтримки текстур */ glEnable (GL TEXTURE 2D); /* * Для підтримки прозорості */ glEnable (GL_BLEND); glBlendFunc (GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); /* * Білий фоновий колір */ glClearColor (1,1,1,1); }
  • повинен зберігати поточні стани осередків:
    //Клас Cells напишемо дещо пізніше private static Cell[][] cells;
  • повинен відмальовувати ці осередки:
    ///Малює всі клітинки public static void draw (){ ///Очищає екран від старого зображення glClear (GL_COLOR_BUFFER_BIT); for (Cell[] line:cells){ for (Cell cell:line){ drawElement (cell);
    } } }private static void drawElement (Cell elem){ ///Якщо в осередку немає спрайту, то малювати його не треба if (elem.getSprite () == null) return; ///Власне, малюємо. Детально не зупиняюся, оскільки нам цікава сама логіка гри, а не LWJGL elem.getSprite ().getTexture ().bind (); glBegin (GL_QUADS); glTexCoord2f (0,0); glVertex2f (elem.getX (),elem.getY ()+elem.getHeight()); glTexCoord2f (1,0); glVertex2f (elem.getX ()+elem.getWidth (),elem.getY ()+elem.getHeight()); glTexCoord2f (1,1); glVertex2f (elem.getX ()+elem.getWidth (), elem.getY()); glTexCoord2f (0,1); glVertex2f (elem.getX (), elem.getY()); glEnd (); }
  • і оновлюватися, коли цього запросить головний цикл:
     //Цей метод викликатиметься ззовні
    public static void update (boolean have_to_decrease) { updateOpenGL (); for (Cell[] line:cells){ for (Cell cell:line){ cell.update (have_to_decrease); } } } ///А цей метод використовуватиметься тільки локально, /// оскільки базовим інші класи повинні працювати на більш високому рівні private static void updateOpenGL (){ Display.update (); Display.sync (FPS); }

Як Ви можете бачити, тут я вже використав декілька констант. Для них був створений окремий клас Constants з public static полями. Ось він повністю:

public class Constants { ///Розмір ігрового осередку public static final int CELL_SIZE = 32; ///Розміри ігрового поля в осередках public static final int CELLS_COUNT_X = 20;
public static final int CELLS_COUNT_Y = 20; ///Шанс появи ягід на старті у відсотках. ///При виставленому значенні спавнится 3-5 ягід. ///Не турбуйтеся, що значення занадто низьке, як мінімум одна ягода створюється окремо. public static final int INITIAL_SPAWN_CHANCE = 1;//% ///У нашому випадку змія проходить одну клітину за один фрейм. ///Значення 5 мені здалося оптимальним, але ви можете експериментувати. public static final int FPS = 5; ///Константи для створення вікна, назви що досить говорять. public static final int SCREEN_WIDTH =CELLS_COUNT_X*CELL_SIZE; public static final int SCREEN_HEIGHT = CELLS_COUNT_Y*CELL_SIZE; public static final String SCREEN_NAME = "Tproger' s Snake";}

Enum Sprite, який відповідає за підвантаження текстур, ідентичний тому, що ми писали для сапера, за винятком того, що нам треба тільки дві текстури – для змії та для ягід. Ось код:

public enum Sprite { ///Файли з іменами circle і cherries повинні лежати за адресою /// %тека проекта%/res/ в розширенні .png BODY ("circle"), CHERRIES ("cherries"); private Texture texture; private Sprite (String texturename){ try { this.texture = TextureLoader.getTexture ("PNG", new FileInputStream (new File ("res/"+texturename+".png"))); } catch (IOException e) { e.printStackTrace (); } } public Texture getTexture (){ return this.texture; }}

Механіка гри

Саме час поговорити про те, як наша змія буде переміщатися. Вам, напевно, доводилося бачити вивіски, навколо яких по колу бігають вогники? Зрозуміло, самі лампочки в них не переміщаються, просто кожен тик остання гасне, а перша запалюється. Так само переміщатиметься і наша змія.

Нескладно підрахувати, що кожна лампочка повинна горіти стільки тиків, яка довжина “змії”. Значить, ми повинні повідомити клітинку, в яку потрапляє змія, що вона повинна горіти певну кількість секунд, а кожен тик зменшувати це число у кожної клітинки з ненульовим таймером, і міняти спрайт, якщо змія з клітинки вже виповзла (тобто таймер дорівнював нулю). У разі ж необхідності подовжити ланцюжок, доволі просто: не зменшувати час “горіння” клітинок на якомусь тику. Саме тому метод update () у класів Cell і GUI приймає параметр – якщо він дорівнює false, значить, змія щось з’їла.

Пишемо клас клітинки

public class Cell { private int x; private int y; private int state;/* 0 -> осередок порожній
>0 -> В осередку тіло змії, яке буде там ще N фреймів <0 -> Щось незвичайне: - 1: Ягоди */ ///Конструктор просто виставляє початкові значення координат і стану public Cell (int x, int y, int state){ this.x=x; this.y=y; this.state=state; } ///==== Нічим не примітні геттери і сетери public int getX (){ return x; } public int getY (){ return y; } public int getHeight (){ return CELL_SIZE; } public int getWidth (){ return CELL_SIZE; } public int getState (){ return this.state; } public void setState (int state){ this.state = state; } ///==== ///Метод оновлення клітини. Зменшуємо час " горіння", якщо це необхідно
public void update (boolean have_to_decrease){ if (have_to_decrease && this.state > 0){ this.state--; } } ///Осередок "думає" як вона повинна виглядати public Sprite getSprite (){ if (this.state > 0){ ///Якщо в ній тіло змії -- як змія return Sprite.BODY; }else if (this.state==0){ ///Якщо в ній немає нічого -- ніяк виглядати і не повинна return null; }else{ ///Інакше проходимося свитчем по можливих об'єктах. ///Оскільки це демо -- я додав тільки ягоди switch (this.state){ default: return Sprite.CHERRIES; } } }}

Додаємо гетер і сетер для стану клітинки поля в GUI

getState (x, y){return cells[x][y].getState ();}setState (x, y, state){
cells[x][y].setState (state);}

Додаємо метод, що створює початкове поле в GUI

Просто ініціалізували OpenGL, потім масив Cell[][] cells і заповнюємо останній клітинками з випадковим полем state.

public static void init (){ initializeOpenGL (); cells = new Cell[CELLS_COUNT_X][CELLS_COUNT_Y]; Random rnd = new Random (); for (int i=0; i<CELLS_COUNT_X; i++){ for (int j=0; j<CELLS_COUNT_Y; j++){ cells[i][j]=new Cell (i*CELL_SIZE, j*CELL_SIZE, (rnd.nextInt (100) < INITIAL_SPAWN_CHANCE?- 1: 0); } } }

Головний керівний клас

class Main{ ///Змінна, при зверненні якої в true додаток закривається
private static boolean isExitRequested=false; ///Дані про нашу змію. Виповзати вона буде з нижнього лівого кута, ///Праворуч (напрями пораховані по годинникової стрілки від півночі, тобто /// 0 -- вгору, 1 -- управо, 2 -- вниз, 3 -- вліво private static int x=-1, y=0, direction=1, length=3; ///Прапор, який звертається в false, якщо на цьому тику змія щось з'їла private static boolean have_to_decrease = true; ///Вхідний клас public static void main (String[] args){ ///Ініціалізували графічний інтерфейс GUI.init (); ///Створюємо ягідку у випадковому місці generate_new_obj (); ///Поки не отримаємо сигнал на закриття, в циклі... while (!isExitRequested){ ///Перевіряємо введення даних input (); ///Рухаємо змію move (); ///Оновлюємо і малюємо графічні елементи GUI.draw ();
GUI.update (have_to_decrease); } } private static void move (){ /// Якщо на минулому тику ми щось з'їли, то на цьому повинні повернути значення на true have_to_decrease=true; ///Міняємо координати змії залежно від напряму switch (direction){ case 0: y++; break; case 1: x++; break; case 2: y--; break; case 3: x--; break; } ///Перевіряємо, чи не вийшла змія за межі if (x < 0 || x >= CELLS_COUNT_X || y < 0 || y >= CELLS_COUNT_Y){ //TODO gameover System.exit (1); } ///Дивимося стан осередку, куди зайшла змія int next_cell_state = GUI.getState (x, y); ///Якщо там змія, то це програш if (next_cell_state>0){ //TODO gameover
System.exit (1); }else{ ///Якщо там їжа, то if (next_cell_state < 0){ length++; ///Збільшуємо довжину на одиницю generate_new_obj (); ///Створюємо нову їжу have_to_decrease=false; ///Виставляємо прапор того, що ми з'їли щось } ///"Запалюємо" клітину GUI.setState (x, y, length); } }/*Алгоритм генерації нової їжі наступний.Ми вираховуємо кількість клітин, які не заповнені змією, по формулі: CELLS_COUNT_X*CELLS_COUNT_Y - lengthИ вибираємо випадкову таку клітину (зберігаємо її номер в point).Потім проходимося по усіх клітинах, і, якщо в клітині не змія, зменшуємо лічильник.Як тільки лічильник дорівнює нулю, створюємо в цій клітині їду і виходимо з циклу.УВАГА! При такому методі ягоди можуть створюватися поверх інших ягід, тобто
Їх загальна кількість зменшуватиметься з часом. Щоб уникнути цього можна при знищенні однієї ягоди створювати випадкове число (1-3) ягод.*/ private static void generate_new_obj (){ int point = new Random ().nextInt (CELLS_COUNT_X*CELLS_COUNT_Y - length); for (int i=0; i<CELLS_COUNT_X; i++){ for (int j=0; j<CELLS_COUNT_Y; j++){ if (GUI.getState (i, j)  <= 0) { if (point == 0) { GUI.setState (i, j, - 1); //TODO randomize objects return; }else { point--; } } } } } private static void input (){ ///Перебираємо події клавіатури int newdirection = direction; while (Keyboard.next()){ if (Keyboard.getEventKeyState()){
switch (Keyboard.getEventKey()) { case Keyboard.KEY _ESCAPE: isExitRequested = true; break; case Keyboard.KEY _UP: if (direction!=2) newdirection=0; break; case Keyboard.KEY _RIGHT: if (direction!=3) newdirection=1; break; case Keyboard.KEY _DOWN: if (direction!=0) newdirection=2; break; case Keyboard.KEY _LEFT: if (direction!=1) newdirection=3; break; } } } direction = newdirection; //Ці рокіровки потрібні, щоб правильно працювала система умов, //що забороняє повертати назад, "в себе"
///Обробляємо клик по кнопці "закрити" вікна isExitRequested=isExitRequested || Display.isCloseRequested (); }}

Готово!

P.S. Первинники можна скачати тут (архів усієї теки проекту).

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


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

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