Как я написал игру на bash'е.

Нетерпеливые могут посмотреть\поиграть, скачав игру тут, а пользователи Ubuntu 18.04 могут установить игру apt'ом:
Далее небольшой рассказ о процессе создания и разбор интересных (по моему мнению) мест.
Идея, как водится, пришла внезапно. Как-то раз я «красил» (вставлял управляющие коды в текстовые сообщения для изменения цвета текста/фона) очередной свой сценарий и подумал: «Хм, а ведь из этого может получиться игра!»
Кстати для раскрашивания я пользуюсь вот такой табличкой:
Вставляется в сценарий при помощи source или просто копипастится.
Обратите внимание на этот управляющий код — '\e${Y};${X}H'
Он позволяет перемещать позицию курсора, где Y — строка, X — столбец, собственно на этом и построена игра. Для удобства я завернул его в функцию XY.
Итак, вдохновившись такими консольными утилитами как: sl, cowsay, figlet и т.д. и т.п. я начал придумывать игру. Всегда питал теплые чувства к скроллерам-пулялкам, так что выбор пал именно на этот жанр. Кроме того, хотелось сделать action игру! А не какую-нибудь текстовую адвенчуру (ни чего не имею против текстовых адвенчур). Экшен в консоли, на bash'е? Вызов принят!
Первым делом я нарисовал «спрайт» самолетика (героя):
«Окно» кабины пилота мешало сделать нижнюю часть, тут пригодился эфект подчеркивания.
Заодно добавил цвета:
Для отрисовки я решил использовать массив, разбиваем строки на элементы массива:
И выводим вот такой командой:
Цикл по количеству элементов массива. Каждый элемент рисуется со смещением строки вниз.
Первый и последний элементы массива пустые для того, чтобы затирать остатки самолетика при вертикальном перемещении. А для удаления артефактов при горизонтальном перемещении строки рисуются в обрамлении пробелов — " ${hero[$i]} "
Заставим самолетик летать. Необходимо как-то обработать нажатие кнопок(WASD).
Воспользуемся утилитой — read:
Бесконечный цикл while. В цикле read в переменную input. Параметры read:
-t0.0001 устанавливает таймаут — время ожидания ввода, я установил минимум, чтобы игра не вставала колом
-n1 количество символов, нам нужен только 1
Case проверяет что же прилетело в input и в зависимости от этого двигает самолетик (уменьшает/увеличивает координаты X и Y). Далее уже знакомый цикл for для отрисовки самолетика.
Получилось так:

Отлично, самолетик двигается, но мигает курсор и вылезаеет input.
Отключим курсор и спрячем ввод. Но это «испортит» терминал (с отключенным вводом работать несколько не комфортно), так что надо включить ввод после выхода из игры.
Создадим функцию bye:
Красота, но если самолетик «улетает» за край экрана, происходит факап, функция XY перестает работать.

Необходимо определить размер «экрана» и не пускать самолетик за края. Удобно поместить это в функцию, чтобы значения переменных менялись при растягивании «экрана»:
Модифицируем основной цикл while:
Уже можно играть в двиганье самолетика по экрану. Поиграем немного. Хватит, пора добавить пулялку.
Для этого добавим опрос кнопки «P»(piu), она будет отвечать за выстрел:
Пулек может быть много, тут опять пригодится массив. При нажатии «P» в массив «PIU» добавляется запись "$HY $HX".
Это координаты новой пульки. А чтобы пульки появлялись у самолета из нужного места, а не из хвоста или крыла, эти координаты задаются со смещением от координат самолетика — HX=$(($X + 9)); HY=$(($Y + 3)).
Итак, пулька это запись вида «Y X» в массиве «PIU».
Необходим «спрайт» пульки, я решил сделать пульки не простые, азолотые анимированные.
У пульки будет анимация полета, как будто это мини ракета, получилось так:
И в цвете:
В фунукию get_dimensions добавляем ограничения для пульки:
В основной цикл while помещаем вот такую конструкцию:
Пиу, пиу, пиу!

Но стрелять пока не в кого. Добавим мишени. Мишенями будут извечные враги человечества, засланцы из космоса — инопланетяне в летающих тарелках.
Тарелки я тоже решил сделать анимированными, как будто тарелка крутится. Спрайт:
Эфект кручения достигается анимацией внутренней части летающей тарелки:
В цвете:
Чтобы не делать кучу фреймов летающей тарелки я решил сделать генератор спрайтов:
Добавляем функцию sprites в цикл while, тайминг L подошел тут как нельзя кстати.
Добавляем ограничители для тарелок:
Количество врагов задается переменными: enumber (текущее количество) и enmax (максимальное количество).
Я сразу подумал о том, что буду добавлять еще какие-нибудь объекты кроме летающих тарелок. Поэтому создал массив «OBJ» и начал добавлять туда записи вида «X Y type».
Тарелки появляются у правого края экрана X=$enmyendx, а координата Y задается случайным образом Y=$(( (RANDOM % $enmyendy) + 3 )).
Добавляем в основной цикл while:
Рисуем чужих:
Юху! Эм, пришельцы не умирают…

Реализуем проверку коллизий:
Стрельба по тарелочкам.

Пришло время добавить немного смысла, чужие должны сталкиваться с самолетиком и отнимать жизни у игрока.
Для этого необходимо добавить проверку коллизий с самолетиком, но чтобы избежать копипасты желательно создать функции удаления объектов и пулек:
Перепишем «пульки» и «пришельцы» с использованием функций и добавим проверку колизий с самолетиком:
Добавляем вывод игровой информации и проверку количества жизней:
Игра закочена, дружище!

