43、脚本编程中的锁机制与太空游戏实现

脚本编程中的锁机制与太空游戏实现

锁机制在脚本编程中的应用

在脚本编程中,当多个进程需要同时访问和修改同一资源时,可能会出现数据混乱的问题。锁机制就可以很好地解决这个问题,下面通过具体的脚本示例来详细介绍。

无锁脚本示例

以下是一个无锁脚本 domain-nolock.sh ,其功能是查询域名的创建日期、过期日期和 DNS 服务器信息,并将结果写入文件 /tmp/domains.txt

#!/bin/bash
KEYFILE=/tmp/domains.txt
MYDOMAIN=$1
echo “$MYDOMAIN Creation Date:” | tee -a $KEYFILE
sleep 2
whois $MYDOMAIN | grep -i created | cut -d”:” -f2- | tee -a $KEYFILE
sleep 2
echo “$MYDOMAIN Expiration Date:” | tee -a $KEYFILE
sleep 2
whois $MYDOMAIN | grep “Expiration Date:” | cut -d”:” -f2- | tee -a $KEYFILE
sleep 2
echo “$MYDOMAIN DNS Servers:” | tee -a $KEYFILE
sleep 2
whois $MYDOMAIN | grep “Name Server:” | cut -d”:” -f2- | \
        grep -v “^$” | tee -a $KEYFILE
sleep 2
echo “... end of $MYDOMAIN information ...” | tee -a $KEYFILE

当我们在两个不同的交互式 shell 中分别运行这个脚本查询不同的域名时,会发现输出文件 /tmp/domains.txt 中的内容变得混乱不堪。这是因为两个脚本同时向同一个文件写入数据,导致信息交错。具体如下:
- 第一个 shell 运行

Instance One$ ./domain-nolock.sh example.com
example.com Creation Date:
      1992-01-01
example.com Expiration Date:
 13-aug-2011
example.com DNS Servers:
 A.IANA-SERVERS.NET
 B.IANA-SERVERS.NET
... end of example.com information ...
Instance One$
  • 第二个 shell 运行
Instance Two$ ./domain-nolock.sh steve-parker.org
steve-parker.org Creation Date:
20-Jun-2000 13:48:46 UTC
steve-parker.org Expiration Date:
20-Jun-2011 13:48:46 UTC
steve-parker.org DNS Servers:
NS.123-REG.CO.UK
NS2.123-REG.CO.UK
... end of steve-parker.org information ...
Instance Two$
  • 查看输出文件
Instance One$ cat /tmp/domains.txt
example.com Creation Date:
      1992-01-01
steve-parker.org Creation Date:
example.com Expiration Date:
20-Jun-2000 13:48:46 UTC
 13-aug-2011
steve-parker.org Expiration Date:
example.com DNS Servers:
20-Jun-2011 13:48:46 UTC
steve-parker.org DNS Servers:
 A.IANA-SERVERS.NET
 B.IANA-SERVERS.NET
NS.123-REG.CO.UK
NS2.123-REG.CO.UK
... end of example.com information ...
... end of steve-parker.org information ...
Instance One$
有锁脚本示例

为了解决上述问题,我们可以使用锁机制。以下是有锁脚本 domain.sh

#!/bin/bash
# LOCK is a global variable. For this usage, lock.myapp.$$ is not suitable.
# /var/run is suitable for root-owned processes; others may use /tmp or /var/tmp
# or their home directory or application filesystem.
# LOCK=/var/run/lock.myapp
LOCK=/tmp/lock.myapp
KEYFILE=/tmp/domains.txt
MYDOMAIN=$1
mydom=/tmp/${MYDOMAIN}.$$
# See kill(1) for the different signals and what they are intended to do.
trap cleanup 1 2 3 6

function release_lock
{
  MYPID=$1
  echo “Releasing lock.”
  sed -i “/^${MYPID}$/d” $LOCK
}

