В
разделе «Игры» на 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);
|
Удалить элемент из списка
|
Больше о списках можете узнать из этих статей:
- Класс ArrayList
- Работа ArrayList в картинках
- Удаление элемента из списка 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%.
Представьте, что вы пишете эмулятор человеческого утра. Вам нужно вызывать события с определенной вероятностью. Для этого, опять-таки, надо воспользоваться генератором случайных чисел. Реализации могут быть разные, но самая простая должна происходить по следующему алгоритму:
- устанавливаем пределы, в которых нужно сгенерировать число;
- генерируем случайное число;
- обрабатываем полученное число.
Итак, в данном случае пределом будет 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("Встал на час раньше положенного ");
}
Вообще, вариантов применения случайных чисел может быть очень много. Все зависит только от вашей фантазии. Но наиболее эффективно их применять, если нужно многократно получать какой-нибудь результат. Тогда этот результат будет отличаться от предыдущего. С какой-то вероятностью, естественно.
На этом все!
Если вы хотите узнать о разделе "Игры" больше, вот полезная документация, которая может в этом помочь:
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