Основные моменты, из которых строится движуха, разобраны. Дальнейшее повествование пойдет в стиле «как нарисовать сову».
Я уже упоминал, что хотел добавить еще объекты кроме летающих тарелок. В итоге, я добавил бонусы, выпадающие из убитых врагов.
Жизнь — добавляет жизнь, патроны — добавляет патроны (да, патроны заканчиваются) и усилитель ствола — стрелялка х2 и х3, но и расход патронов соответствующий.
А также добавил элементы фона: деревья, облака, солнышко и взрывы уничтоженных врагов.
Деревья и облака разбиты на 3 «битплана» и появляются рандомно. Пришлось полностью переделать цикл объектов, переместив всю логику в функцию mover:
А цикл объектов стал выглядеть так:
Ну и какая же стрелялка без босса? Как только число убитых пришельцев достигнет 100, появится злой папик. С появлением большой тарелки игрок понимает, откуда берутся эти бесконечные пришельцы, вот отсюда! Из этой большой тарелки, надо ее замочить!
Босс:

Вот такая получилась игра. Что хочется сделать еще: попробовать реализовать появление объектов посимвольно.
Тут придется либо отказываться от цвета/эфектов, либо серьезно приседать. Есть мысль добавить в массивы спрайтов дополнительные поля с управляющими кодами для каждого символа.
Возможно, придумаю какую-нибудь сюжетную линию и кто знает, что еще, но это уже совсем другая история.
Продолжение И. BASH'им дальше
Пиу, пиу, пиу!)

Нетерпеливые могут посмотреть\поиграть, скачав игру тут, а пользователи Ubuntu 18.04 могут установить игру apt'ом:
sudo apt install -y piu-piu
Далее небольшой рассказ о процессе создания и разбор интересных (по моему мнению) мест.
гифка с геймплеем

