Предыдущие части: Давайте уже рисовать, Чёрный квадрат, Графический контекст, Расстановка объектов, Инициализация, Функции и библиотеки, Проектирование
В предыдущей части я дошел до места, где игрок уже видит перед собой игровое поле. Он видит себя, он видит роботов, стенки, и всё остальное. Игрок решает, в какую сторону ему нужно пойти, чтобы спастись от роботов. Приняв решение, игрок нажимает какую-то клавишу.
Программа должна понять, какую клавишу игрок нажал. Но проблема стоит гораздо шире. Откуда программа вообще узнает, что хоть какая-то клавиша была нажата?
Ожидание ввода изначально делалось очень просто. Давайте я расскажу про это на примере почты.
- Вы заходите на почту и спрашиваете – пришло какое-нибудь письмо?
- Вам отвечают – нет.
- Вы выходите из почты.
- Вы сразу поворачиваетесь и заходите обратно.
То есть вы делаете опрос в цикле. Есть письмо? – Нет. Повторим. Есть письмо? – Нет. Повторим. И так до тех пор, пока не придет письмо.
В чём недостаток данного метода?
Посмотрите сами: если вы всё время заходите обратно на почту, то вы не можете заниматься никакими другими делами. Процессор крутится в цикле, не делая больше ничего. Раньше, когда компьютеры были однозадачными, это было нормой. Одна программа – одна задача. Если программа крутится в цикле, ей всё равно больше нечего делать, а другие программы в это время не работают.
Возможен и другой вариант.
- Вы заходите на почту и спрашиваете – пришло какое-нибудь письмо?
- Вам отвечают – нет.
- Вы выходите из почты и некоторое время делаете какие-то свои дела.
- Поделав дела, вы снова идете на почту.
В этом варианте программа тоже ожидает ввода, но между опросами она может делать что-то ещё. Этот метод подходит для динамичных игр, где что-то происходит без действия пользователя (как Tetris или Doom), но недостаток тоже есть: если свои дела вы делаете слишком долго, то на почту будете приходить редко. И если письмо придет, пока вы заняты, то вы получите его с опозданием. Для пользователя это будет выражаться в том, что игра будет с задержкой реагировать на его действия. Сейчас мне это, конечно, совсем не грозит.
Наконец, третий вариант.
- Вы приходите на почту и говорите – Вот мой телефон. Когда придет письмо, позвоните.
- Вы отправляетесь делать свои дела или просто спать, и на почту больше не приходите.
Как можно видеть, здесь вам вообще не нужен никакой цикл. Вы полностью свободны. Как только на почту придет письмо, вы получите звонок и сможете сразу же забрать письмо, чем бы вы ни занимались в этот момент.
В JavaScript возможен только третий вариант. Поэтому я буду делать более-менее общую реализацию игры на его основе.
Как я уже говорил, JavaScript выполняется в среде HTML-документа, загруженного в браузер. Поэтому просить "позвонить" надо у этой среды. Но сначала мне нужен собственно "телефон", куда можно звонить. Я напишу функцию, которая должна обрабатывать состояние игры, получив событие:
function process_game(event) {}
Я не буду пока реализовывать её тело. Достаточно договориться, что в функцию передается событие event, которое было получено от среды. Надо теперь объяснить среде, чтобы она "звонила" в эту функцию:
document.addEventListener('keydown', process_game);
Что произошло: у объекта document, являющегося средой, я вызвал функцию addEventListener (что значит "добавить обработчик события") и передал в неё два параметра – строку 'keydown' и название моей функции process_game. Так я сообщил документу:
"document, когда ты там у себя получишь событие с именем 'keydown', то отдай его в мою функцию process_game".
Фактически я попросил документ позвонить мне.
Теперь каждый раз, когда нажимается клавиша, моя функция будет вызываться и ей будет передаваться объект-событие. Мне не нужно её вызывать самому.
Посмотрим, как организовать ожидание ввода в Python.
Я уже использую библиотеку pygame, которая, кроме графических функций, также имеет функции для работы с событиями.
Я могу получить событие из функции:
pygame.event.wait()
Она удобна тем, что сама, внутри себя, организует какой-то цикл ожидания, который не вредит никому, поэтому я могу смело вызвать эту функцию и ждать, когда она вернет мне событие (так как мне в это время всё равно больше нечем заниматься, игра стоит). Условно говоря, я пришел на почту и спросил, есть ли письмо, а они ушли искать. Я стою и жду, но никуда из почты не ухожу.
Но эта функция возвращает любые события (например, движения мыши), а не только нажатия клавиш. Ситуация похожа на то, как если бы я на почте попросил найти письмо, а мне нашли бы письма, открытки, посылки и бандероли, а также предложили купить конверты.
Поэтому события надо фильтровать. Я могу сделать это, проверив тип события:
У объекта event есть атрибут type, который можно сравнить с константой pygame.KEYDOWN, то есть "клавиша нажата". И если они равны, значит меня интересует это событие и я передам его в функцию process_game.
Теперь у меня есть общая точка: и в Javascript, и в Python есть моя (ещё не написанная) функция process_game, в которую передается событие. Значит, начиная с этой функции я могу делать одинаковую реализацию игровой логики, так как владею информацией о событии и уже не нуждаюсь в услугах среды или библиотеки.
Сейчас, без дополнительных уточнений и разбиений, process_game является моей главной функцией: получив управление, она должна сделать всю работу – проверить, что было нажато, передвинуть игрока, передвинуть монстров и завершиться.
Да, именно завершиться. Так проходит один шаг игры. После его завершения я должен принять решение о продолжении и повторить всё сначала, то есть снова получить событие и снова отправить его в process_game.
В JavaScript функция завершается и всё, мне не нужно больше ничего делать. Когда игрок нажмет на клавишу, обработчик этого события в документе-среде автоматически запустит мою функцию process_game, которая осуществит очередной шаг игры. Цикл существует, но неявно. Игрок буквально подталкивает его вручную.
В Python после завершения функции я должен сам перейти к ожиданию события, то есть задать явный цикл:
Цикл будет повторять опрос событий и вызов process_game, пока логическая переменная is_playing равна "истине". Её я и буду использовать для того, чтобы решить, надо продолжать игру или нет.
В следующей части я полностью опишу логику обработки игры, и надеюсь, что в целом уложусь в 10 частей, потому что, во-первых, это красиво.
Читайте дальше: Эпичный провал