Как я написал игру на 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'им дальше
Пиу, пиу, пиу!)