function get_lock
{
  DELAY=2
  GOT_LOCK=0
  MYPID=$1
  while [ “$GOT_LOCK” -ne “1” ]
  do
    PID=
    while [ -s “$LOCK” ]
    do
      PID=`cat $LOCK 2>/dev/null`
      name=`ps -o comm= -p “$PID” 2>/dev/null`
      if [ -z “$name” ]; then
        echo “Process $PID has claimed the lock, but is not running.”
        release_lock $PID
      else
        echo “Process $PID ($name) has already taken the lock:”
        ps -fp $PID | sed -e 1d
        date
        echo
        sleep $DELAY
        let DELAY=”$DELAY + 1”
      fi
    done
    # Store our PID in the lock file
    echo $MYPID >> $LOCK
    # If another instance also wrote to the lock, it will contain
    # more than $$ and $PID
    # PID could be blank, so surround it with quotes.
    # Otherwise it is saying “-e $LOCK” and passing no filename,
    grep -vw $MYPID $LOCK > /dev/null 2>&1
    if [ “$?” -eq “0” ]; then
      # If $? is 0, then grep successfully found something else in the file.
      echo “An error occurred. Another process has taken the lock:”
      ps -fp `grep -vw -e $MYPID -e “$PID” $LOCK`
      # The other process can take care of itself.
      # Relinquish access to the lock
      # sed -i can do this atomically.
      # Back off by sleeping a random amount of time.
      sed -i “/^${$MYPID}$/d” $LOCK
      sleep $((RANDOM % 5))
    else
      GOT_LOCK=1
      # Claim exclusive access to the lock
      echo $MYPID > $LOCK
    fi
  done
}

function cleanup
{
  echo “$$: Caught signal: Exiting”
  release_lock
  exit 0
}

# Main Script goes here.
# You may want to do stuff without the lock here.
# Then get the lock for the exclusive work
get_lock $$
############
# Do stuff #
############
echo “$MYDOMAIN Creation Date:” | tee -a $KEYFILE
sleep 2
whois $MYDOMAIN | grep -i created | cut -d”:” -f2- | tee -a $KEYFILE
sleep 2
echo “$MYDOMAIN Expiration Date:” | tee -a $KEYFILE
sleep 2
whois $MYDOMAIN | grep “Expiration Date:” | cut -d”:” -f2- | tee -a $KEYFILE
sleep 2
echo “$MYDOMAIN DNS Servers:” | tee -a $KEYFILE
sleep 2
whois $MYDOMAIN | grep “Name Server:” | cut -d”:” -f2- | \
        grep -v “^$” | tee -a $KEYFILE
sleep 2
echo “... end of $MYDOMAIN information ...” | tee -a $KEYFILE
echo >> $KEYFILE
# Then release the lock when you are done.
release_lock $$
# Again, there may be stuff that you will want to do after the lock is released
# Then cleanly exit.
exit 0

当我们使用这个有锁脚本时,情况就会有所不同。第一个实例运行时会获取锁,第二个实例在检测到锁被占用后,会等待锁被释放,然后再继续执行。具体如下:
- 第一个 shell 运行

Instance One$ ./domain.sh example.com
example.com Creation Date:
      1992-01-01
example.com Expiration Date:
 13-aug-2011
example.com DNS Servers:
 A.IANA-SERVERS.NET
 B.IANA-SERVERS.NET
... end of example.com information ...
Releasing lock.
Instance One$
  • 第二个 shell 运行
