Pull to refresh

Змейка на ПЛК. Наш ответ Сименсу

Reading time 10 min
Views 19K
Здравствуйте.

Недавно мне прислали ссылку на статью, где был показан пример реализации простой и в то же время культовой игры «Змейка» в контроллере семейства Siemens s7-300. И я подумал: все знают о таких монстрах, как Siemens, ABB и т.д. Но современные отечественные разработки остаются в тени.

В этой статье я покажу, как за полчаса реализовать алгоритм игры «Змейка» на российской АСУ ТП «КВИНТ 7», разработанной в НИИТеплоприборе. И для большего интереса игра будет целиком реализована на языке технологического программирования FBD, которому уделяется незаслуженно мало внимания.

Итак, начнем:

Наша задача — написать игру «Змейка» за полчаса.
Для реализации задумки нам понадобятся:
  • Компьютер с установленным ПО КВИНТа 7
  • Контроллер Р-400
  • Патчкорд для связи контроллера с компьютером
  • Лицензия на оперативные средства (для игры) и средства проектирования (чтобы написать саму игру) КВИНТа 7

Небольшое пояснение
Хотя КВИНТ 7 и обладает полной виртуализацией всей АСУ ТП, и сколь угодно сложный проект можно реализовать на одном компьютере, но мы будем делать все по взрослому.


Сам алгоритм игры можно разделить на несколько больших блоков:
  • Генератор (для формирования тактовой частоты)
  • Управление (для того, чтобы можно было играть)
  • Голова змейки (тот элемент, которым мы будем управлять)
  • Хвост змейки (без него игра теряет смысл, ведь цель игры собрать как можно более длинный хвост)
  • Еда (те ячейки, которые змейка будет «есть» и увеличивать длину хвоста)
  • Дополнительные проверки (позволяют сделать игру удобнее и логичнее)


Генератор

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

Принцип действия следующий. На выходе алгоблока "И1 " каждый цикл меняется логическое значение 1<->0.
Дальше на алгоблоке "Слож1" идет подсчет количества единиц, которое сравнивается с заданной нами величиной (в данном примере 50 т.к. время цикла стоит 10 мсек, то за 1 секунду счетчик насчитает 50 включений).
При равенстве показаний счетчика и нашей заданной величины отматываем назад накрученное на счетчике значение (алгоритм "Выч1") и выдаем тактовый импульс длительностью 10 мсек (1 цикл контроллера) с выхода "=" алгоритма "Сравн1". Последний алгоритм выбора служит для обнуления счетчика по сигналу сброса.
Для внимательных читателей
Внимательный читатель наверняка заметил, что при первом включении счетчик отсчитает 50 импульсов не за 1 секунду, а за 980 мсек. потому, что мы ведем подсчет по переднему фронту, а не по заднему. Но при этом все остальные тактовые импульсы он будет выдавать ровно через 1 секунду. Конечно исправить эту ситуацию очень просто, однако я намеренно оставил такую версию реализации генератора именно для этого вопроса.

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


Управление

Тут все предельно просто. У нас уже есть генератор. Змейка непрерывно ползет по полю, меняя лишь направление своего движения. Само поле представляет собой двумерный массив, каждая ячейка которого задается координатами Х и У. При движении вправо координата Х увеличивается на единицу с каждым шагом, а координата У не меняется. При движении влево координата Х уменьшается при постоянной координате У. Для движении вверх-вниз аналогично, только меняться будет координата У, а координата Х будет константой.
Следовательно ставим два реверсивных счетчика (по одному на каждую координату), которые в зависимости от команды одновременно с приходом нового тактового импульса будут либо добавлять, либо отнимать единицу от текущей координаты головы змейки. В итоге получаем следующую схему.

Небольшое пояснение по поводу избыточности схемы
Опять таки внимательные читатели могут задать вопрос — почему данная схема реализована не оптимальным способом, а с задействованием дополнительных ненужных алгоритмов. Все очень просто. Эти, пока бесполезные, алгоритмы включены в схему заранее и понадобятся нам на этапе «причесывания» программы. Чтобы лишний раз не возвращаться и не править исходную конструкцию, лучше ввести их заранее. Для чего они нужны расскажу в спойлере ниже

Принцип действия
Алгоритм "РучСелектор1" служит для ввода команд змейки. Он имеет 4 выхода которые соответствуют четырем направлениям движения (вправо-влево-вверх-вниз). Далее стоит еще один алгоритм селектирования "РучСелектор2". Т.к. команды управления могут поступать в любой момент времени, используем его для синхронизации команд управления с тактовыми импульсами. т.е. будем с первого селектора передавать значение на второй только по приходу тактового импульса. На самом деле очевидно, что данный алгоритм избыточен, т.к. мы проводим синхронизацию с тактовыми импульсами непосредственно на счетчиках (это видно из схемы), однако данный алгоритм был оставлен мной в программе исключительно в целях отладки. Далее стоят 4 алгоритма "И" для синхронизации счетчиков с тактовыми импульсами, о чем я написал выше. По соответствующему сигналу с алгоритмов "И" выбирается увеличение или уменьшение нужной нам координаты на единицу. Т.к. алгоритм селектирования работает по схеме «1 из n», следовательно в каждый момент времени логическая единица будет только на одном из выходов селектора. А это значит, что мы можем просто сложить приращение соответствующей координаты и полученное значение отправить на реверсивный счетчик данной координаты, реализованный как и в случае с генератором на алгоритме суммирования с обратной связью.

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

