Mark blog

知行合一 划水归档

使用shell脚本实现俄罗斯方块

需求分析

学习 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;
}

里面定义了方块移动的相关信息,通过判断方块的位置来确定是否可以进行移动