Instance Two$ ./domain.sh steve-parker.org
Process 14228 (domain.sh) has already taken the lock:
steve    14228 12786  0 12:47 pts/7    00:00:00 /bin/bash ./domain.sh example.com
Fri Apr 22 12:47:11 BST 2011
Process 14228 (domain.sh) has already taken the lock:
steve    14228 12786  0 12:47 pts/7    00:00:00 /bin/bash ./domain.sh example.com
Fri Apr 22 12:47:14 BST 2011
Process 14228 (domain.sh) has already taken the lock:
steve    14228 12786  0 12:47 pts/7    00:00:00 /bin/bash ./domain.sh example.com
Fri Apr 22 12:47:17 BST 2011
Process 14228 (domain.sh) has already taken the lock:
steve    14228 12786  0 12:47 pts/7    00:00:00 /bin/bash ./domain.sh example.com
Fri Apr 22 12:47:21 BST 2011
steve-parker.org Creation Date:
20-Jun-2000 13:48:46 UTC
steve-parker.org Expiration Date:
20-Jun-2011 13:48:46 UTC
steve-parker.org DNS Servers:
NS.123-REG.CO.UK
NS2.123-REG.CO.UK
... end of example.com information ...
Releasing lock.
Instance Two$
  • 查看输出文件
Instance One$ cat /tmp/domains.txt
example.com Creation Date:
      1992-01-01
example.com Expiration Date:
 13-aug-2011
example.com DNS Servers:
 A.IANA-SERVERS.NET
 B.IANA-SERVERS.NET
... end of example.com information ...
steve-parker.org Creation Date:
20-Jun-2000 13:48:46 UTC
steve-parker.org Expiration Date:
20-Jun-2011 13:48:46 UTC
steve-parker.org DNS Servers:
NS.123-REG.CO.UK
NS2.123-REG.CO.UK
... end of steve-parker.org information ...
Instance One$

从上述结果可以看出,使用锁机制后,输出文件的内容变得清晰有序,每个域名的信息都完整且独立。

锁机制总结

锁机制是确保代码实例能够独占资源的有效方法。一旦获取了锁,就可以进行诸如文件写入等操作,并且可以保证在该实例执行期间,其他进程不会干扰。在实际应用中,无论是否使用锁,第一个进程通常都不知道其他进程的存在,而使用锁的版本则能够确保对资源的独占访问。

基于 shell 脚本的太空游戏实现

在 shell 脚本中,除了实现系统管理功能,还可以开发一些有趣的游戏,比如下面要介绍的太空游戏。

游戏概述

这个太空游戏的灵感来源于经典的 70 年代街机游戏《太空侵略者》。游戏的目标是在外星人到达地球(屏幕底部)之前将其消灭。玩家可以使用 “a” 和 “l” 键左右移动飞船,按下 “f” 键发射炮弹,且每次只能发射一发炮弹。

使用的技术

该游戏使用了以下技术:
- kill trap SIGALRM 进行计时。
- 高级使用 read 实现对按键的及时响应。
- tput 控制终端。
- ANSI 颜色进行显示。
- 数组,特别是将数组传递给函数。
- 基本数学运算来计算位置和进行碰撞检测。

游戏概念

游戏背后的概念相对简单:
- 外星人移动 :外星人从左到右再返回移动,并且每次到达屏幕右侧时会向下移动一行。这通过不断增加 ceiling 变量来实现,外星人的位置由 (row*2) + ceiling 决定,乘以 2 是为了在每波外星人之间留出空白行。
- 激光炮 :激光炮由 cannonX cannonY 两个变量表示, cannonX 确保激光发射后保持垂直方向,且在发射后独立于飞船位置。在之前的炮弹击中外星人或到达屏幕顶部之前,不能发射新的炮弹。
- 数据结构 :飞船和激光炮的位置由简单的整数变量表示,每一行外星人由一个数组表示,该数组存储每个外星人被击中时的得分。当外星人被消灭时,数组中对应的值会变为 0,这表示该外星人已不存在,在绘制和碰撞检测时将不再考虑。
- 动画效果 :使用 aliens1 aliens2 两个数组以及取模 2 函数,使外星人看起来更具动画效果。