Голова и хвост змейки

Как говорилось ранее, голова змейки это ячейка массива с определенными координатами Х и У. Но пока у нас этого массива нет. Кроме того, заглянув немного в будущее (а именно в создание хвоста змейки), становится понятно, что эта ячейка массива должна быть с памятью. Самый простой логический элемент с памятью это триггер. Их существует много разновидностей, каждая из которых применяется для своих целей. В данном примере мы отдадим предпочтение «RS-триггеру». Для добавления красоты в игру (например раскрашивать «змейку» и «еду» разными цветами, выделять цветом голову змейки и т.д.) логической ячейки памяти недостаточно, ведь нам придется хранить в ней уже не 2, а как минимум 4 состояния. Т.е. придется делать ячейку памяти либо из нескольких триггеров, либо в другом формате — например хранить в ячейке целое число. Но это несколько усложнит программу, а одна из целей всего этого действа — уложиться в полчаса.
Сделаем для нашей змейки поле 20 на 20. Т.е. нам понадобится 400 триггеров. Проблема решается просто. делаем одну ячейку с одним триггером и сворачиваем ее в макрос. Делаем двадцать макросов ячеек и сворачиваем их в новый, большой макрос. Ставим двадцать больших макросов и получаем 400 ячеек памяти.
Сам макрос ячейки будет иметь 5 входов и 4 выхода.

Входы:
  1. Вход (логический признак, находится ли «голова» змейки в этой ячейке памяти)
  2. Такт (тактовые импульсы для синхронизации с главным генератором)
  3. Длина (длина хвоста змейки)
  4. Сброс (логический признак нажатия на кнопку сброс для запуска новой игры)
  5. Еда (логический признак, находится ли «еда» для змейки в этой ячейки)

Про еду и голову змейки
«Еду» для змейки нужно отделять от «головы» т.к. они по разному проходят проверку на попадание головы змейки в данную клетку. Попадание головы змейки на клетку с «едой» это хорошо, в то время как попадание змейки на клетку с хвостом змейки приводит к поражению и концу игры.

Выходы:
  1. Выход (логический признак, закрашивать эту ячейку на поле или нет)
  2. GameOver (логический признак конца игры если змейка наступила себе на хвост)
  3. Ням_ням (логический признак того, что змейка наступила на ячейку с едой и нужно генерировать новую еду для змейки)
  4. Еще_попытка (логический признак того, что выброшенная случайным образом ячейка с едой попала на поле, уже занятое змейкой и нужно заново генерировать координаты новой случайной клетки)

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

Принцип работы алгоритма ячейки
На вход поступает сигнал, что змея наступила на эту ячейку. Их него выделяется импульс длиной 10 мсек. и по алгоритму "И" складывается с тактовым импульсом. Если оба условия выполнены, т.е. в одном цикле пришел тактовый импульс и сигнал о том, что змея появилась в этой ячейке, то триггер "RS1" взводится. Это главный триггер этой ячейки памяти. В принципе для обработки головы змеи достаточно сбрасывать триггер следующим тактовым сигналом. Все остальное, это обработка хвоста, и разных ситуаций.

1. обработка ситуации «змея наступила себе на хвост». тут все просто за исключением маленькой тонкости. Мы складываем по И входной сигнал и состояние триггера. Если оба сигнала равны единице, то пришел голова наступила на клетку, занятую хвостом. Это нарушение правил и мы выдаем сигнал «GameOver» по которому игра заканчивается. Тонкость заключается в том, что сигнал с триггера нужно брать не текущий, а в предыдущем цикле. Для этого здесь использована обратная связь (отображается пунктирной линией).

2. обработка «еды». тут тоже все просто. Получаем сигнал о том, что в этой клетке должна находится еда. Складываем его по И с главным триггером. Если оба сигнала единицы, значит клетка еды выпала на поле, уже занятое змеей. В этом случае выдаем на выход сигнал «Еще_попытка», который дает команду генерировать новые случайные координаты.

3. обработка ситуации «змей съела еду». Если условия 2 у нас не выполнились, т.е. клетка с едой попала на свободное поле, то взводится триггер еды "RS2". Далее мы просто ждем сигнала о том, что змея наступила на эту клетку. Как только такой сигнал получен — сбрасываем триггер еды (еда съедена) и выдаем сигнал «Ням-ням» по которому увеличиваем длину змеи на единицу и одновременно посылаем команду генерировать новые координаты для еды.

