JavaRush /Java блог /JavaRush /Раздел "Игры" на JavaRush: Полезная теория

Раздел "Игры" на JavaRush: Полезная теория

Статья из группы JavaRush
В разделе «Игры» на JavaRush вы найдете увлекательные проекты по написанию популярных компьютерных игр. Хотите создать свою версию популярных «2048», «Сапера», «Змейки» и других игр? Это просто. Мы превратили написание игр в пошаговый процесс.Раздел Чтобы попробовать себя в роли гейм-разработчика, не обязательно быть продвинутым программистом, но определенный набор Java-знаний все же необходим. Здесь вы найдете информацию, которая будет полезна при написании игр.

1. Наследование

Работа с игровым движком JavaRush подразумевает использование наследования. Но что делать, если вы не знаете, что это такое? С одной стороны, нужно в этой теме разобраться: она изучается на 11 уровне. С другой стороны, движок специально спроектировали очень простым, поэтому можно обойтись поверхностным знанием наследования. Итак, что же такое наследование. Если очень упростить, наследование — это связь между двумя классами. Один из них становится родителем, а второй — потомком (классом-наследником). При этом класс-родитель может даже не знать, что у него есть классы-потомки. Т.е. особой выгоды от наличия классов-наследников он не получает. А вот классу-потомку наследование дает много преимуществ. И главное из них — в том, что все переменные и методы класса-родителя появляются в классе-потомке, как будто код класса-родителя скопировали в класс-потомок. Это не совсем так, но для упрощенного понимания наследования пойдет. Вот несколько примеров, чтобы лучше понять наследование. Пример 1: самое простое наследование.

public class Родитель {

}
Класс Потомок унаследован от класса Родитель с помощью ключевого слова extends.

public class Потомок extends Родитель {

}
Пример 2: использование переменных класса-родителя.

public class Родитель {

   public int age;
   public String name;
}
Класс Потомок может использовать переменные age и name класса Родитель как будто они объявлены в нем.

public class Потомок extends Родитель {

   public void printInfo() {

     System.out.println(name+" "+age);
   }
}
Пример 3: использование методов класса-родителя.

public class Родитель {

   public int age;
   public String name;

   public getName() {
      return name;
   }
}
Класс Потомок может использовать переменные и методы класса Родитель как будто они объявлены в нем. В этом примере мы используем метод getName().

public class Потомок extends Родитель {

   public void printInfo() {

     System.out.println(getName()+" "+age);
   }
}
Вот как выглядит класс Потомок с точки зрения компилятора:

public class Потомок extends Родитель {

   public int age; //  унаследованная переменная
   public String name; //  унаследованная переменная

   public getName() { //  унаследованный метод.
      return name;
  }
   public void printInfo() {

     System.out.println(getName()+" "+age);
   }
}

2. Переопределение методов

Иногда бывают ситуации, что мы унаследовали наш класс Потомок от какого-то очень полезного нам класса Родителя вместе со всеми переменными и методами, но вот некоторые методы работают не совсем так, как нам хочется. Или совсем не так, как нам не хочется. Что делать в этой ситуации? Можем переопределить не понравившийся нам метод. Делается это очень просто: в нашем классе Потомке просто объявляем метод с такой же сигнатурой (заголовком), что и метод класса Родителя и пишем в нем наш код. Пример 1: переопределение метода.

public class Родитель {

   public String name;

   public void setName (String nameNew) {
       name = nameNew;
  }

   public getName() {
      return name;
  }
}
Метод printInfo() выведет на экран фразу "Luke, No!!!"

public class Потомок extends Родитель {

   public void setName (String nameNew) {
       name = nameNew + ",No!!!";
  }

   public void printInfo() {

      setName("Luke");
      System.out.println( getName());
   }
}
Вот как выглядит класс Потомок с точки зрения компилятора:

public Потомок extends Родитель {

   public String name; //  унаследованная переменная

   public void setName (String nameNew) { //  Переопределенный метод взамен унаследованного
   
       name = nameNew + ", No!!!";
   }
   public getName() { //  унаследованный метод.

      return name;
   }
   public void printInfo() {

     setName("Luke");
     System.out.println(getName());
   }
}
Пример 2: немного магии наследования (и переопределения методов).

public class Родитель {

   public getName() {
      return "Luke";
  }
   public void printInfo() {

     System.out.println(getName());
   }
}

public class Потомок extends Родитель {

   public getName() {
      return "I'm your father, Luke";
  }
}
В данном примере: если в классе Потомок не переопределен метод printInfo (из класса Родителя), при вызове этого метода у объекта класса Потомок будет вызван его метод getName(), а не getName() класса Родителя.