Идея, как водится, пришла внезапно. Как-то раз я «красил» (вставлял управляющие коды в текстовые сообщения для изменения цвета текста/фона) очередной свой сценарий и подумал: «Хм, а ведь из этого может получиться игра!»
Кстати для раскрашивания я пользуюсь вот такой табличкой:
#----------------------------------------------------------------------+ #Color picker, usage: printf ${BLD}${CUR}${RED}${BBLU}"Some text"${DEF}| #---------------------------+--------------------------------+---------+ # Text color | Background color | | #------------+--------------+--------------+-----------------+ | # Base |Lighter\Darker| Base | Lighter\Darker | | #------------+--------------+--------------+-----------------+ | RED='\e[31m'; LRED='\e[91m'; BRED='\e[41m'; BLRED='\e[101m' #| Red | GRN='\e[32m'; LGRN='\e[92m'; BGRN='\e[42m'; BLGRN='\e[102m' #| Green | YLW='\e[33m'; LYLW='\e[93m'; BYLW='\e[43m'; BLYLW='\e[103m' #| Yellow | BLU='\e[34m'; LBLU='\e[94m'; BBLU='\e[44m'; BLBLU='\e[104m' #| Blue | MGN='\e[35m'; LMGN='\e[95m'; BMGN='\e[45m'; BLMGN='\e[105m' #| Magenta | CYN='\e[36m'; LCYN='\e[96m'; BCYN='\e[46m'; BLCYN='\e[106m' #| Cyan | GRY='\e[37m'; DGRY='\e[90m'; BGRY='\e[47m'; BDGRY='\e[100m' #| Gray | #------------------------------------------------------------+---------+ # Effects | #----------------------------------------------------------------------+ DEF='\e[0m' # Default color and effects | BLD='\e[1m' # Bold\brighter | DIM='\e[2m' # Dim\darker | CUR='\e[3m' # Italic font | UND='\e[4m' # Underline | INV='\e[7m' # Inverted | COF='\e[?25l' # Cursor Off | CON='\e[?25h' # Cursor On | #----------------------------------------------------------------------+ # Text positioning, usage: XY 10 10 "Some text" | XY () { printf "\e[${2};${1}H${3}"; } # | #----------------------------------------------------------------------+ # Line, usage: line - 10 | line -= 20 | line "word1 word2 " 20 | line () { printf %.s"${1}" $(seq ${2}); } # | #----------------------------------------------------------------------+
Вставляется в сценарий при помощи source или просто копипастится.
Обратите внимание на этот управляющий код — '\e${Y};${X}H'
Он позволяет перемещать позицию курсора, где Y — строка, X — столбец, собственно на этом и построена игра. Для удобства я завернул его в функцию XY.
Итак, вдохновившись такими консольными утилитами как: sl, cowsay, figlet и т.д. и т.п. я начал придумывать игру. Всегда питал теплые чувства к скроллерам-пулялкам, так что выбор пал именно на этот жанр. Кроме того, хотелось сделать action игру! А не какую-нибудь текстовую адвенчуру (ни чего не имею против текстовых адвенчур). Экшен в консоли, на bash'е? Вызов принят!
Первым делом я нарисовал «спрайт» самолетика (героя):
__ |★〵____ \_| / °)- |/
«Окно» кабины пилота мешало сделать нижнюю часть, тут пригодился эфект подчеркивания.
Заодно добавил цвета:
__ |${RED}★${DEF}〵____ \_| /${UND}${BLD} °${DEF})${DGRY}-${DEF} |/
Для отрисовки я решил использовать массив, разбиваем строки на элементы массива:
hero=(" " "__ " "|${RED}★${DEF}〵____ " " \_| /${UND}${BLD} °${DEF})${DGRY}-${DEF}" " |/ " " ")
И выводим вот такой командой:
for (( i=0; i<${#hero[@]}; i++ )); do XY ${X} $(($Y + $i)) " ${hero[$i]} "; done
Цикл по количеству элементов массива. Каждый элемент рисуется со смещением строки вниз.
Первый и последний элементы массива пустые для того, чтобы затирать остатки самолетика при вертикальном перемещении. А для удаления артефактов при горизонтальном перемещении строки рисуются в обрамлении пробелов — " ${hero[$i]} "
Заставим самолетик летать. Необходимо как-то обработать нажатие кнопок(WASD).
Воспользуемся утилитой — read:
while true; do read -t0.0001 -n1 input; case $input in "w") ((Y--));; "a") ((X--));; "s") ((Y++));; "d") ((X++));; esac for (( i=0; i<${#hero[@]}; i++ )); do XY ${X} $(($Y + $i)) " ${hero[$i]} " done done
Бесконечный цикл while. В цикле read в переменную input. Параметры read:
-t0.0001 устанавливает таймаут — время ожидания ввода, я установил минимум, чтобы игра не вставала колом
-n1 количество символов, нам нужен только 1
Case проверяет что же прилетело в input и в зависимости от этого двигает самолетик (уменьшает/увеличивает координаты X и Y). Далее уже знакомый цикл for для отрисовки самолетика.
Получилось так:

Отлично, самолетик двигается, но мигает курсор и вылезаеет input.
Отключим курсор и спрячем ввод. Но это «испортит» терминал (с отключенным вводом работать несколько не комфортно), так что надо включить ввод после выхода из игры.
Создадим функцию bye:
function bye () { stty echo # включает ввод printf "${CON}${DEF}" # включает курсор, и цвета по умолчанию exit # выход из скрипта } trap bye INT # вызывает функцию bye при нажатии Ctrl+C printf "${COF}" # отключает курсор stty -echo # отключает ввод
Красота, но если самолетик «улетает» за край экрана, происходит факап, функция XY перестает работать.

Необходимо определить размер «экрана» и не пускать самолетик за края. Удобно поместить это в функцию, чтобы значения переменных менялись при растягивании «экрана»:
function get_dimensions { endx=$( tput cols ) # кол-во столбцов(X) endy=$( tput lines ) # кол-во линий(Y) heroendx=$(( $endx - 12 )) # ограничение для самолетика по коорд. X heroendy=$(( $endy - 7 )) # ограничение для самолетика по коорд. Y }
Модифицируем основной цикл while:
while true; do get_dimensions read -t0.0001 -n1 input; case $input in "w") ((Y--)); [ $Y -lt 1 ] && Y=1;; "a") ((X--)); [ $X -lt 1 ] && X=1;; "s") ((Y++)); [ $Y -gt $heroendy ] && Y=$heroendy;; "d") ((X++)); [ $X -gt $heroendx ] && X=$heroendx;; esac for (( i=0; i<${#hero[@]}; i++ )); do XY ${X} $(($Y + $i)) " ${hero[$i]} " done done
Уже можно играть в двиганье самолетика по экрану. Поиграем немного. Хватит, пора добавить пулялку.
Для этого добавим опрос кнопки «P»(piu), она будет отвечать за выстрел:
while true; do HX=$(($X + 9)); HY=$(($Y + 3)) get_dimensions read -t0.0001 -n1 input; case $input in "w") ((Y--)); [ $Y -lt 1 ] && Y=1;; "a") ((X--)); [ $X -lt 1 ] && X=1;; "s") ((Y++)); [ $Y -gt $heroendy ] && Y=$heroendy;; "d") ((X++)); [ $X -gt $heroendx ] && X=$heroendx;; "p") PIU+=("$HY $HX");; esac for (( i=0; i<${#hero[@]}; i++ )); do XY ${X} $(($Y + $i)) " ${hero[$i]} " done done
Пулек может быть много, тут опять пригодится массив. При нажатии «P» в массив «PIU» добавляется запись "$HY $HX".
Это координаты новой пульки. А чтобы пульки появлялись у самолета из нужного места, а не из хвоста или крыла, эти координаты задаются со смещением от координат самолетика — HX=$(($X + 9)); HY=$(($Y + 3)).
Небольшое лирическое отступление
Массивы вообще очень полезы (О_о правда чтоле!?)
Небольшой пример: задача — взять с нескольких разномастных серверов postgresql дампы и скопировать себе.
Параметры подключения к серверам разные, базы называются по разному, дампы хранятся в разных местах…
Придется хардкодить, массив как нельзя лучше подходит для этого. Создаем массив:
Значения выстраиваем столбиками, получается удобная табличка. Удобно добавлять/убавлять строки, можно вставлять комменты.
В переменной N подсчитывается общее количество значений, переменная C — количество столбцов (задается вручную).
Затем в цикле for ((i=0; i<${N}; i+=${C})); do используем вот такую конструкцию:
Можно использовать read:
Но read лажает с пробелами. Итого:
Вуаля. Можно использовать массив для хардкодного парсинга, не прибегая к услугам grep, sed, awk etc:
И т.д. и т.п., но вернемся к нашим пулькам.
Небольшой пример: задача — взять с нескольких разномастных серверов postgresql дампы и скопировать себе.
Параметры подключения к серверам разные, базы называются по разному, дампы хранятся в разных местах…
Придется хардкодить, массив как нельзя лучше подходит для этого. Создаем массив:
dbases=( #-------------------------------+---------------+------------+---------+ # Ssh address | Dump folder | DB name | New db | #-------------------------------+---------------+------------+---------+ '-p123 user@192.168.0.1' '/backup' 'test_db' 'db1' '-p321 looser@127.1' '/tmp' 'main_db' 'db2' 'someserver' '/tmp/backup' 'a' 'db3' ); N=${#dbases[*]}; C=4
Значения выстраиваем столбиками, получается удобная табличка. Удобно добавлять/убавлять строки, можно вставлять комменты.
В переменной N подсчитывается общее количество значений, переменная C — количество столбцов (задается вручную).
Затем в цикле for ((i=0; i<${N}; i+=${C})); do используем вот такую конструкцию:
tmp=("${dbases[@]:$i:$C}") srvadr="${tmp[0]}" bkpath="${tmp[1]}" dbname="${tmp[2]}" dbtest="${tmp[3]}"
Можно использовать read:
read srvadr bkpath dbname dbtest <<< "${dbases[@]:$i:$C}"
Но read лажает с пробелами. Итого:
#!/bin/bash dbases=( #-------------------------------+---------------+------------+---------+ # Ssh address | Dump folder | DB name | New db | #-------------------------------+---------------+------------+---------+ '-p123 user@192.168.0.1' '/backup' 'test_db' 'db1' '-p321 looser@127.1' '/tmp' 'main_db' 'db2' 'someserver' '/tmp/backup' 'a' 'db3' ); N=${#dbases[*]}; C=4 for ((i=0; i<${N}; i+=${C})); do tmp=("${dbases[@]:$i:$C}") srvadr="${tmp[0]}" bkpath="${tmp[1]}" dbname="${tmp[2]}" dbtest="${tmp[3]}" # copy dump scp ${srvadr}${bkpath}/${dbname}.gz . # test dump gunzip -c ${dbname}.gz | psql -v ON_ERROR_STOP=1 ${dbtest} \ || { printf "\nDB error!"; continue; } done
Вуаля. Можно использовать массив для хардкодного парсинга, не прибегая к услугам grep, sed, awk etc:
df=($(df -h)); echo ${df[5]}
И т.д. и т.п., но вернемся к нашим пулькам.
Итак, пулька это запись вида «Y X» в массиве «PIU».
Необходим «спрайт» пульки, я решил сделать пульки не простые, а
У пульки будет анимация полета, как будто это мини ракета, получилось так:
shoot=( " ->" "-=>" "=->" "- >")
И в цвете:
shoot=( "${RED} -${DEF}${BLD}${GRN}>${DEF}" "${BLD}${LRED}-=${DEF}${GRN}>${DEF}" "${LRED}=-${DEF}${BLD}${GRN}>${DEF}" "${RED}- ${DEF}${GRN}>${DEF}")
В фунукию get_dimensions добавляем ограничения для пульки:
function get_dimensions { endx=$( tput cols ) # кол-во столбцов(X) endy=$( tput lines ) # кол-во линий(Y) heroendx=$(( $endx - 12 )) # ограничение для самолетика по коорд. X heroendy=$(( $endy - 7 )) # ограничение для самолетика по коорд. Y bullendx=$(( $endx - 4 )) # ограничение для пульки по коорд. X }
В основной цикл while помещаем вот такую конструкцию:
#-------------------------------{ Пульки }--------------------------------------- # переключатель спрайтов, почему L? исторически так сложилось ((L++)); [ $L -gt 3 ] && L=0 NP=${#PIU[@]} # вычисляем кол-во пулек # циклом перебираем пульки for (( t=0; t<${NP}; t++ )); do # преобразуем запись вида "Y X" в отдельные переменные # т.к. X должна изменяться(пулька летит) PI=(${PIU[$t]}); PY=${PI[0]}; PX=${PI[1]} # увеличиваем X координату пульки ((PX++)) [ $PX -ge $bullendx ] && { # если пулька долетела до края экрана: XY ${PX} ${PY} " " # удаляем пульку(затираем пробелами) unset PIU[$t] # удаляем пульку из массива PIU=("${PIU[@]}") # массив необходимо переопределить # т.к. unset не меняет индексы ((NP--)) # уменьшаем кол-во пулек continue # переходим к след. пульке # если нет, записываем новые координаты пульки в массив "PIU" } || { PIU[$t]="$PY $PX"; } # рисуем пульку, пробел в начале затирает остатки старой пульки XY ${PX} ${PY} " ${shoot[$L]}" done
Пиу, пиу, пиу!

Но стрелять пока не в кого. Добавим мишени. Мишенями будут извечные враги человечества, засланцы из космоса — инопланетяне в летающих тарелках.
Тарелки я тоже решил сделать анимированными, как будто тарелка крутится. Спрайт:
alien=( " ___ " "( o ) " ' `¯´ ')
Эфект кручения достигается анимацией внутренней части летающей тарелки:
small=( 'o ' ' ' ' o' ' o ')
В цвете:
small=( ${YLW}'o '${DEF} ${YLW}' '${DEF} ${YLW}' o'${DEF} ${YLW}' o '${DEF})
Чтобы не делать кучу фреймов летающей тарелки я решил сделать генератор спрайтов:
function sprites { # добавил тут эфекты чтобы в верхней части тарелки появилась "пупочка" alien=( " _${UND}${BLD}_${DEF}_ " # каждый раз сюда вставляется следующий элемент массива small "(${small[$L]}${DEF}) " ' `¯´ ') }
Добавляем функцию sprites в цикл while, тайминг L подошел тут как нельзя кстати.
Добавляем ограничители для тарелок:
enmyendx=$(( $endx - 5 )) enmyendy=$(( $endy - 7 ))
Количество врагов задается переменными: enumber (текущее количество) и enmax (максимальное количество).
Я сразу подумал о том, что буду добавлять еще какие-нибудь объекты кроме летающих тарелок. Поэтому создал массив «OBJ» и начал добавлять туда записи вида «X Y type».
Тарелки появляются у правого края экрана X=$enmyendx, а координата Y задается случайным образом Y=$(( (RANDOM % $enmyendy) + 3 )).
Добавляем в основной цикл while:
#-----------------------------{ Пришельцы }-------------------------------------- [ $enumber -lt $enmax ] && { OBJ+=("$enmyendx $(( (RANDOM % $enmyendy) + 3 )) alien") ((enumber++)) }
Рисуем чужих:
NO=${#OBJ[@]} # вычисляем кол-во объектов er=${#alien[@]} # вычисляем кол-во элементов спрайта # циклом перебираем объекты for (( i=0; i<$NO; i++ )); do # преобразуем запись вида "X Y type" в отдельные переменные OI=(${OBJ[$i]}); OX=${OI[0]}; OY=${OI[1]}; type=${OI[2]} # уменьшаем X координату(пришельцы летят к самолетику) ((OX--)) # если пришелец долетел до края экрана: [ $OX -lt 1 ] && { # удаляем пришельца, затирая пробелами # тут нужен цикл т.к. спрайт занимает несколько строк for (( k=0; k<$er; k++ )); do XY ${OX} $(($OY + $k)) " " done unset OBJ[$i] # удаляем пришельца из массива OBJ=("${OBJ[@]}") # массив необходимо переопределить # т.к. unset не меняет индексы ((NO--)) # уменьшаем кол-во объектов ((enumber--)) # уменьшаем текущее кол-во пришельцев continue # переходим к след. объекту # если нет, записываем новые координаты в массив "OBJ" } || { OBJ[$i]="$OX $OY $type"; } # рисуем пришельца, также циклом for (( p=0; p<${er}; p++ )); do XY ${OX} $(($OY + $p)) "${alien[$p]}" done done
Юху! Эм, пришельцы не умирают…

Реализуем проверку коллизий:
# проверка коллизий for (( p=0; p<${er}; p++ )); do # цикл по елементам спрайта for (( t=0; t<${NP}; t++ )); do # вложенный цикл по пулькам # преобразуем запись вида "Y X" в отдельные переменные # т.к. X должна изменяться(пулька летит) PI=(${PIU[$t]}); PY=${PI[0]}; PX=${PI[1]} # проверка условия попадания делается при помощи оператора # <i>case</i> а не <i>test([])</i> т.к. <i>case</i> # работает гораздо быстрей, скорость тут очень важна # задаем смещение координат, обозначающее попадание case "$(($OY + 1)) $(($OX + $p))" in # смещение координат должно совпасть с координатами пули "${PIU[$t]}") # удаляем пришельца, затирая пробелами # тут нужен цикл т.к. спрайт занимает несколько строк for (( k=0; k<$er; k++ )); do XY ${OX} $(($OY + $k)) " " done unset OBJ[$i] # удаляем пришельца из массива OBJ=("${OBJ[@]}") # переопределяем массив ((NO--)) # уменьшаем кол-во объектов ((enumber--)) # уменьшаем текущее кол-во # пришельцев # удаляем пульку(затираем пробелами) XY ${PX} ${PY} " " unset PIU[$t] # удаляем пульку из массива PIU=("${PIU[@]}") # переопределяем массив ((NP--)) # уменьшаем кол-во пулек break # прерываем цикл ;; esac done done
Стрельба по тарелочкам.

Пришло время добавить немного смысла, чужие должны сталкиваться с самолетиком и отнимать жизни у игрока.
Для этого необходимо добавить проверку коллизий с самолетиком, но чтобы избежать копипасты желательно создать функции удаления объектов и пулек:
frags=0 # очки life=3 # жизни function remove_obj () { for (( k=0; k<$er; k++ )); do XY ${OX} $(($OY + $k)) " " done unset OBJ[$1]; OBJ=("${OBJ[@]}"); ((NO--)) } function remove_piu () { XY ${PX} ${PY} " " unset PIU[$1]; PIU=("${PIU[@]}"); ((NP--)) }
Перепишем «пульки» и «пришельцы» с использованием функций и добавим проверку колизий с самолетиком:
#-------------------------------{ Пульки }--------------------------------------- # переключатель спрайтов, почему L? исторически так сложилось ((L++)); [ $L -gt 3 ] && L=0 NP=${#PIU[@]} # вычисляем кол-во пулек # циклом перебираем пульки for (( t=0; t<${NP}; t++ )); do # преобразуем запись вида "Y X" в отдельные переменные # т.к. X должна изменяться(пулька летит) PI=(${PIU[$t]}); PY=${PI[0]}; PX=${PI[1]} # увеличиваем X координату пульки ((PX++)) [ $PX -ge $bullendx ] && { # если пулька долетела до края экрана: remove_piu ${t} # удаляем пульку функцией, передав ей # индекс пульки в массиве "PIU" continue # переходим к след. пульке # если нет, записываем новые координаты пульки в массив "PIU" } || { PIU[$t]="$PY $PX"; } # рисуем пульку, пробел в начале затирает остатки старой пульки XY ${PX} ${PY} " ${shoot[$L]}" done #------------------------------{ Пришельцы }------------------------------------- [ $enumber -lt $enmax ] && { OBJ+=("$enmyendx $(( (RANDOM % $enmyendy) + 3 )) alien") ((enumber++)) } NO=${#OBJ[@]} # вычисляем кол-во объектов er=${#alien[@]} # вычисляем кол-во элементов спрайта # циклом перебираем объекты for (( i=0; i<$NO; i++ )); do # преобразуем запись вида "X Y type" в отдельные переменные OI=(${OBJ[$i]}); OX=${OI[0]}; OY=${OI[1]}; type=${OI[2]} # уменьшаем X координату(пришельцы летят к самолетику) ((OX--)) [ $OX -lt 1 ] && { # если пришелец долетел до края экрана: remove_obj ${i} # удаляем пришельца, функцией, передав ей # индекс объекта в массиве "OBJ" ((enumber--)) # уменьшаем текущее кол-во пришельцев continue # переходим к след. объекту # если нет, записываем новые координаты в массив "OBJ" } || { OBJ[$i]="$OX $OY $type"; } # рисуем пришельца, также циклом for (( p=0; p<${er}; p++ )); do XY ${OX} $(($OY + $p)) "${alien[$p]}" done # проверка коллизий for (( p=0; p<${er}; p++ )); do # цикл по елементам спрайта for (( t=0; t<${NP}; t++ )); do # вложенный цикл по пулькам # преобразуем запись вида "Y X" в отдельные переменные # т.к. X должна изменяться(пулька летит) PI=(${PIU[$t]}); PY=${PI[0]}; PX=${PI[1]} # проверка условия попадания делается при помощи # <i>case</i> а не <i>test([])</i> т.к. <i>case</i> # работает гораздо быстрей, скорость тут очень важна # задаем смещение координат, обозначающее попадание case "$(($OY + 1)) $(($OX + $p))" in # смещение координат должно совпасть # с координатами пульки "${PIU[$t]}") remove_obj ${i} # удаляем пришельца, функцией, # передав ей индекс объекта # в массиве "OBJ" ((enumber--)) # уменьшаем текущее кол-во # пришельцев ((frags++)) # увеличиваем очки remove_piu ${t} # удаляем пульку функцией, # передав ей индекс пульки # в массиве "PIU" break # прерываем цикл ;; esac done # задаем смещение координат, обозначающее # столкновение с самолетиком case "$(($OY + 1)) $(($OX + $p))" in # смещение координат должно совпасть # с координатами самолетика "$HY $HX") remove_obj ${i} # удаляем пришельца, функцией, передав ей # индекс объекта в массиве "OBJ" ((enumber--)) # уменьшаем текущее кол-во пришельцев ((life--)) # уменьшаем жизни игрока ((frags++)) # увеличиваем очки break # прерываем цикл ;; esac done done
Добавляем вывод игровой информации и проверку количества жизней:
#----------------------------{ Игровая информация }------------------------------ XY 0 0 "${BLD}killed aliens: ${DEF}${CYN}${frags}${DEF} ${BLD}Life: ${DEF}${CYN}${life}${DEF} " [ $life -le 0 ] && { clear; echo 'Game over man!'; bye; }
Игра закочена, дружище!

промежуточный итог
#!/bin/bash #----------------------------------------------------------------------+ #Color picker, usage: printf ${BLD}${CUR}${RED}${BBLU}"Some text"${DEF}| #---------------------------+--------------------------------+---------+ # Text color | Background color | | #------------+--------------+--------------+-----------------+ | # Base |Lighter\Darker| Base | Lighter\Darker | | #------------+--------------+--------------+-----------------+ | RED='\e[31m'; LRED='\e[91m'; BRED='\e[41m'; BLRED='\e[101m' #| Red | GRN='\e[32m'; LGRN='\e[92m'; BGRN='\e[42m'; BLGRN='\e[102m' #| Green | YLW='\e[33m'; LYLW='\e[93m'; BYLW='\e[43m'; BLYLW='\e[103m' #| Yellow | BLU='\e[34m'; LBLU='\e[94m'; BBLU='\e[44m'; BLBLU='\e[104m' #| Blue | MGN='\e[35m'; LMGN='\e[95m'; BMGN='\e[45m'; BLMGN='\e[105m' #| Magenta | CYN='\e[36m'; LCYN='\e[96m'; BCYN='\e[46m'; BLCYN='\e[106m' #| Cyan | GRY='\e[37m'; DGRY='\e[90m'; BGRY='\e[47m'; BDGRY='\e[100m' #| Gray | #------------------------------------------------------------+---------+ # Effects | #----------------------------------------------------------------------+ DEF='\e[0m' # Default color and effects | BLD='\e[1m' # Bold\brighter | DIM='\e[2m' # Dim\darker | CUR='\e[3m' # Italic font | UND='\e[4m' # Underline | INV='\e[7m' # Inverted | COF='\e[?25l' # Cursor Off | CON='\e[?25h' # Cursor On | #----------------------------------------------------------------------+ # Text positioning, usage: XY 10 10 "Some text" | XY () { printf "\e[${2};${1}H${3}"; } # | #----------------------------------------------------------------------+ # Line, usage: line - 10 | line -= 20 | line "word1 word2 " 20 | line () { printf %.s"${1}" $(seq ${2}); } # | #----------------------------------------------------------------------+ small=( ${YLW}'o '${DEF} ${YLW}' '${DEF} ${YLW}' o'${DEF} ${YLW}' o '${DEF}) hero=(" " "__ " "|${RED}★${DEF}〵____ " " \_| /${UND}${BLD} °${DEF})${DGRY}-${DEF}" " |/ " " ") shoot=( "${RED} -${DEF}${BLD}${GRN}>${DEF}" "${BLD}${LRED}-=${DEF}${GRN}>${DEF}" "${LRED}=-${DEF}${BLD}${GRN}>${DEF}" "${RED}- ${DEF}${GRN}>${DEF}") X=1; Y=1 # начальные координаты самолетика enumber=0 # текущее количество врагов enmax=10 # максимальное количество врагов frags=0 # очки life=3 # жизни #-----------------------------{ функции }------------------------------------- function sprites { alien=( " _${UND}${BLD}_${DEF}_ " "(${small[$L]}${DEF}) " ' `¯´ ') } function remove_obj () { for (( k=0; k<$er; k++ )); do XY ${OX} $(($OY + $k)) " " done unset OBJ[$1]; OBJ=("${OBJ[@]}"); ((NO--)) } function remove_piu () { XY ${PX} ${PY} " " unset PIU[$1]; PIU=("${PIU[@]}"); ((NP--)) } function bye () { stty echo printf "${CON}${DEF}" exit } function get_dimensions { endx=$( tput cols ) endy=$( tput lines ) heroendx=$(( $endx - 12 )) heroendy=$(( $endy - 7 )) bullendx=$(( $endx - 4 )) enmyendx=$(( $endx - 5 )) enmyendy=$(( $endy - 7 )) } #----------------------------------------------------------------------------- trap bye INT printf "${COF}" stty -echo clear #---------------------------{ основной цикл }--------------------------------- while true; do HX=$(($X + 9)); HY=$(($Y + 3)) get_dimensions; sprites read -t0.0001 -n1 input; case $input in "w") ((Y--)); [ $Y -lt 1 ] && Y=1;; "a") ((X--)); [ $X -lt 1 ] && X=1;; "s") ((Y++)); [ $Y -gt $heroendy ] && Y=$heroendy;; "d") ((X++)); [ $X -gt $heroendx ] && X=$heroendx;; "p") PIU+=("$HY $HX");; esac for (( i=0; i<${#hero[@]}; i++ )); do XY ${X} $(($Y + $i)) " ${hero[$i]} " done #--------------------------{ Пульки }--------------------------------- ((L++)); [ $L -gt 3 ] && L=0 NP=${#PIU[@]} # перебираем пульки for (( t=0; t<${NP}; t++ )); do PI=(${PIU[$t]}); PY=${PI[0]}; PX=${PI[1]}; ((PX++)) [ $PX -ge $bullendx ] && { remove_piu ${t} continue } || { PIU[$t]="$PY $PX"; } XY ${PX} ${PY} " ${shoot[$L]}" done #-------------------------{ Пришельцы }------------------------------- [ $enumber -lt $enmax ] && { OBJ+=("$enmyendx $(( (RANDOM % $enmyendy) + 3 )) alien") ((enumber++)) } NO=${#OBJ[@]} # вычисляем кол-во объектов er=${#alien[@]} # вычисляем кол-во элементов спрайта # перебираем объекты for (( i=0; i<$NO; i++ )); do OI=(${OBJ[$i]}); OX=${OI[0]}; OY=${OI[1]}; ((OX--)) [ $OX -lt 1 ] && { remove_obj ${i} ((enumber--)) continue } || { OBJ[$i]="$OX $OY $type"; } # рисуем пришельца for (( p=0; p<${er}; p++ )); do XY ${OX} $(($OY + $p)) "${alien[$p]}" done # проверка коллизий for (( p=0; p<${er}; p++ )); do for (( t=0; t<${NP}; t++ )); do PI=(${PIU[$t]}); PY=${PI[0]}; PX=${PI[1]} case "$(($OY + 1)) $(($OX + $p))" in "${PIU[$t]}") remove_obj ${i} ((enumber--)) ((frags++)) remove_piu ${t} break ;; esac done # столкновение с самолетиком case "$(($OY + 1)) $(($OX + $p))" in "$HY $HX") remove_obj ${i} ((enumber--)) ((life--)) ((frags++)) break ;; esac done done #-----------------------{ Игровая информация }------------------------ XY 0 0 "${BLD}killed aliens: ${DEF}${CYN}${frags}${DEF} ${BLD}Life: ${DEF}${CYN}${life}${DEF} " [ $life -le 0 ] && { clear; echo 'Game over man!'; bye; } done
Основные моменты, из которых строится движуха, разобраны. Дальнейшее повествование пойдет в стиле «как нарисовать сову».
Я уже упоминал, что хотел добавить еще объекты кроме летающих тарелок. В итоге, я добавил бонусы, выпадающие из убитых врагов.
Жизнь — добавляет жизнь, патроны — добавляет патроны (да, патроны заканчиваются) и усилитель ствола — стрелялка х2 и х3, но и расход патронов соответствующий.
А также добавил элементы фона: деревья, облака, солнышко и взрывы уничтоженных врагов.
Деревья и облака разбиты на 3 «битплана» и появляются рандомно. Пришлось полностью переделать цикл объектов, переместив всю логику в функцию mover:
function mover () { er=${#sprite[@]} # двигайся [ ${1} = 0 ] && { ((OX--)) [ $OX -lt 0 ] && OX=0 OBJ[$i]="$OX $OY $type" } # край экрана [ $OX -lt 1 ] && { remove_obj ${i} case ${type} in "alien") ((enumber--));; esac continue } # совместил рисование и проверку коллизий в одном цикле for (( p=0; p<${er}; p++ )); do # рисуем XY ${OX} $(($OY + $p)) "${sprite[$p]}" # проверяем коллизии case ${type} in "life" ) # подобрал жизнь case "$(($OY + $p)) $OX" in "$HY $HX") ((life++)) remove_obj ${i} break;; esac;; "ammo" ) # подобрал пули case "$(($OY + $p)) $OX" in "$HY $HX") ((ammo+=100)) remove_obj ${i} break;; esac;; "gunup" ) # подобрал усилитель ствола case "$(($OY + $p)) $OX" in "$HY $HX") ((G++)) remove_obj ${i} break;; esac;; "bfire" ) # прилетело от босса case "$OY $OX" in "$HY $HX") ((life--)) remove_obj ${i} break;; esac;; "alien" ) for (( t=0; t<${NP}; t++ )); do # в чужого попала пуля case "$(($OY + 1)) $(($OX + $p))" in "${PIU[$t]}") # даст или не даст бонус [ $((RANDOM % $rnd)) -eq 0 ] && { OBJ+=("$OX $OY \ ${bonuses[$((RANDOM % ${#bonuses[@]}))]}") } ((frags++)) ((enumber--)) remove_obj ${i} remove_piu ${t} # добавляем взрыв OBJ+=("${OX} ${OY} boom") break;; esac done # столкнулся с самолетом case "$(($OY + 1)) $(($OX + $p))" in "$HY $HX") ((life--)) ((frags++)) ((enumber--)) remove_obj ${i} OBJ+=("${OX} ${OY} boom") break;; esac;; esac done }
А цикл объектов стал выглядеть так:
# двигаем\проверяем\рисуем все объекты, летящие к игроку |------------------ NO=${#OBJ[@]} for (( i=0; i<$NO; i++ )); do OI=(${OBJ[$i]}); OX=${OI[0]}; OY=${OI[1]}; type=${OI[2]} case $type in # взрывы не летают просто рисуем 1 раз "boom" ) er=${boomC} for part in "${boom[@]:$B:$boomC}"; do XY ${OX} ${OY} " ${part}" ((OY++)) done [ ${E} = 0 ] && { ((B+=${boomC})) [ $B -gt ${boomN} ] && { B=0 remove_obj ${i} } };; # копируем нужный спрайт в массив sprite # и выполняем mover передав ему тайминг # за счет таймингов работают битпланы фоновых # объектов(деревья и облака) "alien" ) sprite=("${alien[@]}"); mover 0;; "bfire" ) sprite=("${bfire[@]}"); mover 0;; "ammo" ) sprite=("${ammob[@]}"); mover 0;; "life" ) sprite=("${lifep[@]}"); mover 0;; "gunup" ) sprite=("${gunup[@]}"); mover 0;; "tree1" ) sprite=("${tree1[@]}"); mover ${Q};; "tree2" ) sprite=("${tree2[@]}"); mover ${W};; "tree3" ) sprite=("${tree3[@]}"); mover ${E};; "cloud1") sprite=("${cloud1[@]}"); mover ${Q};; "cloud2") sprite=("${cloud2[@]}"); mover ${W};; "cloud3") sprite=("${cloud3[@]}"); mover ${E};; esac done
Ну и какая же стрелялка без босса? Как только число убитых пришельцев достигнет 100, появится злой папик. С появлением большой тарелки игрок понимает, откуда берутся эти бесконечные пришельцы, вот отсюда! Из этой большой тарелки, надо ее замочить!
Босс:
# БОСС |----------------------------------------------------------------- if [ $frags -ge $tillboss ]; then # шкала жизни босса bar=; hp=$(( $bosshbar * $bhealth / 100 )); hm=$(( $endx - 10 )) for (( i=0 ; i<${hp}; i++ )); do bar="▒${bar}"; done for (( i=$hp; i<${hm}; i++ )); do bar="${bar} "; done XY 1 $(($endy - 1)) " ${BLD}BOSS: |${RED}${bar}${DEF}${BLD}|${DEF}" # двигай [ $BY -lt $Y ] && { ((BY++)) } [ $BY -gt $Y ] && { ((BY--)) } [ $BX -gt $(($endx / 2)) -a "$goback" == "false" ] && { ((BX--)) } || goback=true [ $BX -lt $bossendx -a "$goback" == "true" ] { && ((BX++)) } || goback=false # рисуй for (( i=0; i<${#boss[@]}; i++ )); do XY ${BX} $(($BY + $i)) " ${boss[$i]} " done # стреляй [ $BY -eq $Y -a $K -eq 0 ] && { OBJ+=("$(($BX - 4)) $(($BY + 3)) bfire") } # мелкие вылетают из босса [ $enumber -lt $enmax ] && { ((enumber++)) OBJ+=("$(($BX + 2)) $(($BY + 3)) alien") } else [ $enumber -lt $enmax ] && { ((enumber++)) OBJ+=("$enmyendx $(( (RANDOM % $enmyendy) + 3 )) alien") } fi

Вот такая получилась игра. Что хочется сделать еще: попробовать реализовать появление объектов посимвольно.
Тут придется либо отказываться от цвета/эфектов, либо серьезно приседать. Есть мысль добавить в массивы спрайтов дополнительные поля с управляющими кодами для каждого символа.
Возможно, придумаю какую-нибудь сюжетную линию и кто знает, что еще, но это уже совсем другая история.
Продолжение И. BASH'им дальше
Пиу, пиу, пиу!)
