Старшая группа

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() {

}
}


11 декабря 2016 г

Отправлено 3 янв. 2017 г., 10:40 пользователем Dimitrijs Fedotovs   [ обновлено 3 янв. 2017 г., 10:41 ]


Тема урока

Часто встречаются игры, в которых нужно управлять персонажем, передвигающемуся по поверхностям, и если под ногами ничего нет - персонаж падает вниз. Так же можно ползать по стенкам. Например, вот такая игра:


Логика (и код) такого персонажа довольно сложная. Но к счастью этот код уже написан. Все что нам нужно - подключить искуственный интеллект под названием GravityAI, во время регистрации спрайта.

"Скелет" игры

Для начала зарегистрируем все нужные спрайты:
register('#', WallSprite::new)
.onInit(w -> w.setColor(WallColor.BLUE));

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

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

register('T', ManSprite::new);

Подключение искусственного интеллекта пишется в строчке регистрации:

register('T', ManSprite::new, new GravityAI().wall('#').ladder('H'));

Теперь человечек будет падать вниз, если там пустое пространство. При промощи метода wall и ladder мы настраиваем, какие спрайты будут являтся стенками, а какие лестницами.

Осталось только добавить управление клавишами "вверх", "вниз", "вправо" и "влево", а так же прыжок клавишей "пробел".

Движение вправо и влево

Начнем с передвижения вправо и влево. Когда мы нажимаем клавишу вправо или влево, надо установить скорость (setSpeed), повернуть спрайт в нужном направлении (setRotation), задать направление движения (setDirection) и двигать спрайт по этому направлению (followDirection). Это довольн много операций, поэтому чтобы сократить код - напишем это в отдельном методе:

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

Этот метод будем вызывать тогда, когда нажата клавиша вправо или влево. А когда клавишу отпускаем - нужно установить скорость в 0:

register('T', ManSprite::new, new GravityAI().wall('#').ladder('H'))
.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);

Теперь, при нажатии на "пробел" человечек "переключается" в режим прыжка, взлетая вверх на несколько клеток, а потом опять падает вниз.

Код целиком

Вот что должно было получится:
public class MyGame extends Game {

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

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

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

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

register('T', ManSprite::new, new GravityAI().wall('#').ladder('H'))
.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");
}

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


@Override
public void loop() {

}
}

20 ноября 2016 г

Отправлено 20 нояб. 2016 г., 12:30 пользователем Dimitrijs Fedotovs   [ обновлено 20 нояб. 2016 г., 13:51 ]

Разбор домашнего задания

Домашнее задание прошлго урока было довольно сложным. Поэтому было решено разобраться с тем как его сделать правильно.

Задание 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, что будет двигать шарик.

Домашнее задание

Как я и обещал домашнего задания не будет. Для тех кто отсутствовал или хочет просто повторить материал - предлагаю самостоятельно перевести игру "лабиринт" на управление клавишами.

6 ноября 2016 г

Отправлено 7 нояб. 2016 г., 14:13 пользователем Dimitrijs Fedotovs   [ обновлено 9 нояб. 2016 г., 9:11 ]


Пройденный материал

  • Состояния спрайтов
    • конструкция состояния - defineState("state name")
    • включение состояния - activate("state name")
    • событие - onActivation(...)

Трещины на блоках

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

Для того чтобы установить "уровень разбитости" блока существует метод setCrackLevel(цифра). Где цифра - это уровень разбитости от 0 до 3.

Вот таблица всех материалов блоков и "уровней разбитости":

  0 1 2 3
 CANDY 
 
 
 
 AMBER 
 
 
 
 METAL 
 
 
 
 BUBBLESTONE 
 
 
 
 GREENSTONE 
 
 
 
 CITRINE 
 
 
 
 BRICK 
 
 
 
 SAND 
 
 
 
 CONCRETE 
 
 
 