游戏实现中的问题及解决方法
  • 实时交互 :实时读取按键而无需用户按回车键以及定期重绘屏幕是 shell 脚本实现此类游戏的难点。 read -n 语法在较旧的 Unix 系统中不存在,但 GNU 系统和 Solaris 10 提供了该功能。外星人的实时更新通过 SIGALRM 信号实现,该信号每隔 $DELAY 秒唤醒脚本以刷新显示。
  • 睡眠问题 :脚本中的 sleep $DELAY 假设 sleep 可以接受非整数参数,但传统 Unix 的 sleep 最低只能睡眠 1 秒,这会使游戏在 Unix 系统上运行缓慢。可以通过每 X 次迭代睡眠一次来解决,随着 $DELAY 的逐渐减小,X 逐渐增大。
  • 数组传递问题 :在 bash 中,数组不能直接作为参数传递给函数,也不能作为返回值返回。可以通过调用函数时使用 ${a[@]} ,并处理 $@ 来解决,同时要注意引号的使用。例如:
$ cat func-array.sh
#!/bin/bash
a=( one “two three” four five )
function myfunc
{
  for value in “$@”
  do
    echo I was passed: $value
  done
}
myfunc “${a[@]}”
$ ./func-array.sh
I was passed: one
I was passed: two three
I was passed: four
I was passed: five
$
潜在问题及优化
  • 碰撞检测 :由于外星人的宽度大于一个单元格,碰撞检测是最难实现正确的部分。
  • 屏幕刷新 :过多的屏幕刷新会导致游戏出现过度闪烁,影响游戏体验。调用 clear 命令会花费较长时间,使显示变得非常闪烁。
  • 性能优化 :在编写脚本时,将取模函数从调用外部 expr 命令改为使用内置的 (( … %2 )) 构造,提高了改变外星人形状时的性能。将取模操作移出循环也能略微提高效率。

下面是游戏的结构流程图:

graph TD;
    A[开始] --> B[隐藏光标];
    B --> C[主循环];
    C --> D{按键判断};
    D -- "a或l" --> E[更新飞船位置并绘制];
    D -- "f" --> F{炮弹是否可用};
    F -- "是" --> G[设置炮弹位置];
    C --> H[定时调用move函数];
    H --> I[外星人移动];
    I --> J{到达边缘};
    J -- "是" --> K[改变方向并下移];
    I --> L[调用drawrow函数];
    L --> M{是否击中};
    M -- "是" --> N[更新数组];
    I --> O{外星人是否全灭};
    O -- "是" --> P[显示祝贺消息并退出];
    I --> Q[调用drawcannon函数];
    C --> R{游戏结束};
    R -- "是" --> S[显示光标并退出];

通过以上的介绍,我们了解了锁机制在脚本编程中的重要性以及如何在 shell 脚本中实现一个简单的太空游戏。这些技术不仅可以帮助我们解决实际的系统管理问题,还能让我们在脚本编程中发挥更多的创意。

脚本编程中的锁机制与太空游戏实现

太空游戏的详细结构与代码分析
游戏脚本的整体结构

该太空游戏脚本主要由四个核心函数和一个主循环构成,下面我们来详细分析其结构和功能。

  1. 主循环 :主循环位于脚本的底部,其主要功能是实时读取键盘输入。通过 read -n 1 语句读取单个字符,根据不同的按键进行相应的操作:
    • 如果按下 “a” 或 “l” 键,分别表示向左或向右移动飞船,此时会调用 drawship 函数更新飞船的位置并重新绘制。
    • 如果按下 “f” 键,且炮弹未在使用中( cannonY -eq 0 ),则设置炮弹的初始位置, cannonX 与飞船当前的 X 位置相同, cannonY 固定在屏幕底部。

以下是一个简单的示意代码,展示主循环的基本逻辑:

while true; do
    read -n 1 key
    case $key in
        a)
            # 向左移动飞船
            update_ship_left
            drawship
            ;;
        l)
            # 向右移动飞船
            update_ship_right
            drawship
            ;;
        f)
            if [ $cannonY -eq 0 ]; then
                cannonX=$shipX
                cannonY=$shipY
            fi
            ;;
    esac
