📜 ⬆️ ⬇️

Play bash'im together

Playing bash with multiplayer support, myth or reality?

image

The truth is somewhere here . Exposure text below.

The first article I. BASH'im top
The second article I. BASH'im further
')
The implementation of multiplayer did not give me rest. But I understood that the game would slow down in a co-op. Therefore, there was a lot of work to increase productivity. I turned the sprites this way and that and thought: “What if the coordinates of the sprite (the control markup characters \ e [$ {Y}; $ {X} H) are placed directly in the sprite? And display the whole sprite at once in one team, not in pieces in a loop. ” All the sprites had to be redone) Now the sprite is a sprite (O_o) and a function of the form (on the example of someone else's):

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`¯´ ') } 

The sprite_alien function sets variables: hight - the height of the sprite (number of lines), width - the width of the sprite (the number of characters in the widest element of the sprite) and the array color - character-by-character coloring required for character-by-character output. In the sprite array, a sprite is generated for the “fast” mode, the control characters of coordinates and colors are inserted. The target array specifies the coordinates of the collisions of this sprite (absent from the background objects). Sprite2 is required for “slow” character-by-character output, which is implemented by the functions:

 #    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 } 

The symbol “Z” plays the role of a mask, now sprites can be with “holes” inside. The output of these functions is an array of sprite . Objects are still processed and drawn with the mover function , which, however, has undergone some changes:

 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 } 

Handling collisions with a new method also had a positive effect on performance. In the main loop, processing objects looks like this:

 #-{ 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 

What happened to squeeze in the end? For comparison, the old method:

image

New method:

image

Double increase in FPS, not bad. By the way, for the measurement function is used fps_counter which initially looked like this:

 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 was used, but this method of measuring performance noticeably reduced this very performance. I was prompted by another option, use 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++)) } 

It turned out much nicer. Thank you, Alexander! I decided to use the performance boost for mining the anonymous cryptocurrency Monero and made the appropriate tab to the game. Let's see how the FPS has changed:

image

At the level of the old method, as if nothing has changed, great, no one will notice anything!

This is a joke, of course, although this trend is planned, beware. Not stopping on my laurels, I began to think and guess how to raise the FPS even higher. I developed the first idea and decided not to display the sprites separately, but to draw them into the screen array, and then draw ALL at once with one command! It was not necessary to redo much, in the mover function output via printf

  printf "${sprite[*]}" 

replaced by apend screen array

  screen+=("${sprite[*]}") 

The rest of the drawers in the loop also switched to the screen and at the end of the loop added

  printf "${screen[*]}" 

Waiting for a 10-fold increase in performance, I quickly launched a new version:

image

But the increase was not as breathtaking as I expected (I forgot to turn off mining)), however, this modest step in increasing productivity was a huge leap on the way to multiplayer. This method turned out to be extremely convenient for drawing a picture from a client, the client immediately receives a ready frame from the server and draws it! Multiplayer is almost ready, practically.

Additional FPS went into the business. The new engine allowed to increase the number of background objects. There are more trees, and the clouds in the fall cover the whole sky and the sun is almost invisible (as in the real world).

image

It's time to do multiplayer. What do you need for multiplayer? It is necessary to transfer data between the computers involved in the game. What data? You can transfer all changes back and forth, but there are a lot of problems synchronizing all objects on the client and server. Therefore, you need to send only the necessary minimum, perform all calculations on the server, giving the client the finished result. I did not invent a bicycle here, the idea was borrowed from modern games, most of which work that way. My client sends the server its address and port so that the server knows who to answer. Configuration options: the symbol of the airplane and the color of the airplane \ symbol. Handles the pressing of the WASDP buttons and sends the coordinates of the airplane, as well as the fact of pressing the trigger. Here is the client line:

  "${caddr[0]} $cport $HS $SC $HC $X $Y $PIU" 

The server, on the basis of this information, adds a second airplane to the game, performs all calculations, draws a picture and sends the finished picture to the client. Thus, at both terminals we get the same picture.
How to transmit? Bash doesn’t know how to listen to the port (or I don’t know how to do it on the bash), so I had to use the netcat utility in the common people nc .

Small lyrical digression
Netcat is generally a very useful utility. There were already a lot of articles about him, but how I use it daily:

 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 

As part of ProxyCommand'a in the config ~ / .ssh / config , very convenient. Hmm, this time is really small. Add a few words. Having stumbled somewhere on these your Internet on information about changing the command line prompt, I decided to do something of my own. The result is such a project :

image

The command line is maximized and the necessary information: working directory, time, date and funny emoticons, which are randomly generated each time, are located on the top and are separated by lines. It turned out very convenient. And one more hand-made article for convenience, I called it a spiner, then this word was not yet abusive. What for? Um, I was somewhat puzzled by the lack of, for example, the cp command of the progress bar , you are copying a large file and looking into the void. In a black hole, I would even say. It turned out such a script. It is screwed with an alias like this:

 alias cp="~/SCR/spiner cp" 

It runs the cp command with the specified arguments in the background, and while it is executed it shows a cool animation:

image

Separately, an interesting point:

image

This, uh, eyes, they are knocking against each other like this, uh, here) Not a progress bar of course, but also interesting.

Here is what the client looks like:

 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 

Yes, that's all) The client_read function is a button poll:

 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 } 

Why did you need to make read into a separate function and perform several times? The survey is performed with a delay of 0.0001 seconds, however, for the co-op mode, it was necessary to increase the waiting time to 0.001, i.e. read is performed with the -t0.001 parameter, data transfer to the server takes much longer. The client does not know whether the port is open on the server or not; he simply “knocks” until he is “opened”. And the player all this time presses the buttons and yells: “Why does he not fly ?! I pressed !!! 111 .... "It turns out the effect of non-working buttons. Therefore, a poll has been added to the body of the until loop and several more times in the main loop, so for sure) Then the client waits for the result from the server, reading to the screen variable, this is also a big delay, but, unfortunately, it can not be diluted.

But as it turned out later, this effect was present on my computer due to the fact that I was running both the client and the server (on the same computer). In the end, I left the original value of $ spd in multiplayer mode and changed the client’s work. The client now sends only $ input . Here's what it looks like:

 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 

What happens on the server? The function sprite_hero2 opens the port and waits for information from the client. And on the basis of the information received, a second player's sprite is created, and bullets are added, if necessary. The bullets are added to the common PIU array in order not to perform additional collision checks. To determine who should be credited frags, the bullet record has been expanded, an index of 1 or 2, the first or second player, respectively, has been added. And in the mover'e added verification of the owner when the bullet hit the stranger:

 case $owner in 1) ((frags++));; 2) ((frags2++));; esac 

Sprite_hero2 function:

 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[*]}") } 

At the end of the main loop, the server draws a picture of itself and sends it to the client.

 [[ $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 

To handle the death of one of the players, type checks are added:

 [[ $life -gt 0 ]] && ... 

If the client is the first to die, the message “lose” is sent instead of the picture, and the server goes into single mode, and the client draws a “game over”. Therefore, the new Boss is trying to kill the client first, to make life easier for the server (sorry). But if the server dies first, the client should be given a chance, the data exchange continues, but the server instead of the picture draws a gamover and transfers the picture to the client. As a result, we get:

image

Having worked a bit, I added the duel mode:

image

And it was necessary to check how it affected the co-op regime ... But I did not do that and as a result I got an interesting effect. Airplane of the second player flew backwards and shot, sorry, from the ass tail)

image

Corrected kosyachek with winter trees, exposing them:

image

Well, let's fly further! It will be necessary to work on optimization. But in general, it turned out quite well.

Piu, piu, piu!)

Source: https://habr.com/ru/post/339268/


All Articles