Пример:
register('*', BlockSprite::new)
.onInit(b -> b.setMaterial(BlockMaterial.SAND))
.onInit(b -> b.setCrackLevel(2));

Состояния спрайтов

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

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

Состояние - это набор обработчиков событий, которые работают только тогда, когда это состояние активно.

Мы можем написать свои обработчики (например onCollision) для каждого состояния по-разному, поэтому спрайт будет вести себя по-разному, в зависимости от того, в каких он находится состояниях. Так же мы сами можем переключать спрайт из состояния в состояние.

Состояние по умолчанию

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

Состояние по умолчанию нельзя отключить - оно есть всегда и запускается автоматически вместе со спрайтом.

register('+', BlockSprite::new)
// onInit для состояния по умолчанию
.onInit(b -> b.setMaterial(BlockMaterial.BRICK));

Включение состояния

Для того чтобы активировать состояние нужно вызвать метод activate("state name"):

register('+', BlockSprite::new)
// onInit для состояния по умолчанию
.onInit(b -> b.setMaterial(BlockMaterial.BRICK))
 // при столкновении перейти в состояние CRACK
.onCollision(b -> b.activate("CRACK"));

Определение состояний

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

Чтобы определить новое состояние нужно воспользоваться конструкцией defineState("state name") - где, state name - любое название, которое мы даем определяемому состоянию.

register('+', BlockSprite::new)
// onInit для состояния по умолчанию
.onInit(b -> b.setMaterial(BlockMaterial.BRICK))
  // при столкновении перейти в состояние CRACK
.onCollision(b -> b.activate("CRACK"))

// определяем состояние CRACK
.defineState("CRACK");

Теперь все что будет написано ниже defineState("CRACK") будет срабатывать только тогда, когда состояние CRACK активно, т.е. после первого столкновения с мячом.

В состоянии CRACK мы хотим, чтобы на спрайте блока нарисовалась одна трещина и при втором столкновении блок должен исчезнуть:

register('+', BlockSprite::new)
// onInit для состояния по умолчанию
.onInit(b -> b.setMaterial(BlockMaterial.BRICK))
// при столкновении перейти в состояние CRACK
.onCollision(b -> b.activate("CRACK"))

// определяем состояние CRACK
.defineState("CRACK")
// все описанные ниже обработчики событий
// будут срабатывать только если
// состояние CRACK активно:
// в момент перехода в состояние CRACK
// установить трещины на блоке
.onActivation(b -> b.setCrackLevel(1))
// при втором столкновении - блок должен исчезнуть
.onCollision(ai::halt);

Событие onActivation

В тот момент, когда состояние становится активным - вызывается событие onActivation, если конечно оно написано для нашего состояния. В предыдущем примере по событию onActivation для состояния CRACK мы добавляем трещины на блоке.

Еще более "крепкий" блок

Если мы хотим сделать еще более крепкий блок, нам нужно добавить еще одно состояние. Код блока может выглядеть вот так:

register('+', BlockSprite::new)
.onInit(b -> b.setMaterial(BlockMaterial.BRICK))
.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(ai::halt);

Дополнительно

Сообщения

До сих пор события происходили у каждого спрайта индивидуально и мы так же их обрабатывали индивидуально для каждого спрайта. Но если нам нужно по событию одного спрайта поменять что-то на других спрайтах? Например, если шарик выбивает кирпичный блок (BRICK), все песочные блоки (SAND) должны взорваться.

Для этого существуют событие. Спрайт в любое время может отправить сообщение всем другим спрайтам. Другие спрайты могут на это событие отреагировать должным образом.

Отправка сообщений

Чтобы отправить сообщение нужно вызвать метод message("text") - где text - это любой текст сообщения (слова, цифры, знаки). Мы можем придумывать для себя любые сообщения - главное чтобы самим понимать зачем это сообщение нужно.

Получение сообщений

Другие спрайты могут получить сообщение. Для этого существует событие onMessage(обработчик, "text") - где обработчик - это то действие, которое должно произойти, а text - это должен быть тот текст, сообщения, который мы хотим "поймать".