Родитель parent = new Родитель ();
parent.printnInfo();
Этот код выведен на экран надпись "Luke".

Потомок child = new Потомок ();
child.printnInfo();
Этот код выведен на экран надпись "I'm your father, Luke;".
Вот как выглядит класс Потомок с точки зрения компилятора:

public class Потомок extends Родитель {

   public getName() {
      return "I'm your father, Luke";
   }
   public void printInfo() {

     System.out.println(getName());
   }
}

3. Списки

Если вы еще не познакомились со списками (List), вот вам краткая информация. Полную информацию вы можете найти на 6-7 уровнях курса JavaRush. Списки имеют много общего с массивами:
  • могут хранить много данных определенного типа;
  • позволяют получать элементы по их индексу/номеру;
  • индексы элементов начинаются с 0.
Преимущества списков: В отличие от массивов, списки могут динамически менять размер. Сразу после создания список имеет размер 0. По мере добавления элементов в список, его размер увеличивается. Пример создания списка:

ArrayList<String> myList = new ArrayList<String>(); // создание нового списка типа ArrayList
Значение в угловых скобках — это тип данных, которые может хранить список. Вот некоторые методы для работы со списком:
Код Краткое описание действий кода
ArrayList<String> list = new ArrayList<String>(); Создание нового списка строк
list.add("name"); Добавить элемент в конец списка
list.add(0, "name"); Добавить элемент в начало списка
String name = list.get(5); Получить элемент по его индексу
list.set(5, "new name"); Изменить элемент по его индексу
int count = list.size(); Получить количество элементов в списке
list.remove(4); Удалить элемент из списка
Больше о списках можете узнать из этих статей:
  1. Класс ArrayList
  2. Работа ArrayList в картинках
  3. Удаление элемента из списка ArrayList

4. Массивы

Что такое матрица? Матрица — не что иное как прямоугольная таблица, которая может быть заполнена данными. Другими словами, это двумерный массив. Как вы, наверное, знаете, массивы в Java являются объектами. Стандартный одномерный массив типа int выглядит так:

int [] array = {12, 32, 43, 54, 15, 36, 67, 28};
Представим это визуально:
0 1 2 3 4 5 6 7
12 32 43 54 15 36 67 28
Верхняя строка указывает адреса ячеек. То есть, чтобы получить число 67, нужно обратиться к элементу массива с индексом 6:

int number = array[6];
Здесь все очень просто. Двумерный массив является массивом одномерных массивов. Если вы об этом слышите впервые, остановитесь и представьте это у себя в голове. Двумерный массив примерно выглядит так:
0 Одномерный массив Одномерный массив
1 Одномерный массив
2 Одномерный массив
3 Одномерный массив
4 Одномерный массив
5 Одномерный массив
6 Одномерный массив
7 Одномерный массив
В коде:

int [][] matrix = {
{65, 99, 87, 90, 156, 75, 98, 78}, {76, 15, 76, 91, 66, 90, 15, 77}, {65, 96, 17, 25, 36, 75, 54, 78}, {59, 45, 68, 14, 57, 1, 9, 63}, {81, 74, 47, 52, 42, 785, 56, 96}, {66, 74, 58, 16, 98, 140, 55, 77}, {120, 99, 13, 90, 78, 98, 14, 78}, {20, 18, 74, 91, 96, 104, 105, 77} }
0 0 1 2 3 4 5 6 7
65 99 87 90 156 75 98 78
1 0 1 2 3 4 5 6 7
76 15 76 91 66 90 15 77
2 0 1 2 3 4 5 6 7
65 96 17 25 36 75 54 78
3 0 1 2 3 4 5 6 7
59 45 68 14 57 1 9 63
4 0 1 2 3 4 5 6 7
81 74 47 52 42 785 56 96
5 0 1 2 3 4 5 6 7
66 74 58 16 98 140 55 77
6 0 1 2 3 4 5 6 7
120 99 13 90 78 98 14 78
7 0 1 2 3 4 5 6 7
20 18 74 91 96 104 105 77
Чтобы получить значение 47, нужно обратиться к элементу матрицы по адресу [4][2].

