Игра на bash'е с поддержкой мультиплеера, миф или реальность?
![image](https://habrastorage.org/r/w1560/getpro/habr/post_images/4ed/c0c/1fe/4edc0c1fe18d8841e72feb454090673e.png)
Истина где-то тут. Разоблачительный текст далее.
Первая статья И. BASH'им в начало
Вторая статья И. BASH'им дальше
Реализация мультиплеера не давала мне покоя. Но я понимал что игра будет тормозить в коопе. Поэтому предстояла большая работа по увеличению производительности. Я повертел спрайты и так и эдак и подумал: «А что если координаты спрайта (управляющие символы разметки \e[${Y};${X}H) разместить непосредственно в спрайте? И выводить вразу весь спрайт целиком одной командой, а не по кусочкам в цикле». Все спрайты пришлось переделать) Теперь спрайт — это спрайт (О_о) и функция вида (на примере чужого):
Функция sprite_alien задает переменные: hight — высота спрайта (количество линий), width — ширина спрайта (количество символов в самом широком элементе спрайта) и массив color — посимвольная раскраска, необходимые для посимвольного вывода. В массиве sprite генерируется спрайт для «быстрого» режима, вставляются управляющие символы координат и цветов. Массив target задает координаты коллизий данного спрайта (отсутствует у объектов фона). Sprite2 необходим для «медленного» посимвольного вывода, который реализован функциями:
Символ «Z» играет роль маски, теперь спрайты могут быть с «дырками» внутри. На выходе у этих функций получается массив sprite. Объекты обрабатываются и рисуются по-прежнему функцией mover которая, однако, претерпела некоторые изменения:
Обработка коллизий новым методом тоже положительно сказалась на производительности. В основном цикле обработка объектов выглядит так:
Что же получилось выжать в итоге? Для сравнения, старый метод:
![image](https://habrastorage.org/getpro/habr/post_images/ce4/469/923/ce4469923d1b2bde9df7249bd300352a.gif)
Новый метод:
![image](https://habrastorage.org/getpro/habr/post_images/12f/fef/cc3/12ffefcc37f315f3b7b2b1edca0b6241.gif)
Двойное увеличение фпс, недурно. Кстати, для замера используется функция fps_counter которая вначале выглядела так:
Использовался date, но такой метод замера производительности заметно эту самую производительность уменьшал. Мне подсказали другой вариант, использовать printf:
Получилось гораздо приятней. Спасибо, Александр! Прирост производительности я решил использовать для майнинга анонимной криптовалюты Monero и сделал соответствующую закладку в игру. Посмотрим как изменился фпс:
![image](https://habrastorage.org/getpro/habr/post_images/ce4/469/923/ce4469923d1b2bde9df7249bd300352a.gif)
На уровне старого метода как будто ничего не изменилось, отлично, никто ничего не заметит!
Это шутка, конечно, хотя тренд такой намечается, будьте бдительны. Не остановливаясь на достигнутом, стал я думать и гадать как же еще выше фпс поднять. Я развил первую идею и решил не выводить спрайты по отдельности, а фигачить их в массив screen, а затем рисовать сразу ВСЕ одной командой! Переделывать много не пришлось, в функции mover вывод через printf
заменил на апенд массива screen
Остальные рисовалки в цикле также переключил на screen а в конце цикла добавил
Ожидая 10-ти кратного прироста производительности, я поскорей запустил новый вариант:
![image](https://habrastorage.org/getpro/habr/post_images/d22/178/2c0/d221782c06202c996ff3f7fafc16b636.gif)
Но прирост оказался не таким умопомрачительным, как я ожидал (майнинг то я забыл отключить)), однако, этот скромный шаг в деле увеличения производительности стал огромным скачком на пути к мультиплееру. Этот метод оказался крайне удобным для отрисовки картинки у клиента, клиент получает сразу готовый кадр с сервера и рисует его! Мультиплеер практически готов, практически.
Дополнительные фпсы пошли в дело. Новый движок позволил увеличить количество объектов фона. Деревьев стало больше, а облака осенью закрывают все небо и солнца практически не видно (как в реале).
![image](https://habrastorage.org/getpro/habr/post_images/896/0ae/ae6/8960aeae63e7d84b9e383208143f9df3.gif)
Пора заняться мультиплеером. Что нужно для мультиплеера? Нужно передавать данные между компьютерами участвующими в игре. Какие данные? Можно передавать все изменения туда-сюда, но возникает куча проблем синхронизации всех объектов на клиенте и сервере. Поэтому пересылать нужно только необходимый минимум, выполнять все расчеты на сервере, отдавая клиету готовый результат. Я тут не изобрел велосипед, идея позаимствована из современных игр, большинство из которых работает именно так. У меня клиент отправляет серверу свой адрес и порт, чтобы сервер знал кому отвечать. Параметры конфигурации: символ самолетика и цвет самолетика\символа. Обрабатывает нажатия кнопок WASDP и отправляет координаты самолетика, а также факт нажатия на гашетку. Вот строка клиента:
Сервер же на основе этой информации добавляет в игру второй самолетик, выполняет все расчеты, рисует картинку и передает готовую картинку клиенту. Таким образом, на обоих терминалах мы получаем одинаковую картинку.
Как передавать? Баш не умеет слушать порт (или я не умею делать это на баше), поэтому пришлось воспользоваться утилитой netcat впростонародье nc.
Вот как выглядит клиент:
Да, это все) Функция client_read это опрос кнопок:
Для чего понадобилось выносить read в отдельную функцию и выполнять несколько раз? Опрос выполняется с задержкой 0.0001 секунды, правда, для кооп режима пришлось увеличить время ожидания до 0.001, т.е. read выполняется с параметром -t0.001, передача данных на сервер же происходит значительно дольше. Клиент не знает, открыт на сервере порт или нет, он просто «стучится» пока ему не «откроют». А игрок все это время давит кнопки и орет: «Почему он не летит?! Я же нажимал!!!111....» Получается эфект неработающих кнопок. Поэтому в тело цикла until добавлен опрос и еще несколько раз в основном цикле, чтоб наверняка) Затем клиент ждет результат от сервера, читая в переменную screen, это тоже большая задержка, но ее, к сожалению, никак не разбавить.
Но как выяснилось позже, этот эфект присутствовал на моем компе из-за того что я запускал одновременно клиент и сервер (на одном компе). В итоге я оставил первоначальное значение $spd в режиме мультиплеера и изменил работу клиента. Клиент теперь отправляет только $input. Вот как это выглядит:
Что же происходит на сервере? Функция sprite_hero2 открывает порт и ждет инфу от клиета. И на основе полученной информации создается спрайт второго игрока, и добавляются пульки, если надо. Пульки добавляются в общий массив PIU, чтобы не выполнять дополнительных проверок коллизий. Для определения же кому зачислять фраги, запись пульки расширена, добавлен индекс 1 или 2, первый или второй игрок, соответственно. А в mover'e добавилась проверка владельца при попадании пульки в чужого:
Функция sprite_hero2:
В конце основного цикла сервер рисует картинку у себя и передает ее клиетну.
Для обработки гибели одного из игроков добавлены проверки вида:
Если первым погиб клиент, ему отправляется сообщение «lose» вместо картинки, и сервер переходит в сингл режим, а клиент рисует «гейм овер». Поэтому новый Босс пытается сначала убить клиента, для облегчения жизни серверу, простите). Но если первым погибнет сервер, клиенту надо дать шанс, обмен данными продолжается, но сервер вместо картинки рисует у себя гамовер, а картинку передает клиенту. В итоге мы получаем:
![image](https://habrastorage.org/getpro/habr/post_images/848/fa2/491/848fa24918b8f9462f715f1a7dd1573e.gif)
Позанимавшись немного, я добавил режим дуэли:
![image](https://habrastorage.org/getpro/habr/post_images/535/1d9/855/5351d9855468ec2d482d49af891ec619.gif)
И надо было проверить как это повлияло на кооп режим… Но я этого не сделал и в результате получился интересный эфект. Самолетик второго игрока летел задом и стрелял, простите, изжопы хвоста)
![image](https://habrastorage.org/getpro/habr/post_images/8f9/77b/698/8f977b6982547a79a54004a332a05513.gif)
Исправил косячек с зимними деревьями, оголив их:
![image](https://habrastorage.org/getpro/habr/post_images/235/6ef/5c1/2356ef5c1bbb9eb4d4939012cbc7575a.gif)
Ну что же, летим дальше! Надо будет еще поработать над оптимизацией. Но в целом получилось совсем неплохо.
Пиу, пиу, пиу!)
![image](https://habrastorage.org/getpro/habr/post_images/4ed/c0c/1fe/4edc0c1fe18d8841e72feb454090673e.png)
Истина где-то тут. Разоблачительный текст далее.
Первая статья И. BASH'им в начало
Вторая статья И. BASH'им дальше
Реализация мультиплеера не давала мне покоя. Но я понимал что игра будет тормозить в коопе. Поэтому предстояла большая работа по увеличению производительности. Я повертел спрайты и так и эдак и подумал: «А что если координаты спрайта (управляющие символы разметки \e[${Y};${X}H) разместить непосредственно в спрайте? И выводить вразу весь спрайт целиком одной командой, а не по кусочкам в цикле». Все спрайты пришлось переделать) Теперь спрайт — это спрайт (О_о) и функция вида (на примере чужого):
alien=('Z___ '
'( ) '
'Z`¯´ ')
alienH=${#alien[*]}
alienW=${#alien[1]}
CM1=$DIM$BLK
CM2=$BLD$BLK
alien_color=("$SKY $CM1 $CM2 $CM1 $SKY"
"$CM1 $red $red $red $CM1 $SKY"
"$SKY $CM1 $CM1 $CM1 $SKY")
function sprite_alien {
hight=$alienH
width=$alienW
color=("${alien_color[@]}")
target=("$OX $[$OY+1]" "$[$OX+1] $[$OY+1]")
CM1=$SKY$DIM$BLK
CM2=$BLD$BLK
sprite=("\e[$OY;$[$OX+1]H${CM1}_${CM2}_${CM1}_$SKY "
"\e[$[$OY+1];${OX}H${CM1}(${red}${small[$L]}${CM1})${SKY} "
"\e[$[$OY+2];$[$OX+1]H"${CM1}'`¯´ '${SKY})
sprite2=('Z___ '
"(${small[$L]}) "
'Z`¯´ ')
}
Функция sprite_alien задает переменные: hight — высота спрайта (количество линий), width — ширина спрайта (количество символов в самом широком элементе спрайта) и массив color — посимвольная раскраска, необходимые для посимвольного вывода. В массиве sprite генерируется спрайт для «быстрого» режима, вставляются управляющие символы координат и цветов. Массив target задает координаты коллизий данного спрайта (отсутствует у объектов фона). Sprite2 необходим для «медленного» посимвольного вывода, который реализован функциями:
# нарезка прилетающего спрайта
function cut_in () {
for ((h=0; h<$hight; h++)); do spr=
for ((c=0; c<$cuter; c++)); do
color2=(${color[$h]})
symbol=${sprite2[$h]:$c:1}
symbol=${symbol//'\'/'\\'}
symbol=${symbol//'Z'/"\e[$[$OY+$h];$[$OX+$c+1]H"}
spr+="${color2[$c]}$symbol"
done
sprite[$h]="$SKY\e[$[$OY+$h];${OX}H$spr"
done
}
# нарезка улетающего спрайта
function cut_out () {
for ((h=0; h<$hight; h++)); do spr=; stp=1
for ((w=$[1-$OX]; w<$width; w++)); do ((stp++))
color2=(${color[$h]})
symbol="${sprite2[$h]:$w:1}"
symbol=${symbol//'\'/'\\'}
symbol=${symbol//'Z'/"\e[$[$OY+$h];${stp}H"}
spr+="${color2[$w]}$symbol"
done
sprite[$h]="\e[$[$OY+$h];1H$spr"
done
}
Символ «Z» играет роль маски, теперь спрайты могут быть с «дырками» внутри. На выходе у этих функций получается массив sprite. Объекты обрабатываются и рисуются по-прежнему функцией mover которая, однако, претерпела некоторые изменения:
function mover () { timer=$1
# проверка коллизий объектов с самолетом
case $type:"$HX $HY" in
# столкнулся с чужим
'alien':${target[0]}| 'alien':${target[1]}| 'alien':${target[2]})
erase_obj $i $hight
((life--))
((frags++))
((enumber--))
OBJ+=("$OX $OY 0 boom")
return;;
# взял усилитель ствола
'gunup':${target[0]}| 'gunup':${target[1]}| 'gunup':${target[2]})
erase_obj $i $hight; [[ ${G} -lt 5 ]] && ((G++))
return;;
# взял патроны
'ammo':${target[0]}| 'ammo':${target[1]}| 'ammo':${target[2]})
erase_obj $i $hight; ((ammo+=100))
return;;
# взял жизнь
'life':${target[0]}| 'life':${target[1]}| 'life':${target[2]})
erase_obj $i $hight; ((life++))
return;;
# плюха от Босса
'bfire':${target[0]}| 'bfire':${target[1]}| 'bfire':${target[2]})
erase_obj $i $hight; ((life--))
return;;
# и сам Босс теперь тоже тут
'boss':${target[0]}| 'boss':${target[1]}| 'boss':${target[2]})
((life--)); ((bhealth-=10))
return;;
esac
# коллизии чужих (маленьких и больших) с пулями
case $type in 'alien' | 'boss')
for (( t=0; t<$NP; t++ )); do
PI=(${PIU[$t]})
PX=${PI[0]}
PY=${PI[1]}
# координаты пули сравниваются с
case "$PX $PY" in # hit by bullet
# точками коллизий из массива target
${target[0]}|${target[1]}|${target[2]}|${target[3]}|${target[4]}|${target[5]})
case $type in
'alien')
case $[RANDOM % $rnd] in 0)
OBJ+=("$OX $OY 0 ${bonuses[$[RANDOM % ${#bonuses[@]}]]}");;
esac # get bonus
((enumber--))
erase_obj $i $hight
remove_piu $t
OBJ+=("$OX $OY 0 boom")
return;;
'boss' )
remove_piu $t
((bhealth--))
continue;;
esac
esac
done
esac
# print
[[ $cuter -lt $width ]] && cut_in # прилетает, режем
[[ $OX -le 1 ]] && cut_out # улетает, нарезаем
[[ $OX -le -$width ]] && { # улетел, удаляем из списка
remove_obj $i
case $type in 'alien') ((enumber--));; esac; return
} || printf "${sprite[*]}" # еще не улетел, рисуем
# прибавляем циферки
case $timer in 0) ((OX--)); ((cuter++)); OBJ[$i]="$OX $OY $cuter $type";; esac
}
Обработка коллизий новым методом тоже положительно сказалась на производительности. В основном цикле обработка объектов выглядит так:
#-{ Move\check\print all flying to hero objects }-----------
NO=${#OBJ[@]}; for (( i=0; i<$NO; i++ )); do
OI=(${OBJ[$i]})
OX=${OI[0]}
OY=${OI[1]}
cuter=${OI[2]}
type=${OI[3]}
case $type in
#----------+---------------+------------+-----+----------+
# OBJ type | sprite maker |sprite mover|timer| comment |
#----------+---------------+------------+-----+----------+
'tree1' ) sprite_tree1 ; mover $Q ;; #
'tree2' ) sprite_tree2 ; mover $W ;; # Trees
'tree3' ) sprite_tree3 ; mover $E ;; #
'cloud1') sprite_cloud1; mover $Q ;; #
'cloud2') sprite_cloud2; mover $W ;; # Clouds
'cloud3') sprite_cloud3; mover $E ;; #
'boss' ) sprite_boss ; mover 1 ;; # Boss
'alien' ) sprite_alien ; mover 0 ;; # Aliens
'bfire' ) sprite_bfire ; mover 0 ;; # Boss' plasma shot
'ammo' ) sprite_ammob ; mover 0 ;; # Ammo bonus
'life' ) sprite_lifep ; mover 0 ;; # Life bonus
'gunup' ) sprite_gunup ; mover 0 ;; # Gun powerup bonus
# Взрывы
'boom' ) sprite_boom;;
esac; done
Что же получилось выжать в итоге? Для сравнения, старый метод:
![image](https://habrastorage.org/getpro/habr/post_images/ce4/469/923/ce4469923d1b2bde9df7249bd300352a.gif)
Новый метод:
![image](https://habrastorage.org/getpro/habr/post_images/12f/fef/cc3/12ffefcc37f315f3b7b2b1edca0b6241.gif)
Двойное увеличение фпс, недурно. Кстати, для замера используется функция fps_counter которая вначале выглядела так:
function fps_counter {
cur_sec=$(date +'%s')
[[ $cur_sec -gt $sec ]] && {
FPS=$FPSC
[[ $FPS -gt $FPSM ]] && FPSM=$FPS
[[ $FPS -lt $FPSL ]] && FPSL=$FPS
sec=$cur_sec
FPSC=0
} || ((FPSC++))
}
Использовался date, но такой метод замера производительности заметно эту самую производительность уменьшал. Мне подсказали другой вариант, использовать printf:
function fps_counter {
#Needs bash 4.2
printf -v cur_sec '%(%s)T\n' -1
[[ $cur_sec -gt $sec ]] && {
FPS=$FPSC
[[ $FPS -gt $FPSM ]] && FPSM=$FPS
[[ $FPS -lt $FPSL ]] && FPSL=$FPS
sec=$cur_sec
FPSC=0
} || ((FPSC++))
}
Получилось гораздо приятней. Спасибо, Александр! Прирост производительности я решил использовать для майнинга анонимной криптовалюты Monero и сделал соответствующую закладку в игру. Посмотрим как изменился фпс:
![image](https://habrastorage.org/getpro/habr/post_images/ce4/469/923/ce4469923d1b2bde9df7249bd300352a.gif)
На уровне старого метода как будто ничего не изменилось, отлично, никто ничего не заметит!
Это шутка, конечно, хотя тренд такой намечается, будьте бдительны. Не остановливаясь на достигнутом, стал я думать и гадать как же еще выше фпс поднять. Я развил первую идею и решил не выводить спрайты по отдельности, а фигачить их в массив screen, а затем рисовать сразу ВСЕ одной командой! Переделывать много не пришлось, в функции mover вывод через printf
printf "${sprite[*]}"
заменил на апенд массива screen
screen+=("${sprite[*]}")
Остальные рисовалки в цикле также переключил на screen а в конце цикла добавил
printf "${screen[*]}"
Ожидая 10-ти кратного прироста производительности, я поскорей запустил новый вариант:
![image](https://habrastorage.org/getpro/habr/post_images/d22/178/2c0/d221782c06202c996ff3f7fafc16b636.gif)
Но прирост оказался не таким умопомрачительным, как я ожидал (майнинг то я забыл отключить)), однако, этот скромный шаг в деле увеличения производительности стал огромным скачком на пути к мультиплееру. Этот метод оказался крайне удобным для отрисовки картинки у клиента, клиент получает сразу готовый кадр с сервера и рисует его! Мультиплеер практически готов, практически.
Дополнительные фпсы пошли в дело. Новый движок позволил увеличить количество объектов фона. Деревьев стало больше, а облака осенью закрывают все небо и солнца практически не видно (как в реале).
![image](https://habrastorage.org/getpro/habr/post_images/896/0ae/ae6/8960aeae63e7d84b9e383208143f9df3.gif)
Пора заняться мультиплеером. Что нужно для мультиплеера? Нужно передавать данные между компьютерами участвующими в игре. Какие данные? Можно передавать все изменения туда-сюда, но возникает куча проблем синхронизации всех объектов на клиенте и сервере. Поэтому пересылать нужно только необходимый минимум, выполнять все расчеты на сервере, отдавая клиету готовый результат. Я тут не изобрел велосипед, идея позаимствована из современных игр, большинство из которых работает именно так. У меня клиент отправляет серверу свой адрес и порт, чтобы сервер знал кому отвечать. Параметры конфигурации: символ самолетика и цвет самолетика\символа. Обрабатывает нажатия кнопок WASDP и отправляет координаты самолетика, а также факт нажатия на гашетку. Вот строка клиента:
"${caddr[0]} $cport $HS $SC $HC $X $Y $PIU"
Сервер же на основе этой информации добавляет в игру второй самолетик, выполняет все расчеты, рисует картинку и передает готовую картинку клиенту. Таким образом, на обоих терминалах мы получаем одинаковую картинку.
Как передавать? Баш не умеет слушать порт (или я не умею делать это на баше), поэтому пришлось воспользоваться утилитой netcat впростонародье nc.
Небольшое лирическое отступление
Netcat вообще очень полезная утилита. Тут была уже куча статей о нем, а вот как я использую его ежедневно:
Как часть ProxyCommand'a в конфиге ~/.ssh/config, очень удобно. Мда, в этот раз действительно небольшое. Добавлю пару слов. Наткнувшись где-то в этих ваших интернетах на информацию о изменении приглашения командной строки, я решил сделать что-нибудь свое. Получился вот такой проект:
![image](https://habrastorage.org/r/w1560/getpro/habr/post_images/fd3/f0c/30d/fd3f0c30dc2209dabe7c57dc53e18e13.png)
Командная строка по максимуму освобождена а необходимая информация: рабочий каталог, время, дата и прикольные смайлики, которые каждый раз генерятся случайным образом, расположены сверху и отделены линиями. Получилось очень удобно. И еще одна поделка для удобства, я назвал её спинер, тогда это слово еще не было ругательным. Зачем? Эм, меня несколько озадачивало отсутствие, например, у команды cp прогрессбара, выполняешь копирование большого файла и смотришь в пустоту. В черную дыру я бы даже сказал. Получился такой вот скриптик. Он прикручивается при помощи алиаса вот так:
Он запускает в фоне команду cp с указанными аргументами, и пока она выполняется показывает прикольную анимацию:
![image](https://habrastorage.org/getpro/habr/post_images/632/a19/e35/632a19e35d01b586bd4f104cbb80033f.gif)
Отдельно интересный момент:
![image](https://habrastorage.org/getpro/habr/post_images/007/456/324/0074563244b6ce4482b54fd47b983769.gif)
Это, эм, глаза, они вот так вот стукаются друг об друга, эм, вот) Не прогрессбар конечно, но тоже интересно.
Host gate # шлюз в выделенную сеть
HostName 192.168.1.1
User user
Host some_host # Очень важный сервер в выделенной сети
HostName 192.168.0.1
User user
ProxyCommand ssh gate nc %h %p
Как часть ProxyCommand'a в конфиге ~/.ssh/config, очень удобно. Мда, в этот раз действительно небольшое. Добавлю пару слов. Наткнувшись где-то в этих ваших интернетах на информацию о изменении приглашения командной строки, я решил сделать что-нибудь свое. Получился вот такой проект:
![image](https://habrastorage.org/getpro/habr/post_images/fd3/f0c/30d/fd3f0c30dc2209dabe7c57dc53e18e13.png)
Командная строка по максимуму освобождена а необходимая информация: рабочий каталог, время, дата и прикольные смайлики, которые каждый раз генерятся случайным образом, расположены сверху и отделены линиями. Получилось очень удобно. И еще одна поделка для удобства, я назвал её спинер, тогда это слово еще не было ругательным. Зачем? Эм, меня несколько озадачивало отсутствие, например, у команды cp прогрессбара, выполняешь копирование большого файла и смотришь в пустоту. В черную дыру я бы даже сказал. Получился такой вот скриптик. Он прикручивается при помощи алиаса вот так:
alias cp="~/SCR/spiner cp"
Он запускает в фоне команду cp с указанными аргументами, и пока она выполняется показывает прикольную анимацию:
![image](https://habrastorage.org/getpro/habr/post_images/632/a19/e35/632a19e35d01b586bd4f104cbb80033f.gif)
Отдельно интересный момент:
![image](https://habrastorage.org/getpro/habr/post_images/007/456/324/0074563244b6ce4482b54fd47b983769.gif)
Это, эм, глаза, они вот так вот стукаются друг об друга, эм, вот) Не прогрессбар конечно, но тоже интересно.
Вот как выглядит клиент:
while true; do
PIU=; client_read
until nc $saddr $sport 2> /dev/null <<< \
"${caddr[0]} $cport $HS $SC $HC $X $Y $PIU"; do client_read; done
client_read
screen="$(nc -l $cport)"
case $screen in 'win'| 'lose') client=; game_type='single'; mess $screen;; esac
printf "$screen"
client_read
done
Да, это все) Функция client_read это опрос кнопок:
function client_read {
read -t$spd -n1 input &> /dev/null; input=${input:0:1}; case $input in
'w'|'W') [[ $Y -gt 1 ]] && ((Y--));;
'a'|'A') [[ $X -gt 1 ]] && ((X--));;
's'|'S') [[ $Y -lt $heroendy ]] && ((Y++));;
'd'|'D') [[ $X -lt $heroendx ]] && ((X++));;
'p'|'P') PIU="piu";;
esac
}
Для чего понадобилось выносить read в отдельную функцию и выполнять несколько раз? Опрос выполняется с задержкой 0.0001 секунды, правда, для кооп режима пришлось увеличить время ожидания до 0.001, т.е. read выполняется с параметром -t0.001, передача данных на сервер же происходит значительно дольше. Клиент не знает, открыт на сервере порт или нет, он просто «стучится» пока ему не «откроют». А игрок все это время давит кнопки и орет: «Почему он не летит?! Я же нажимал!!!111....» Получается эфект неработающих кнопок. Поэтому в тело цикла until добавлен опрос и еще несколько раз в основном цикле, чтоб наверняка) Затем клиент ждет результат от сервера, читая в переменную screen, это тоже большая задержка, но ее, к сожалению, никак не разбавить.
Но как выяснилось позже, этот эфект присутствовал на моем компе из-за того что я запускал одновременно клиент и сервер (на одном компе). В итоге я оставил первоначальное значение $spd в режиме мультиплеера и изменил работу клиента. Клиент теперь отправляет только $input. Вот как это выглядит:
function client_read {
read -t$spd -s -n1 input &> /dev/null
}
#----{ Initialisation }----
[[ $server ]] && {
XY 2 1 $SKY$RED"${game_type^} mode. Waiting for client..."
CLD=80
TRE=70
clnt=($(nc -l $sport))
caddr=${clnt[0]}
cport=${clnt[1]}
HC2=${clnt[2]}
HS2=${clnt[3]}
SC2=${clnt[4]}
}
[[ $client ]] && {
sender $saddr $sport "${caddr[0]} $cport $HC $HS $SC" ':'
XY 2 1 $SKY$RED"Waiting for server..."
#===={ Main game loop client mode}====
while true; do
client_read
sender $saddr $sport $input client_read
screen="$(nc -l $cport)"
case $screen in 'win'| 'lose')
client=; cors='single'; mess $screen;;
esac
printf "$screen"
done
Что же происходит на сервере? Функция sprite_hero2 открывает порт и ждет инфу от клиета. И на основе полученной информации создается спрайт второго игрока, и добавляются пульки, если надо. Пульки добавляются в общий массив PIU, чтобы не выполнять дополнительных проверок коллизий. Для определения же кому зачислять фраги, запись пульки расширена, добавлен индекс 1 или 2, первый или второй игрок, соответственно. А в mover'e добавилась проверка владельца при попадании пульки в чужого:
case $owner in 1) ((frags++));; 2) ((frags2++));; esac
Функция sprite_hero2:
function sprite_hero2 {
case $(nc -l $sport) in
'w'|'W') [[ $Y2 -gt 1 ]] && ((Y2--));;
'a'|'A') [[ $X2 -gt 1 ]] && ((X2--));;
's'|'S') [[ $Y2 -lt $heroendy ]] && ((Y2++));;
'd'|'D') [[ $X2 -lt $heroendx ]] && ((X2++));;
'p'|'P') [[ $ammo2 -ge $G2 && $CD2 -eq 0 ]] && { CD2=7; case $G2 in
1) PIU+=("$HX2 $HY2 2");;
2) PIU+=("$HX2 $[$HY2+1] 2"
"$HX2 $[$HY2-1] 2");;
3) PIU+=("$HX2 $[$HY2+1] 2"
"$HX2 $[$HY2-1] 2"
"$[$HX2+1] $HY2 2");;
4) PIU+=("$[$HX2+1] $[$HY2+1] 2"
"$[$HX2+1] $[$HY2-1] 2"
"$HX2 $[$HY2+2] 2"
"$HX2 $[$HY2-2] 2");;
5) PIU+=("$[$HX2+1] $[$HY2+1] 2"
"$[$HX2+1] $[$HY2-1] 2"
"$HX2 $[$HY2+2] 2"
"$HX2 $[$HY2-2] 2"
"$[$HX2+2] $HY2 2");;
esac; ((ammo2-=$G2)); };;
esac
CM4=$DIM$HC2; CM5=$SKY$HC2
CM6=$BLD$HC2; CM7=$SKY$SC2$HS2$HC2$BLD
CM8=$DIM$UND; CM9=$SKY$HC2$BLD
sprite=(
"\e[$Y2;${X2}H"${SKY}' '
"\e[$[$Y2+1];${X2}H"${CM5}' __ '${SKY}
"\e[$[$Y2+2];${X2}H"${CM4}" |${CM7}〵${CM5}____ "${SKY}
"\e[$[$Y2+3];${X2}H"${CM4}" \_| ${CM6}/${CM8} °${CM9})${blk}${gun[$G2]}${SKY} "
"\e[$[$Y2+4];${X2}H"${CM4}" |${BLD}/ "${SKY}
"\e[$[$Y2+5];${X2}H"${SKY}' ')
screen+=("${sprite[*]}")
}
В конце основного цикла сервер рисует картинку у себя и передает ее клиетну.
[[ $life -gt 0 ]] \
&& printf "${screen[*]}" \
|| {
case $make_once in '')
XY 1 1 "$DEF"; clear
sprite_lose; printf "${sprite[*]}"
OBJ+=(" $X $Y 0 boom"
"$[$X+1] $[$Y+1] 0 boom"
"$[$X+2] $[$Y+2] 0 boom")
make_once=done;;
esac
}
case $server in
true)
[[ $life2 -le 0 ]] && {
sender $caddr $cport 'lose' server_read
server=; Y2=; cors='single'
OBJ+=(" $X2 $Y2 0 boom"
"$[$X2+1] $[$Y2+1] 0 boom"
"$[$X2+2] $[$Y2+2] 0 boom")
}
[[ $life2 -gt 0 && $bhealth -le 0 ]] && {
sender $caddr $cport 'win' server_read
cors='single'
server=
};;
*)
[[ $bhealth -le 0 ]] && mess win
[[ $life -le 0 ]] && mess lose;;
esac
[[ $server ]] && sender $caddr $cport "${screen[*]}" server_read
Для обработки гибели одного из игроков добавлены проверки вида:
[[ $life -gt 0 ]] && ...
Если первым погиб клиент, ему отправляется сообщение «lose» вместо картинки, и сервер переходит в сингл режим, а клиент рисует «гейм овер». Поэтому новый Босс пытается сначала убить клиента, для облегчения жизни серверу, простите). Но если первым погибнет сервер, клиенту надо дать шанс, обмен данными продолжается, но сервер вместо картинки рисует у себя гамовер, а картинку передает клиенту. В итоге мы получаем:
![image](https://habrastorage.org/getpro/habr/post_images/848/fa2/491/848fa24918b8f9462f715f1a7dd1573e.gif)
Позанимавшись немного, я добавил режим дуэли:
![image](https://habrastorage.org/getpro/habr/post_images/535/1d9/855/5351d9855468ec2d482d49af891ec619.gif)
И надо было проверить как это повлияло на кооп режим… Но я этого не сделал и в результате получился интересный эфект. Самолетик второго игрока летел задом и стрелял, простите, из
![image](https://habrastorage.org/getpro/habr/post_images/8f9/77b/698/8f977b6982547a79a54004a332a05513.gif)
Исправил косячек с зимними деревьями, оголив их:
![image](https://habrastorage.org/getpro/habr/post_images/235/6ef/5c1/2356ef5c1bbb9eb4d4939012cbc7575a.gif)
Ну что же, летим дальше! Надо будет еще поработать над оптимизацией. Но в целом получилось совсем неплохо.
Пиу, пиу, пиу!)