Обработчик - это уже знакомый код, как ai::halt, b -> b.setColor(...) и т.д. и т.п.

Пример

Проще показать это на примере. Ниже зарегистрированы два блока. В момент, когда исчезает (выбивается) кирпичный блок (BRICK), отправляется сообщение message("bang!").

Все песочные блоки (SAND) отлавливают это сообщение и так же исчезают: onMessage(ai::halt, "bang!").

register('+', BlockSprite::new)
.onInit(b -> b.setMaterial(BlockMaterial.BRICK))
.onCollision(ai::halt)
.onHalt(b -> message("bang!"));

register('*', BlockSprite::new)
.onInit(b -> b.setMaterial(BlockMaterial.SAND))
.onInit(b -> b.setCrackLevel(2))
.onCollision(ai::halt)
.onMessage(ai::halt, "bang!");

ВНИМАНИЕ! очень важно не ошибиться в тексте сообщения. Если в событии onMessage будет написан текст, отличающийся хоть одной буквой от отправленного сообщения при помощи message - событие не сработает.

Домашнее задание

  1. В игре Арканоид сделать специальный очень крепкий блок из материала METAL, который возможно выбить, попав в него 4 раза. У нас только 3 уровня трещин (1, 2 и 3), поэтому для четвертого раза последний уровень (3) пусть повторится.
  2. Разобраться с сообщениями (описаны выше)
  3. КРЕПКИЙ ОРЕШЕК:
    Когда металлический блок выбивается шариком, шарик должен стать огненным (setFlame(true)). Сделать это при помощи сообщений.
  4. ОЧЕНЬ КРЕПКИЙ ОРЕШЕК:
    Когда шарик становится огненным - он должен пролетать через и уничтожать все блоки, не отскакивая от них.
    подсказка: у шарика должно быть два дополнительных состояния "нормальный" - где он будет отскакивать от блоков и "огненный" - где не будет отскока.
  5. СОВСЕМ КРЕПКИЙ ОРЕШЕК (если со всеми заданиями справились):
    Огненный шарик пролетает сквозь все блоки, кроме металлических. От металлических просто отскакивает.

16 октября 2016 г

Отправлено 16 окт. 2016 г., 13:26 пользователем Dimitrijs Fedotovs   [ обновлено 16 окт. 2016 г., 13:51 ]


Пройденный материал

  • Спрайт каретки CaretSprite
    • создание спрайта вручную
    • движение только по горизонтали
  • Новое событие onHalt - вызывается после того, как спрайт "умер"
  • Создание собственных методов
  • Новый спрайт взрыва DirectedBlastSprite

Ход занятия

Шаг 1: Создать новый проект.

Запускаем IntelliJ IDEA и выбираем Create New Project или File -> New Project

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

Шаг 2: создаем каретку "вручную"

В автоматически созданном коде MyGame модифицируем вызов метода load: 
load("/level.txt", this::create);

create подсвечивается красным, т.к. программа ничего не знает о таком методе. Простой способ создать такой - нажать alt+enter и выбрать create method 'create'.

Таким образом сразу после загрузке уровня level.txt программа вызовет метод create. В данный момент этот метод пустой.

Теперь напишем логику создания каретки в методе create:

private void create() {
sprite(CaretSprite::new, getWidth() / 2, getHeight() - 1.5)
.onLoop(ai::followMouseX)
.onInit(c -> c.setSpeed(15))
.onCollision(ai::stopX);
}

Мы используем метод sprite, а не register, потому что нам нужно создать спрайт уже после того как уровень загрузился. Метод register регистрирует символ для спрайта, а спрайт создается позже - во время загрузки уровня.

Новый спрайт каретки появится в координате getWitdh() / 2 по X и getHeight() - 1.5 по Y.

getWidth() - метод возвращающий ширину загруженного уровня. Мы делим его на два, чтобы разместить каретку в середине.

