Разбор домашнего задания
Домашнее задание прошлго урока было довольно сложным. Поэтому было решено разобраться с тем как его сделать правильно.
Задание 1. Крепкий металлический блок
Предлагаю пример игры Арканоид, где есть несколько разных блоков. Один вид блоков взрывается при первом ударе шара, другой - от двух, третий - от трех и четвертый - от пяти ударов.
Обратите внимание на металлический блок, который взрывается от пяти ударов. Так как у нас только 4 возможных состояний "разбитости" блоков, то пятый мы сделали за счет того, что материал блока меняется - поэтому и при последнем ударе блок выглядит по-другому.
Этот пример решает первый пункт домашнего задания
public class MyGame extends Game {
@Override
public void setup() {
setBackground(new DesertBackground());
register('#', WallSprite::new)
.onInit(w -> w.setColor(WallColor.BLUE));
register('w', TrapSprite::new)
.onInit(w -> w.setMaterial(TrapMaterial.FIRE));
register('C', BlockSprite::new)
.onInit(b -> b.setMaterial(BlockMaterial.CITRINE))
.onCollision(ai::halt)
.onHalt(this::explodeCitrineBlock);
register('B', BlockSprite::new)
.onInit(b -> b.setMaterial(BlockMaterial.BUBBLESTONE))
.onCollision(b -> b.activate("CRACK1"))
.defineState("CRACK1")
.onActivation(b -> b.setCrackLevel(3))
.onCollision(ai::halt)
.onHalt(this::explodeBubbleBlock);
register('S', BlockSprite::new)
.onInit(b -> b.setMaterial(BlockMaterial.SAND))
.onCollision(b -> b.activate("CRACK1"))
.defineState("CRACK1")
.onActivation(b -> b.setCrackLevel(1))
.onCollision(b -> b.activate("CRACK2"))
.defineState("CRACK2")
.onActivation(b -> b.setCrackLevel(3))
.onCollision(ai::halt)
.onHalt(this::explodeSandBlock);
register('M', BlockSprite::new)
.onInit(b -> b.setMaterial(BlockMaterial.METAL))
.onCollision(b -> b.activate("CRACK1"))
.defineState("CRACK1")
.onActivation(b -> b.setCrackLevel(1))
.onCollision(b -> b.activate("CRACK2"))
.defineState("CRACK2")
.onActivation(b -> b.setCrackLevel(2))
.onCollision(b -> b.activate("CRACK3"))
.defineState("CRACK3")
.onActivation(b -> b.setCrackLevel(3))
.onCollision(b -> b.activate("CRACK4"))
.defineState("CRACK4")
.onActivation(b -> b.setMaterial(BlockMaterial.CONCRETE))
.onCollision(ai::halt)
.onHalt(this::explodeMetalBlock);
load("/level.txt", this::onLoad);
}
private void onLoad() {
sprite(CaretSprite::new, getWidth() / 2, getHeight()-1.5)
.onInit(c -> c.setSpeed(15))
.onLoop(ai::followMouseX)
.onCollision(ai::stopX);
createBall();
}
private void createBall() {
sprite(BallSprite::new, getWidth() / 2, getHeight()-2.5)
.onInit(b -> b.setSpeed(12))
.onInit(b -> b.setDirection(Direction.NE))
.onCollision(ai::halt, TrapSprite.class)
.onHalt(this::explodeBall)
.onLoop(ai::turnToDirection)
.onLoop(ai::followDirection)
.onCollision(ai::bounce);
}
private void explodeBall(BallSprite b) {
sprite(DirectedBlastSprite::new, b.getX(), b.getY())
.onInit(e -> e.setRotation(Direction.S))
.onInit(DirectedBlastSprite::explode);
createBall();
}
private void explodeMetalBlock(BlockSprite b) {
sprite(RoundBlastSprite::new, b.getX(), b.getY())
.onInit(e -> e.setColor(RoundBlastColor.BLUE))
.onInit(RoundBlastSprite::explode);
}
private void explodeCitrineBlock(BlockSprite b) {
sprite(RoundBlastSprite::new, b.getX(), b.getY())
.onInit(e -> e.setColor(RoundBlastColor.GREEN))
.onInit(RoundBlastSprite::explode);
}
private void explodeBubbleBlock(BlockSprite b) {
sprite(CollapseBlastSprite::new, b.getX(), b.getY())
.onInit(e -> e.setColor(CollapseBlastColor.BLUE))
.onInit(CollapseBlastSprite::explode);
}
private void explodeSandBlock(BlockSprite b) {
sprite(CollapseBlastSprite::new, b.getX(), b.getY())
.onInit(e -> e.setColor(CollapseBlastColor.RED))
.onInit(CollapseBlastSprite::explode);
}
@Override
public void loop() {
}
}
Кода много, но тут и шарик и каждый блок взрывается по-своему - получилась почти полноценная игра.
Чтобы не тратить время на рисование уровня - вот пример, как он может выглядеть:
##################################
#S S S S S S S S S S S S S S S S #
#C C C C C C C C C C C C C C C C #
#B B B B B B B B B B B B B B B B #
#B B C B S M M M M M M S B C B B #
#B B B B B B B B B B B B B B B B #
#C C C C C C C C C C C C C C C C #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
#w w w w w w w w #
Спрайт каретки и шарик у нас появляются автоматически, поэтому нам не нужно рисовать их на уровне.
Задание 2. Разобраться с сообщениями
Концепция сообщений очень простая, на самом деле. Подробно она описана на страничке прошлого урока.
Идея в том, что мы можем отправить какое либо сообщение. Это сообщение получат все спрайты, нарисованные на экране. Но только те спрайты, которые настроены на это конкретное сообщение будут реагировать.
Задание 3. Огненный шар
Этим свойством сообщений мы воспользуемся, чтобы изменить состояние шарика.
Дополним код металлического блока, чтобы он отправлял сообщение, когда взрывается:
.onHalt(b -> message("fire"))
это можно добавить в одно из двух мест кода: в состояние по умолчанию или в последнее состояние этого блока.
Так же изменим код шара, чтобы он реагировал на это сообщение и включал бы пламя:
.onMessage(b -> b.setFlame(true), "fire")
Задание 4. Уничтожение блоков без отскока
Нам нужно сделать так, чтобы шарик вел себя по-разному в зависимости от того огненный он или нет. Для этого нужно переписать код шара так, чтобы было два состояния - нормальное и огненное. В нормальном состоянии шар отскакивает от всего, а в огненном только от каретки и стен. Таким образом в "огненном" состоянии шарик не будет отталкиваться от блоков, а просто пролетать сквозь. Так как блоки сами реагируют на столкновение с шаром - то они будут взрываться сами.
В самом начале, как только шарик появится на экране мы переходим в "нормальное" состояние. А при получении сообщения "fire" выключим нормальное состояние и включим "огненное":
sprite(BallSprite::new, getWidth() / 2, getHeight() - 2.5)
.onInit(b -> b.setSpeed(12))
.onInit(b -> b.setDirection(Direction.NE))
.onCollision(ai::halt, TrapSprite.class)
.onHalt(this::explodeBall)
.onLoop(ai::turnToDirection)
.onLoop(ai::followDirection)
.onInit(b -> b.activate("normal"))
.defineState("normal")
.onCollision(ai::bounce)
.onMessage(b -> b.deactivate("normal"), "fire")
.onMessage(b -> b.activate("fireball"), "fire")
.defineState("fireball")
.onActivation(b -> b.setFlame(true))
.onCollision(ai::bounce, CaretSprite.class, WallSprite.class);
Чтобы выключить состояние мы используем deactivate("normal")
Чтобы отловить тот момент, когда включается состояние "fireball" мы используем событие onActivation
Задание 5. Отскок от металических блоков.
Все что нам нужно сделать - это добавить реакцию на металические блоки, когда шарик в состоянии "fireball". Мы не можем использовать BlockSprite.class, так как это будет реакция на все блоки, а не только на металические.
У нас есть возможность добавлять реацию onCollision не только по классу спрайтов, но и по символу. Воспользуемся этой возможностью и добавим еще один onCollision в огненное состояние:
.defineState("fireball")
.onActivation(b -> b.setFlame(true))
.onCollision(ai::bounce, CaretSprite.class, WallSprite.class)
.onCollision(ai::bounce, 'M');
Вот и все! Домашняя работа готова. Самые крепкие орешки оказались всего парой строчкой кода!
Новая тема
Загрузка нового уровня
Обычно игры состоят из нескольких уровней. Загружать новые уровни можем и мы!
Как это сделать написано на страничке прошлого урока младшей группы: 13 ноября 2016 г
Управление клавишами
Результат работы "загрузка нового уровня" - игра лабиринт с тремя уровнями.
Шарик в этой игре управляется мышкой. Но мы хотим управлять при помощи клавиш вверх, вниз, вправо и влево.
Для клавиш у нас есть три события:
onKeyHold | событие срабатывает на каждый кадр игры, если определенная клавиша нажата
|
onKeyPressed | срабатывает только один раз на каждое нажатие
|
onKeyReleased | срабатывает только один раз, когда клавиша отпущена
|
Пример onKeyPressed
Предположим нам нужно, чтобы шарик летал по экрану и останавливался только, если врезается в стену. Клавишами мы можем поменять его направление.
register('o', BallSprite::new)
.onInit(b -> b.setSpeed(10))
.onCollision(ai::stopXY)
.onLoop(ai::followDirection)
.onKeyPressed(b -> b.setDirection(Direction.N), KeyCode.UP)
.onKeyPressed(b -> b.setDirection(Direction.S), KeyCode.DOWN)
.onKeyPressed(b -> b.setDirection(Direction.W), KeyCode.LEFT)
.onKeyPressed(b -> b.setDirection(Direction.E), KeyCode.RIGHT);
KeyCode содержит все возможные клавиши клавиатуры. Там есть и буквы и цифры и стрелки и функциональные клавиши.
Пример onKeyHold
Предположим мы хотим управлять шариком стрелками клавиатуры, но так, чтобы шарик двигался пока нажата клавиша. Как только клавишу отпускаем - шарик останавливается.
Один из простых способов как этого добиться:
register('o', BallSprite::new)
.onInit(b -> b.setSpeed(10))
.onCollision(ai::stopXY)
.onKeyPressed(b -> b.setDirection(Direction.N), KeyCode.UP)
.onKeyPressed(b -> b.setDirection(Direction.S), KeyCode.DOWN)
.onKeyPressed(b -> b.setDirection(Direction.W), KeyCode.LEFT)
.onKeyPressed(b -> b.setDirection(Direction.E), KeyCode.RIGHT)
.onKeyHold(ai::followDirection, KeyCode.UP)
.onKeyHold(ai::followDirection, KeyCode.DOWN)
.onKeyHold(ai::followDirection, KeyCode.LEFT)
.onKeyHold(ai::followDirection, KeyCode.RIGHT);
Здесь код очень похож на предыдущий, только теперь followDirection вызывается у нас не на onLoop, а на onKeyHold. Таким образом если нажать кнопку, например, вверх, то вначале сработает событие onKeyPressed и мы зададим новое направление для шарика (Direction.N - север, вверх).
Но на каждый кадр будет срабатывать onKeyHold для клавиши вверх и соответственно будет вызываться followDirection, что будет двигать шарик.
Домашнее задание
Как я и обещал домашнего задания не будет. Для тех кто отсутствовал или хочет просто повторить материал - предлагаю самостоятельно перевести игру "лабиринт" на управление клавишами.