done
  1. drawship 函数 :该函数用于绘制飞船。当主循环检测到按键移动飞船时会调用此函数,同时在 move 函数中也会被调用,以确保炮弹的更新能及时反映在屏幕上。
    • 首先,使用 printf 语句清空屏幕底部的整行。
    • 然后,根据炮弹是否处于发射状态,对飞船内的炮弹进行颜色编码,以提供实时的视觉反馈。

以下是 drawship 函数的简化示例:

function drawship {
    # 清空底部行
    printf "\e[${shipY};1H%s" "$(printf ' ' $(tput cols))"
    # 根据炮弹状态绘制飞船
    if [ $cannonY -eq 0 ]; then
        # 炮弹未发射
        printf "\e[${shipY};${shipX}H\e[32m<^>\e[0m"
    else
        # 炮弹已发射
        printf "\e[${shipY};${shipX}H\e[31m<^>\e[0m"
    fi
}
  1. move 函数 move 函数是游戏的核心逻辑之一,它使用 SIGALRM 信号实现定时调用自身。随着游戏的进行, DELAY 变量会逐渐减小,使得外星人的移动速度越来越快。
    • 每次调用时,外星人会按照当前方向移动一格。当到达屏幕边缘时,改变移动方向,并在到达右侧边缘时向下移动一行(通过增加 ceiling 变量实现)。
    • 然后,针对每一行外星人调用 drawrow 函数进行绘制,并处理碰撞检测。
    • 接着,统计剩余的外星人数量,如果全部消灭,则显示祝贺消息并退出游戏。
    • 最后,调用 drawcannon 函数更新炮弹的位置。

以下是 move 函数的简化逻辑:

function move {
    # 设置下一次定时调用
    trap "move" SIGALRM
    alarm $DELAY

    # 外星人移动
    if [ $direction -eq 1 ]; then
        # 向右移动
        if [ $alienX -ge $(tput cols) - $alienWidth ]; then
            direction=0
            ceiling=$((ceiling + 1))
        else
            alienX=$((alienX + 1))
        fi
    else
        # 向左移动
        if [ $alienX -le 1 ]; then
            direction=1
        else
            alienX=$((alienX - 1))
        fi
    fi

    # 绘制每一行外星人
    for ((i = 0; i < numRows; i++)); do
        result=$(drawrow $i)
        if [ $result -gt 0 ]; then
            row$i[$result]=0
        fi
    done

    # 统计剩余外星人数量
    remaining=0
    for ((i = 0; i < numRows; i++)); do
        for value in "${row$i[@]}"; do
            if [ $value -gt 0 ]; then
                remaining=$((remaining + 1))
            fi
        done
    done

    if [ $remaining -eq 0 ]; then
        echo "Congratulations! You have defeated all the aliens!"
        exit 0
    fi

    # 更新炮弹位置
    drawcannon
}
  1. drawrow 函数 drawrow 函数承担了大部分的绘制和碰撞检测工作。它接收一个参数,用于指定要绘制的外星人类型。
    • 在绘制外星人的过程中,检查是否有外星人与炮弹发生碰撞。如果发生碰撞,返回该外星人的索引;如果没有碰撞,返回 0。
    • 根据返回的索引,更新存储外星人得分的数组,将对应位置的值设为 0,表示该外星人已被消灭。

以下是 drawrow 函数的简化示例:

function drawrow {
    row=$1
    index=0
    for ((i = 0; i < ${#row$row[@]}; i++)); do
        if [ ${row$row[$i]} -gt 0 ]; then
            x=$((alienX + i * alienSpacing))
            y=$((row * 2 + ceiling))
            # 绘制外星人
            printf "\e[${y};${x}H${aliens1[$i]}"
            # 碰撞检测
            if [ $cannonX -eq $x ] && [ $cannonY -eq $y ]; then
                index=$((i + 1))
            fi
        fi
    done
    echo $index
}
  1. drawcannon 函数 :该函数用于更新炮弹的位置。首先,在炮弹的上一个位置绘制一个空格字符,然后计算新的位置(向上移动一格),并在新位置重新绘制炮弹。

以下是 drawcannon 函数的代码:

function drawcannon {
    if [ $cannonY -gt 0 ]; then
        # 清除上一个位置
        printf "\e[${cannonY};${cannonX}H "
        cannonY=$((cannonY - 1))
        if [ $cannonY -gt 0 ]; then
            # 绘制新位置
            printf "\e[${cannonY};${cannonX}H|\e[0m"
        else
            cannonY=0
        fi
    fi
}
游戏脚本的优化与改进方向

虽然当前的太空游戏脚本已经实现了基本的功能,但仍有一些可以优化和改进的地方:
1. 性能优化 :可以进一步优化碰撞检测的逻辑,减少不必要的计算。例如,只对可能发生碰撞的区域进行检测,而不是对整个屏幕进行遍历。
2. 游戏难度调整 :除了简单地加快外星人的移动速度,还可以增加外星人的种类和攻击方式,提高游戏的难度和趣味性。
3. 用户界面改进 :可以添加更多的视觉效果,如爆炸动画、得分显示等,提升用户体验。

总结

通过本文的介绍,我们深入了解了脚本编程中的锁机制和基于 shell 脚本的太空游戏实现。锁机制在多进程访问共享资源时起着至关重要的作用,能够确保数据的一致性和完整性。而在 shell 脚本中实现太空游戏,展示了脚本语言的强大功能和灵活性,不仅可以用于系统管理,还可以开发有趣的交互式应用。

在实际应用中,我们可以根据具体需求对锁机制和游戏脚本进行进一步的优化和扩展。同时,这些技术也为我们提供了一个思路,即在不同的场景中充分发挥脚本编程的优势,解决各种实际问题。

以下是游戏主要函数调用关系的表格总结:
| 函数名称 | 调用位置 | 功能描述 |
| ---- | ---- | ---- |
| drawship | 主循环、 move 函数 | 绘制飞船,根据炮弹状态进行颜色编码 |
| move | SIGALRM 信号触发 | 控制外星人移动,调用 drawrow drawcannon 函数 |
| drawrow | move 函数 | 绘制外星人并进行碰撞检测 |
| drawcannon | move 函数 | 更新炮弹位置 |

希望本文能为你在脚本编程和游戏开发方面提供一些有用的参考和启发。

源码地址: https://pan.quark.cn/s/d1f41682e390 miyoubiAuto 米游社每日米游币自动化Python脚本(务必使用Python3) 8更新:更换cookie的获取地址 注意:禁止在B站、贴吧、或各大论坛大肆传播! 作者已退游,项目不维护了。 如果有能力的可以pr修复。 小引一波 推荐关注几个非常可爱有趣的女孩! 欢迎B站搜索: @嘉然今天吃什么 @向晚大魔王 @乃琳Queen @贝拉kira 第三方库 食用方法 下载源码 在Global.py中设置米游社Cookie 运行myb.py 本地第一次运行时会自动生产一个文件储存cookie,请勿删除 当前仅支持单个账号! 获取Cookie方法 浏览器无痕模式打开 http://user.mihoyo.com/ ,登录账号 按,打开,找到并点击 按刷新页面,按下图复制 Cookie: How to get mys cookie 当触发时,可尝试按关闭,然后再次刷新页面,最后复制 Cookie。 也可以使用另一种方法: 复制代码 浏览器无痕模式打开 http://user.mihoyo.com/ ,登录账号 按,打开,找到并点击 控制台粘贴代码并运行,获得类似的输出信息 部分即为所需复制的 Cookie,点击确定复制 部署方法--腾讯云函数版(推荐! ) 下载项目源码和压缩包 进入项目文件夹打开命令行执行以下命令 xxxxxxx为通过上面方式或取得米游社cookie 一定要用双引号包裹!! 例如: png 复制返回内容(包括括号) 例如: QQ截图20210505031552.png 登录腾讯云函数官网 选择函数服务-新建-自定义创建 函数名称随意-地区随意-运行环境Python3....
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值