getHeight() - 1.5 дает координату чуть выше нижнего края уровня (на полторы клетки)

onLoop(ai::followMouseX) - заставляет на каждый кадр спрайт смещаться только по координате X, следуя за курсором мышки.

onInit(c -> c.setSpeed(15)) - устанавливает скорость движение каретки перед тем как каретка покажется на экране.

onCollision(ai::stopX) - при столкновении с препятствием (например стенкой) - остановить каретку.

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

Шаг 3: создаем мяч "вручную"

Создание каретки должно влечь за собой создание нового мяча. Поэтому сразу после кода управления кареткой в методе create добавим еще одну строчку - вызов метода createBall(). Этот метод так же нужно создать нажав alt+enter, как и было в предыдущем примере.

private void create() {
sprite(CaretSprite::new, getWidth() / 2, getHeight() - 1.5)
.onLoop(ai::followMouseX)
.onInit(c -> c.setSpeed(15))
.onCollision(ai::stopX);
createBall();
}

private void createBall() {
sprite(BallSprite::new, getWidth() / 2, getHeight() - 3)
.onLoop(ai::followDirection)
.onInit(c -> c.setSpeed(10))
.onCollision(ai::bounce);
}

Метод createBall() создает новый спрайт мяча. Помещает его так же в центре нижней части уровня, только чуть-чуть выше, чем каретка.

onLoop(ai::followDirection) - На каждый кадр игры, мы должны смещать шарик по направлению установленному в свойствах шарика.

onInit(c -> c.setSpeed(10)) - при запуске спрайта установить его скорость в 10 клеток в секунду. Иначе мяч будет стоять на месте.

Если хочешь, что бы шарик сразу летел в заданном направлении необходимо дописать еще один обработчик onInit:

.onInit(c -> c.setDirection(Direction.W))

onCollision(ai::bounce) - заставляет отскакивать мячь от любых спрайтов.

Шаг 4: Ловушка для шарика.

Самостоятельно заполняем нижнюю часть уровня спрайтами ловушек. Как это делать - можно подсмотреть в записках предыдущего урока: 2 октября 2016 г

Добавляем реакцию шара на столкновение с ловушкой:

.onCollision(ai::halt, TrapSprite.class)

Шаг 5: Взрыв шарика

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

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

.onHalt(this::explode);

и так же необходимо описать что такое explode.

в общем должно получится так:

private void createBall() {
sprite(BallSprite::new, getWidth() / 2, getHeight() - 3)
.onLoop(ai::followDirection)
.onInit(c -> c.setSpeed(10))
.onCollision(ai::bounce)
.onCollision(ai::halt, TrapSprite.class)
.onHalt(this::explode);
}

private void explode(BallSprite s) {
sprite(DirectedBlastSprite::new, s.getX(), s.getY())
.onInit(b -> b.setDirection(Direction.S))
.onInit(b -> b.explode());
createBall();
}

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

Так же из метода explode мы вызываем createBall() чтобы создать новый мяч после того как предыдущий уничтожился.

Код игры

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

public class MyGame extends Game {

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

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

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

private void create() {
sprite(CaretSprite::new, getWidth() / 2, getHeight() - 1.5)
.onLoop(ai::followMouseX)
.onInit(c -> c.setSpeed(15))
.onCollision(ai::stopX);
createBall();
}

private void createBall() {
sprite(BallSprite::new, getWidth() / 2, getHeight() - 3)
.onLoop(ai::followDirection)
.onInit(c -> c.setSpeed(10))
.onCollision(ai::bounce)
.onCollision(ai::halt, TrapSprite.class)
.onHalt(this::explode);
}

private void explode(BallSprite s) {
sprite(DirectedBlastSprite::new, s.getX(), s.getY())
.onInit(b -> b.setDirection(Direction.S))
.onInit(b -> b.explode());
createBall();
}

@Override
public void loop() {

}
}

Дополнительно