4. «обработка хвоста». Идея заключается в том, чтобы хранить состояние ячейки до тех пор, пока весь хвост змеи не уползет с нее. Для этого ставим счетчик (алгоритм "Слож1") и считаем сколько тактовых импульсов у нас пришло за то время, как главный триггер был взведен. Сравниваем это значение с длиной змеи и как только они становятся равны — сбрасываем триггер. Для простоты и отладки здесь сравнивается не значение счетчика с длиной. А разница между ними с нулем. Один из моментов, на который стоит обратить внимание, это то, что из после вычитания из длины показаний счетчика мы к получившейся разнице добавляем единицу. Это сделано для того, чтобы компенсировать подсчет первого такта, который происходит одновременно с попаданием змеи на клетку.


«Еда»


Здесь самая большая проблема заключается в получении случайного числа. Т.к. команды «random» у нас нет. В интернете есть куча способов генерации псевдослучайного числа. В данном примере реализован один из них. Берется некое большое меняющееся число — UpTime контроллера в секундах. И делится на постоянно меняющийся делитель (реализован на интеграторе). Берется остаток от частного и считается, что это и есть случайное число. На самом деле конечно оно никакое не случайное, и с первого взгляда можно заметить, что вероятность выпадения маленьких чисел больше, чем больших именно за счет «гуляния» делителя в некой области, но если взять допустим не целые от остатка, а скажем сотые или тысячные от остатка. Привести их к диапазону 1-20, то получим число, значение которого уже будет очень похоже на случайную величину.

Соединяем все части и причесываем программу

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

1. Проверка на выход за границы поля.
Тут все зависит от реализации игры. Вспоминаем, что на втором этапе «Управление» у нас был счетчик, в котором находились координаты головы змейки по Х и по У. Просто сравниваем эти координаты с 0 и с 21 (т.к. поле у нас размером 20 на 20 и координатами 1-20). Если любое из условий выполнилось, то выставляем признак «Конец игры» (которые должен складываться по ИЛИ с нашим признаком «GameOver»). Можно сделать как в игре «Pac-man» чтобы при выползании змеи за пределы поля она бы появлялась с другой стороны. Для этого достаточно обнулять соответствующую координату.

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

Принцип действия следующий. Если приходит команда, то проверяем, не является ли она противоположной уже действующей команде. Если нет, то записываем новую команду в память и подаем на выход. Если является, то запись в память не проходит и на выходе остается старая команда. При желании можно этот признак вынести на отдельный выход макроса и по нему формировать звуковой сигнал.

3. Команды «Старт» и «Сброс».
Сделаны для удобства. Команда «Старт» взводит триггер, который подает сигнал на наш генератор. Команда «Сброс» сбрасывает этот триггер, останавливая генератор. Одновременно сброс сбрасывает все ячейки памяти и данные во всех счетчиках.
Итоговая программа. рисунок очень большой


Делаем интерфейс и играем

От отведенного нам времени на игру осталось пара минут, которых как раз хватит чтобы прикрутить графический интерфейс к игре.
Рисуем поле 20х20 из квадратиков. Каждый квадратик соответствует своей ячейке памяти. Задаем в свойствах чтобы при значении 0 соответствующей ячейки памяти квадратик был серый, а при 1 — зеленый (или любые понравившиеся вам цвета).

Поиском картинок в интернете находим красивую рамочку для поля и какой нибудь рисунок змеи, подходящий по смыслу.
Рядом с полем помещаем кнопки управления и привязываем управление мышкой и с клавиатуры клавишами WASD.
Неподалеку размещаем кнопки «Старт» и «Сброс». И большую надпись «GameOver», которая будет появляться при проигрыше.

Компилируем и загружаем программу в контроллер. Запускаем станцию оперативного управления и играем.
Вот такая мнемосхема будет у оператора


Подводим итоги

В данной статье мы немного рассмотрели принципы программирования на языке FBD для контроллеров ПТК КВИНТ 7. Не прилагая больших усилий мы реализовали игру «Змейка». Т.к. язык FBD ориентирован по большей части на технологов, то разобраться в принципах работы и начать писать свои программы может любой человек, знакомый с элементарной логикой (например логикой работы алгоритмов И, ИЛИ, триггеров и счетчиков). При этом не нужно иметь огромный опыт в программировании или схемотехнике.

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

При написании программы на языке FBD важно следить за порядком выполнения алгоритмов. Как я уже писал выше, приведенная программа не оптимальна и снабжена дополнительными алгоритмами, которые нужны только с одной целью — контролировать порядок выполнения и синхронность отработки всех алгоритмов с тактовым сигналом.

Разумеется путем нехитрых манипуляций можно улучшить программу, например увеличивая со временем скорость движения змейки. Или ввести параметр «голод», по которому змейка, не получающая в течение определенного интервала времени не получает новую еду, ускоряется. Кроме того, внимательные читатели могли обратить внимание на функцию запоминания длины змейки при переходе ее в новую клетку (а тут специально заложена небольшая погрешность для привлечения внимания) и подумать, что это неправильно т.к. обычно змейка удлиняется с головы, а не с хвоста. Сделать можно все, что угодно. Это лишь вопрос желания и времени.

Спасибо всем, дочитавшим до конца. Надеюсь это было интересно.
Tags:
Hubs:
+34
Comments 24
Comments Comments 24

Articles