int number = matrix[4][2];
Если вы заметили, координаты матрицы отличаются от классической прямоугольной системы координат (Декартовой системы координат). При обращении к матрице сначала вы указываете y, а потом x, в то время как в математике принято сначала указывать x (x, y). Возможно, вы задаетесь вопросом: «А почему бы не перевернуть матрицу в своем воображении и не обращаться к элементам привычным путем через (x, y)? От этого же содержимое матрицы не изменится». Да, ничего не изменится. Но в мире программирования к матрицам принято обращаться в форме «сначала y, потом x». Это нужно принять как должное. Теперь давайте поговорим о проецировании матрицы на наш движок (класс Game). Как вам известно, у движка есть много методов, которые изменяют клетки игрового поля по заданным координатам. Например, метод setCellValue(int x, int y, String value). Он устанавливает определенной клетке с координатами (x, y) значение value. Как вы заметили, этот метод вначале принимает именно x, как в классической системе координат. Аналогичным образом работают и остальные методы движка. При разработке игр, часто будет появляться необходимость воспроизводить состояние матрицы на экране. Как же это сделать? Во-первых, в цикле нужно перебрать все элементы матрицы. Во-вторых, для каждого из них вызвать метод для отображения с ИНВЕРТИРОВАНЫМИ координатами. Пример:

    private void drawScene() {
        for (int i = 0; i < matrix.length; i++) {
            for (int j = 0; j < matrix[i].length; j++) {
                setCellValue(j, i, String.valueOf(matrix[i][j]));
            }
        }
    }
Естественно, инверсия работает в двух направлениях. В метод setCellValue можно передать (i, j), но при этом из матрицы взять элемент [j][i]. Инверсия может показаться немного трудной, но о ней нужно помнить. И всегда, если возникают какие-то проблемы, стоит взять бумажку с ручкой, начертить матрицу и воспроизвести, какие процессы с ней происходят.

5. Случайные числа

Как работать с генератором случайных чисел? В классе Game определен метод getRandomNumber(int). Под капотом он использует класс Random из пакета java.util, но принцип работы с генератором случайных чисел от этого не меняется. В качестве аргумента getRandomNumber(int) принимает целое число. Это число будет верхней границей, которую может вернуть генератор. Нижней границей является 0. Важно! Генератор НИКОГДА не вернет верхнее граничное число. Например, если вызвать getRandomNumber(3) он случайным образом может вернуть 0, 1, 2. Как видите, 3 он вернуть не может. Такое использование генератора является довольно простым, но очень эффективным во многих случаях. Вам нужно получить случайное число в каких-то пределах: Представьте, что вам необходимо какое-нибудь трехзначное число (100..999). Как вы уже знаете, минимальное возвращаемое число — 0. Значит, вам нужно будет к нему добавить 100. Но в таком случае необходимо позаботиться о том, чтобы не перешагнуть верхнюю границу. Чтобы получить 999 как максимальное случайное значение, следует вызывать метод getRandomNumber(int) с аргументом 1000. Но мы помним о последующем добавлении 100: значит и верхнюю границу следует понизить на 100. То есть, код для получения случайного трехзначного числа будет выглядеть так:

int number = 100 + getRandomNumber(900);
Но для упрощения подобной процедуры в движке предусмотрен метод getRandomNumber(int, int), который в качестве первого аргумента принимает минимальное для возврата число. Используя этот метод, предыдущий пример можно переписать:

int number = getRandomNumber(100, 1000);
Случайные числа могут использоваться для получения случайного элемента массива:

String [] names = {"Андрей", "Валентин", "Сергей"}; 
String randomName = names[getRandomNumber(names.length)]
Вызов определенных событий с некой вероятностью. У человека утро начинается по возможными сценариям: Проспал – 50%; Встал вовремя – 40%; Встал на час раньше положенного – 10%. Представьте, что вы пишете эмулятор человеческого утра. Вам нужно вызывать события с определенной вероятностью. Для этого, опять-таки, надо воспользоваться генератором случайных чисел. Реализации могут быть разные, но самая простая должна происходить по следующему алгоритму:
  1. устанавливаем пределы, в которых нужно сгенерировать число;
  2. генерируем случайное число;
  3. обрабатываем полученное число.
Итак, в данном случае пределом будет 10. Вызовем метод getRandomNumber(10) и проанализируем, что он нам может вернуть. Вернуть он может 10 цифр (от 0 до 9) и каждую с одинаковой вероятностью — 10%. Теперь нам нужно скомбинировать все возможные результаты и сопоставить их с нашими возможными событиями. Комбинаций может быть очень много, в зависимости от вашей фантазии, но самая очевидная звучит: «Если случайное число лежит в пределах [0..4] — вызов события «Проспал», если число в пределах [5..8] — «Встал вовремя», и только если число 9, тогда «Встал на час раньше положенного». Все очень просто: в пределах [0..4] лежит 5 чисел, каждое из которых может вернуться с вероятностью 10%, что в сумме и будет 50%; в пределах [5..8] лежит 4 числа, ну и 9 — единственное число, которое появляется с вероятность 10%. В коде вся эта хитромудрая конструкция выглядит еще проще:

        int randomNumber = getRandomNumber(10);
        if (randomNumber < 5) {
            System.out.println("Проспал ");
        } else if (randomNumber < 9) {
            System.out.println("Встал вовремя ");
        } else {
            System.out.println("Встал на час раньше положенного ");
        }
