需求分析 学习 shell 的最好方法就是去实践,网上有很多的源码可供参考
实现过程 源码分析:
先从游戏主程序开始分析:
1 2 3 4 5 6 7 8 9 10 11 if [[ "$1" == "-h" || "$1" == "--help" ]]; then Usage elif [[ "$1" == "--version" ]]; then echo "$APP_NAME $APP_VERSION" elif [[ "$1" == "--show" ]]; then #当发现具有参数--show时,运行显示函数 RunAsDisplayer else bash $0 --show& #以参数--show将本程序再运行一遍 RunAsKeyReceiver $! #以上一行产生的进程的进程号作为参数 fi
里面出现了两个变量,查看定义后发现
APP_NAME : 打印了脚本名
APP_NAME="${0##*[\\/]}"
APP_VERSION : 自己定义了版本号
APP_VERSION="1.0"
其中出现了3个函数
Usage :显示脚本用法的函数
RunAsDisplayer :处理显示和游戏流程的主函数
RunAsKeyReceiver :接收输入的进程的主函数
我们从头开始分析这些函数的实现
1 2 3 4 5 6 7 8 9 10 function Usage { cat << EOF Usage: $APP_NAME Start tetris game. -h, --help display this help and exit --version output version information and exit EOF }
可以看出 Usage 函数就是打印了一些信息,主要是用户提示.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 function RunAsDisplayer() { local sigThis #定义了局部变量 sigThis InitDraw #挂载各种信号的处理函数 trap "sig=$sigRotate;" $sigRotate trap "sig=$sigLeft;" $sigLeft trap "sig=$sigRight;" $sigRight trap "sig=$sigDown;" $sigDown trap "sig=$sigAllDown;" $sigAllDown trap "ShowExit;" $sigExit while : do #根据当前的速度级iLevel不同,设定相应的循环的次数 for ((i = 0; i < 21 - iLevel; i++)) do sleep 0.02 sigThis=$sig sig=0 #根据sig变量判断是否接受到相应的信号 if ((sigThis == sigRotate)); then BoxRotate; #旋转 elif ((sigThis == sigLeft)); then BoxLeft; #左移一列 elif ((sigThis == sigRight)); then BoxRight; #右移一列 elif ((sigThis == sigDown)); then BoxDown; #下落一行 elif ((sigThis == sigAllDown)); then BoxAllDown; #下落到底 fi done #kill -$sigDown $$ BoxDown #下落一行 done }
暂时先不管 RunAsDisplayer 中调用的 InitDraw 函数,先看一下这个函数的功能.
从流程上分析这个函数实现了按键信号的捕捉,然后在一个死循环中以操作时间为0.02秒,不断的执行对方块的移动操作,然后执行下落一行的操作.
接下来对其中出现的参数和函数进行分析:
trap 这个命令实现了shell中脚本信号的获取,可以在脚本运行中执行额外的操作,比如获取键盘的输入用来控制方块的运动轨迹.
iLevel 这个参数是定义的速度级,这个稍后会提到.
BoxDown : 控制方块下落的函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 function RunAsKeyReceiver() { local pidDisplayer key aKey sig cESC sTTY pidDisplayer=$1 aKey=(0 0 0) cESC=`echo -ne "\033"` cSpace=`echo -ne "\040"` #保存终端属性。在read -s读取终端键时,终端的属性会被暂时改变。 #如果在read -s时程序被不幸杀掉,可能会导致终端混乱, #需要在程序退出时恢复终端属性。 sTTY=`stty -g` #捕捉退出信号 trap "MyExit;" INT TERM trap "MyExitNoSub;" $sigExit #隐藏光标 echo -ne "\033[?25l" while : do #读取输入。注-s不回显,-n读到一个字符立即返回 read -s -n 1 key aKey[0]=${aKey[1]} aKey[1]=${aKey[2]} aKey[2]=$key sig=0 #判断输入了何种键 if [[ $key == $cESC && ${aKey[1]} == $cESC ]] then #ESC键 MyExit elif [[ ${aKey[0]} == $cESC && ${aKey[1]} == "[" ]] then if [[ $key == "A" ]]; then sig=$sigRotate #<向上键> elif [[ $key == "B" ]]; then sig=$sigDown #<向下键> elif [[ $key == "D" ]]; then sig=$sigLeft #<向左键> elif [[ $key == "C" ]]; then sig=$sigRight #<向右键> fi elif [[ $key == "W" || $key == "w" ]]; then sig=$sigRotate #W, w elif [[ $key == "S" || $key == "s" ]]; then sig=$sigDown #S, s elif [[ $key == "A" || $key == "a" ]]; then sig=$sigLeft #A, a elif [[ $key == "D" || $key == "d" ]]; then sig=$sigRight #D, d elif [[ "[$key]" == "[]" ]]; then sig=$sigAllDown #空格键 elif [[ $key == "Q" || $key == "q" ]] #Q, q then MyExit fi if [[ $sig != 0 ]] then #向另一进程发送消息 kill -$sig $pidDisplayer fi done }
这个函数是接收输入的进程的主程序,然后判断用户的输入,在这个脚本中,可以使用方向键和wasd来控制方块的运行轨迹,然后不断显示直到游戏结束.
里面调用了一个MyExit的函数.
接下来分析一下 RunAsDisplayer中BoxDown 这个函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 function BoxDown() { local y s ((y = boxCurY + 1)) #新的y坐标 if BoxMove $y $boxCurX #测试是否可以下落一行 then s="`DrawCurBox 0`" #将旧的方块抹去 ((boxCurY = y)) s="$s`DrawCurBox 1`" #显示新的下落后方块 echo -ne $s else #走到这儿, 如果不能下落了 Box2Map #将当前移动中的方块贴到背景方块中 RandomBox #产生新的方块 fi }
这个函数主要是定义了控制方块下落的功能,首先会判断是否可也下落,如果可以消除的话,先把可以消除的方块抹去,然后显示新的下落后的方块,如果不能下落了就将当前移动中的方块贴到背景方块中,然后继续产生新的方块.
里面调用了四个函数
BoxMove : 测试是否可以下落一行
DrawCurBox : 显示方块
Box2Map : 将当前移动中的方块贴到背景方块中
RandomBox : 产生新的方块
查看一下BoxMove这个函数,看看其中的功能是怎么实现的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 function BoxMove() { local j i x y xTest yTest yTest=$1 xTest=$2 for ((j = 0; j < 8; j += 2)) do ((i = j + 1)) ((y = ${boxCur[$j]} + yTest)) ((x = ${boxCur[$i]} + xTest)) if (( y < 0 || y >= iTrayHeight || x < 0 || x >= iTrayWidth)) then #撞到墙壁了 return 1 fi if ((${iMap[y * iTrayWidth + x]} != -1 )) then #撞到其他已经存在的方块了 return 1 fi done return 0; }
里面定义了方块移动的相关信息,通过判断方块的位置来确定是否可以进行移动