Варианты DirectedBlastSprite

Создавая DirectedBlastSprite можно выбрать один из трех его вариантов при помощи метода setColor(DirectedBlastColor):
  • YELLOW
  • RED
  • GREEN
Пример:
sprite(DirectedBlastSprite::new, ball.getX(), ball.getY())
.onInit(e -> e.setRotation(Direction.S))
.onInit(e -> e.setColor(DirectedBlastColor.YELLOW))
.onInit(Explosion::explode);

Спрайт RoundBlastSprite

Аналогично DirectedBlastSprite можно использовать RoundBlastSprite. Этот взрыв хорош для блоков.

Так же существует несколько вариантов цвета для такого взрыва setColor(RoundBlastColor.BLUE)

  • YELLOW
  • GREEN
  • BLUE
  • RED

Спрайт CollapseBlastSprite

Еще один спрайт для взрывов:  CollapseBlastSprite. Как и предыдущие спрайты взрывов и этот можно покрасить при помощи setColor(CollapseBlastColor.GREEN):
  • RED
  • BLUE
  • GREEN

Домашнее задание

Создать игру Арканоид, в которой будут минимум два (можно больше) разных видов блоков.

Каждый вид блока должен взрываться своим собственным взрывом.

2 октября 2016 г

Отправлено 6 окт. 2016 г., 10:42 пользователем Dimitrijs Fedotovs   [ обновлено 16 окт. 2016 г., 13:48 ]


Пройденый материал

  • События спрайтов
  • Манипулации с событиями и спрайтами
  • Смена фонов игры

Разобрались с событиями, которые могут происходить у спрайта

  • Инициализация - onInit
  • На каждый кадр (фрейм) - onLoop
  • При столкновении (коллизии) с другими спрайтами - onCollision

Примеры, которые использовали на занятии:

Огненный мяч, который улетает (за экран) в заданном направлении

       // зарегистрировать символ 'o' для создания шарика (BallSprite)
register('o', BallSprite::new)                   
           // при инициализации "включить" огненный хвост
    .onInit(b -> b.setFlame(true))
           // при инициализации повернуть шар "лицом" на юго-восток
    .onInit(b -> b.setRotation(Direction.SE))    
           // при инициализации указать направление движения на запад
    .onInit(b -> b.setDirection(Direction.W))    
           // при инициализации указать скорость движения 10 клеток в секунду
    .onInit(b -> b.setSpeed(10))                 
           // на каждый кадр сдвигать шарик в указанном направлении с указанной скоростью
    .onLoop(ai::followDirection);

Мяч, который отскакивает от других спрайтов

    // зарегистрировать символ 'o' для создания шарика (BallSprite)
register('o', BallSprite::new)                   
           // при инициализации указать направление движения на северо-восток
    .onInit(b -> b.setDirection(Direction.NE))   
           // при инициализации указать скорость движения 10 клеток в секунду
    .onInit(b -> b.setSpeed(10))                 
           // на каждый кадр сдвигать шарик в указанном направлении с указанной скоростью
    .onLoop(ai::followDirection)                 
           // на каждый кадр поворачивать шарик "лицом" по направлению движения
    .onLoop(ai::turnToDirection)                 
           // при столкновении с другим (любым) спрайтом - совершить отскок
    .onCollision(ai::bounce);

Блок, который исчезает, если в него врезается другой (любой) спрайт

    // зарегистрировать символ 'x' для создания блока (BlockSprite)
register('x', BlockSprite::new)                  
         // если что-то врезалось в блок - блок перестает существовать (halt)
    .onCollision(ai::halt);

Мяч отскакивает от других спрайтов и исчезает, если врезается в спрайт огня

Код аналогичный предыдущему примеру, только с дополнительной строчкой в конце, указывающей, что надо шарику исчезнуть, если врезался в огонь:

           // при столкновении со спрайтом огня - удалить шарик из игры
    .onCollision(ai::halt, TrapSprite.class);