Вообще, вариантов применения случайных чисел может быть очень много. Все зависит только от вашей фантазии. Но наиболее эффективно их применять, если нужно многократно получать какой-нибудь результат. Тогда этот результат будет отличаться от предыдущего. С какой-то вероятностью, естественно. На этом все! Если вы хотите узнать о разделе "Игры" больше, вот полезная документация, которая может в этом помочь:
Комментарии (23)
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
21 августа 2023
"Но в мире программирования к матрицам принято обращаться в форме «сначала y, потом x». Это нужно принять как должное." - полная чушь. В Си-подобных языках массивы в памяти расположены построчно, поэтому к ним обращаются так как указано в статье. В ЯП Fortran массивы в расположены по столбцам, и там уже идет обращение наоборот.
Egor Kurilko Уровень 7
16 мая 2022
Хочу оспорить навязанное мнение про массивы: "Если вы заметили, координаты матрицы отличаются от классической прямоугольной системы координат (Декартовой системы координат). При обращении к матрице сначала вы указываете y, а потом x, в то время как в математике принято сначала указывать x (x, y). ... в мире программирования к матрицам принято обращаться в форме «сначала y, потом x». Это нужно принять как должное." Интересно, кто придумал, что в программировании система координат перевернута. Вот ни разу. Все как и в математике, координаты "Х" - это горизонталь. А в программировании "Х" - это только количество горизонтальных массивов, т.е. осей Х или индексов строк. А строка это горизонталь, т.е. ось Х. В цикле иногда принимают как i. Теперь про координаты "У". Как и в математике, ось "У" - это вертикаль. А в программировании "У" это количество вертикальных столбцов, т.е. количество индексов столбцов. А столбец - это вертикаль. А вериткаль - это ось "У". В цикле иногда принимают как j. Если по-другому, то строки - это оси "Х", а столбцы - это оси "У".
Максим Глотов Уровень 35
1 июля 2021
Фразу "I'm your father, Luke" все-таки должен говорить Родитель, а не Потомок. А то как это потомок может сказать родителю, что он его отец?
Viacheslav Уровень 26
4 мая 2020
Alex Ter Уровень 14
28 апреля 2020
а не проще было объяснить, что в вашем методе setScreenSize(int x, int y) количество столбцов это Х, а количество строк это У? я весь мозг себе съел, зачем нужно делать инвертирование в коде

private void createGame(){
for (int х=0; х<SIDE;х++){
for (int у=0; у<SIDE;у++){
gameField[х][у]=new GameObject(y, x);
setCellColor(х, у, Color.ORANGE);
когда все оказалось блин просто! неужели раз поле состоит из ячеек идентичных массивам, не проще было сделать x, y идентично 2мерным массивам?) UPD. сам конечно дурак, что полез в datasheet по движку после того как начал разбираться с ошибкой валидатора.. но может для таких идиотов добавить это описание в тело задачи?
Учиха Шисуи Уровень 22 Expert
18 октября 2019

Преимущества списков:

В отличие от массивов, списки могут динамически менять размер. 
Сразу после создания список имеет размер 0
Это не так. Сразу после создания список имеет размер 10 ячеек. И 0 элементов на борту.
Artem Уровень 11
3 апреля 2019
Как скомпилировать игру, чтобы она хранилась на компе? ну типо в .exe сделать её. Видео по компиляции в intelliJ не помогли.
Сергей Уровень 18
18 марта 2019
Офигенная идея! Очень интересно и понятно когда делаешь по шагам, с подсказками направления и валидатором. Спасибо за раздел!
VIKTOR NEZHELSKIY Уровень 27 Expert
8 марта 2019
Про перечисление Color ничего не сказали. Хотелось бы иметь список имён доступных цветов перечисленных в классе Color. И картинку с номерами цветов.
Vesa Уровень 18
1 января 2019
Не понял зачем делать инверсию. Зачем нам надо привязываться к Декартовой системе координат? Есть игровое поле, где каждая ячейка имеет свой однозначный адрес [№ строки] [№ столбца]. >Например, метод setCellValue(int x, int y, String value). Он устанавливает определенной клетке с координатами (x, y) значение value. Как вы заметили, этот метод вначале принимает именно x, как в классической системе координат. Не заметил. Метод принимает два параметра, которые могут иметь какие угодно имена и как угодно интерпретироваться в самом методе. Зачем сделали инверсию?