8 января 2017 г

Отправлено 9 янв. 2017 г., 14:18 пользователем Dimitrijs Fedotovs   [ обновлено 14 янв. 2017 г., 10:02 ]

Тема занятия

Во многих играх персонажу приходится избегать различных опасных мест: дотронувшись до огня или до врага - теряется попытка (жизнь).

До сих пор мы делали игры в которых было либо бесконечное количество попыток либо одна единственная. Сегодня мы научимся как считать израсходованные жизни и заканчивать игру, когда их не осталось вовсе.

Вдобавок (мы этого на уроке не делали) я покажу как заставить монстра двигаться вправо-влево создавая дополнительную сложность.

Рисуем уровень

Возьмем пример из предыдущего урока и добавим монстров и огонь - спрайты, которые человечку следует избегать.

level.txt

Наш уровень выглядит примерно так:
##################################
# # #
# # #
# # #
# T # * #
# ############H########
# H #
# H #
# A H #
############### #########H#
# H#
# # H#
# H#H # AH#
# H#H #####H####
# H#H A # H #
# #################### H #
# ####### H#
# # H#
# # H#
#wwww# ###### ##### ## H#
###### ####
# #
# A #
##################################

Регистрация спрайтов

К коду предыдущего урока (11 декабря 2016 г) добавим новые спрайты:
A - для монстра (привидение в моем случае)
w - огонь
register('w', FlameSprite::new);
register('A', GhostSprite::new);

Движение привидения (дополнительный материал)

Привидение должно двигаться по такому алгоритму:


Наши привидения будут двигаться от стенки до стенки. Соответственно идея простая - если привидение врезалось в одну стенку, то оно должно развернуться и лететь в другую, до стенки с другой стороны.

register('A', GhostSprite::new)
.onInit(a -> {
a.setSpeed(4);
a.setDirection(Direction.E);
})
.onLoop(ai::followDirection)
.onCollision(a -> {
if (a.getDirection() == Direction.E) {
a.setDirection(Direction.W);
} else {
a.setDirection(Direction.E);
}
}, '#');

В этом коде, при инициализации мы устанавливаем скорость - 4 и направление East - направо.

На уроке мы изучили, что код в фигурных скобках { } называется блоком кода. Блок кода удобно использовать, если нам в одном действии нужно выполнить сразу несколько команд. 

На каждый кадр игры мы сдвигаем привидение в указанном направлении при помощи onLoop(ai::followDirection).

Самое интересное в onCollision. Нам нужно поменять направление приведения, если оно врезалось в стенку. Для этого можно применить следующий алгоритм:


Именно это и написано в блоке кода в onCollision:

            if (a.getDirection() == Direction.E) {
a.setDirection(Direction.W);
} else {
a.setDirection(Direction.E);
}

if - это оператор условия. Если утверждение написанное в скобках верно, но срабатывает первый блок кода. Если не верно, то второй блок кода. 

При помощи getDirection() мы узнаем, а какое же направление было у спрайта. И если оно равно (==) направлению на восток (E - East), то изменяем направление на западное (W - West). Если же направление не было восточным (утверждение не верно), то это значит направление было западное. Соответсвенно противоположное будет восток (E).

Таким образом мы заставим привидение летать от стенки до стенки.

Очень важно не путать присвоение с проверкой равенства.

Присвоение пишется с одним знаком равно (=), а равенство с двумя (==)

Про операцию "присвоения" (=) читай дальше.

Подсчет попыток

Вернемся к основной теме урока - подсчет жизней.

Количество оставшихся жизней мы будем хранить в переменной. Чтобы объяснить компьютеру, что мы хотим что-то считать, нужно это объявить.

Наше объявление будет выглядеть следующим образом:

int lives = 3;

Эту строчку нужно располагать в классе, перед методом setup:

public class MyGame extends Game {

int lives = 3;

@Override
public void setup() {

Теперь в переменной lives лежит число 3. Нам нужно уменьшать это число каждый раз, когда человечек попадает в огонь или врезается в привидение.

Слово int значит, что в нашей переменной могут лежать только целые числа, такие как 1, 153, -33, 0. Дробные числа или строки в переменной lives находится не могут.

Уменьшение значения переменной выглядит так:

lives = lives - 1;

После первого выполнения такой строчки переменная lives будет содержать число уже не три, а два; после второго выполнения - один и так далее.

Нам нужно уменьшать количество жизней на событие onCollision у человечка:

register('T', ManSprite::new, new GravityAI().wall('#').ladder('H'))
.onCollision(t -> {
lives = lives - 1;
}, 'w', 'A')

Теперь количество жизней будет уменьшаться при столкновении с огнем или привидением. Нам нужно добавить код, который будет еще как то реагировать на уменьшение жизней.

Если жизней еще больше нуля или равно нулю, то нам нужно перезагрузить уровень игры, чтобы игрок начал все сначала. А если жизней уже меньше, чем ноль, то значит игрок израсходовал последнюю жизнь и надо заканчивать игру.

.onCollision(t -> {
lives = lives - 1;
if (lives >= 0) {
load("/level.txt");
} else {
ai.halt(t);
}
}, 'w', 'A')

Вот блок-схема (алгоритм) происходящего:


Отображаем количество оставшихся жизней

Теперь человечек исчезает после третьего столкновение с привидением или огнем, но на экране не выводится количество жизней. Нам нужно это как то исправить.

Создадим метод который будет отвечать за рисование сердечек - одно сердечко - одна жизнь.

private void drawLives() {
for (int i = 0; i < lives; i++) {
sprite(HeartSprite::new, i + 0.5, 0.5);
}
}

Здесь мы применили цикл. Цикл - это такая конструкция, позволяющяя повторять одно и тоже действие несколько раз.

В нашем случае мы повторяем рисование сердечка столько раз, сколько жизней осталось.

Осталось вызывать этот метод тогда, когда уровень загружается:

load("/level.txt", this::drawLives);

Игра готова!

Код полностью

public class MyGame extends Game {

int lives = 3;

@Override
public void setup() {
setBackground(new SpaceBackground());

register('#', WallSprite::new)
.onInit(w -> w.setColor(WallColor.BLUE));

register( '*', PortalSprite::new);

register('H', LadderSprite::new);

register('w', FlameSprite::new);

register('A', GhostSprite::new)
.onInit(a -> {
a.setSpeed(4);
a.setDirection(Direction.E);
})
.onLoop(ai::followDirection)
.onCollision(a -> {
if (a.getDirection() == Direction.E) {
a.setDirection(Direction.W);
} else {
a.setDirection(Direction.E);
}
}, '#');

register('T', ManSprite::new, new GravityAI().wall('#').ladder('H'))
.onCollision(t -> {
lives = lives - 1;
if (lives >= 0) {
load("/level.txt", this::drawLives);
} else {
ai.halt(t);
}
}, 'w', 'A')
.onKeyHold(t -> move(t, Direction.E), KeyCode.RIGHT)
.onKeyReleased(t -> t.setSpeed(0), KeyCode.RIGHT)
.onKeyHold(t -> move(t, Direction.W), KeyCode.LEFT)
.onKeyReleased(t -> t.setSpeed(0), KeyCode.LEFT)
.onKeyHold(t -> move(t, Direction.N), KeyCode.UP)
.onKeyReleased(t -> t.setSpeed(0), KeyCode.UP)
.onKeyHold(t -> move(t, Direction.S), KeyCode.DOWN)
.onKeyReleased(t -> t.setSpeed(0), KeyCode.DOWN)
.onKeyPressed(t -> t.activate("jump"), KeyCode.SPACE);


load("/level.txt", this::drawLives);
}

private void drawLives() {
for (int i = 0; i < lives; i++) {
sprite(HeartSprite::new, i + 0.5, 0.5);
}
}

private void move(Sprite sprite, Direction dir) {
sprite.setSpeed(3);
sprite.setDirection(dir);
sprite.setRotation(dir);
ai.followDirection(sprite);
}


@Override
public void loop() {

}
}


Comments