Мяч, следующий за курсором мышки

    // зарегистрировать символ 'o' для создания шарика (BallSprite)
register('o', BallSprite::new)                   
        // при инициализации указать скорость движения 10 клеток в секунду
    .onInit(b -> b.setSpeed(10))                 
        // на каждый кадр сдвигать шарик по координатам X и Y
        // в направлении курсора мышки, с указанной скоростью

    .onLoop(ai::followMouseXY)                   
        // на каждый кадр поворачивать шарик "лицом" по направлению к курсору мышки
    .onLoop(ai::turnToMouse)                     
        // при столкновении с другим (любым) спрайтом - остановиться
    .onCollision(ai::stopXY);

Direction

Направления, которые можно использовать

I, Andrew pmk [GFDL (http://www.gnu.org/copyleft/fdl.html) or CC-BY-SA-3.0 (http://creativecommons.org/licenses/by-sa/3.0/)], via Wikimedia Commons

Смена фона

На данный момент существуют три различных фона, которые можно поменять при помощи команды setBackground:

  • Пустыня - setBackground(new DesertBackground());
  • Горы и вулкан - setBackground(new VolcanoBackground());
  • Инопланетная тема - setBackground(new SpaceBackground());

Дополнительные примеры

Вид ловушки (огонь, лава, вода)

register('w', TrapSprite::new)
    .onInit(t -> t.setMaterial(TrapMaterial.FIRE));

Так же можно использовать другие материалы ловушки:

  • ACID
  • LAVA
  • WATER
  • PLASMA
  • FIRE

Цвет шарика

register('o', BallSprite::new)
     .onInit(b -> b.setColor(BallColor.YELLOW));

Возможны следующие цвета:

  • BLUE
  • YELLOW
  • VIOLET
  • GREEN

Материал блока

register('=', BlockSprite::new)
     .onInit(b -> b.setMaterial(BlockMaterial.METAL));

Возможны следующие варианты:

  • AMBER
  • CANDY
  • METAL
  • BUBBLESTONE
  • GREENSTONE
  • CITRINE
  • BRICK
  • SAND
  • CONCRETE

Цвет стены

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

Возможны следующие цвета:

  • ORANGE
  • GREEN
  • BLUE
  • GRAY
  • BLACK

Домашнее задание

Для родителей

Для детей

  • Повторить все что делали на занятии, но по мере возможности самостоятельно

18 сетября 2016 г.

Отправлено 6 окт. 2016 г., 10:06 пользователем Dimitrijs Fedotovs   [ обновлено 10 окт. 2016 г., 8:59 ]

Пройденный материал:

  • что такое алгоритм
  • элементы игр (спрайты, уровни, коллизии)
  • ознакомление с IDE, и редактирование уровня игры

Создание спрайта на экране

register('#', WallSprite::new);     // стена

register('o', BallSprite::new);     // шарик

register('-', BlockSprite::new);    // блок

register('w', TrapSprite::new);     // лава

Домашнее задание:

  1. Представь, что ты дома, делаешь домашние задания. Вдруг захотелось чего то перекусить. (Яблоко, конфету и т.д. и т.п.). Нарисовать алгоритм действий.
    Постараться учесть все возможные условия. Например: "Сходить на кухню, посмотреть, есть ли яблоки. Если нет, сходить в магазин - купить. А есть ли деньги? Если нет попросить у родителей" и т.д.
    Нарисовать в виде блок-схемы. Пример, блок-схемы распечатанный выдан каждому ребенку. Если забыли - скачать здесь Sorting.pdf
  2. Придумать свою простую игру. Постараться уложиться в не более 3-5 персонажей. На бумаге в клеточку схематично нарисовать один или несколько уровней этой игры. Пример здесь: level.pdf
    На разных уровнях использовать по максимуму одни и те же персонажи/спрайты.
  3. Нарисовать блок-схему поведения персонажей. Например: algorithm_monster.pdf

1-7 of 7