贪吃蛇
最后修改于 2023 年 10 月 18 日
在本 Tcl/Tk 教程中,我们将创建一个贪吃蛇游戏克隆。
贪吃蛇 是一款经典的旧视频游戏。它最初创建于 70 年代后期。后来被移植到 PC 上。在这个游戏中,玩家控制一条蛇。目标是吃掉尽可能多的苹果。每次蛇吃掉一个苹果,它的身体就会变长。蛇必须避开墙壁和自己的身体。
开发
贪吃蛇每个关节的大小为 10px。蛇由光标键控制。最初,蛇有三个关节。游戏立即开始。当游戏结束时,我们会在窗口的中心显示“游戏结束”消息。
我们使用 canvas
小部件来创建游戏。游戏中的对象是图像。我们使用 canvas 命令创建图像项。我们使用 canvas 命令使用标签查找 canvas 上的项目,并进行碰撞检测。
#!/usr/bin/wish # ZetCode Tcl/Tk tutorial # # This is simple Nibbles game clone. # # Author: Jan Bodnar # Website: www.zetcode.com package require Img set WIDTH 300 set HEIGHT 300 set DELAY 100 set DOT_SIZE 10 set ALL_DOTS [expr $WIDTH * $HEIGHT / ($DOT_SIZE * $DOT_SIZE)] set RAND_POS 27 canvas .c -width $WIDTH -height $HEIGHT -background black pack .c proc initGame {} { set ::left false set ::right true set ::up false set ::down false set ::inGame true set dots 3 set ::apple_x 100 set ::apple_y 190 for {set i 0} {$i<$dots} {incr i} { set x($i) [expr 50 - $i * 10] set y($i) 50 } set ::idot [image create photo img1 -file "dot.png"] set ::ihead [image create photo img2 -file "head.png"] set ::iapple [image create photo img3 -file "apple.png"] createObjects locateApple bind . "<Key>" "onKeyPressed %K" after $::DELAY onTimer } proc createObjects {} { .c create image $::apple_x $::apple_y \ -image $::iapple -tag apple -anchor nw .c create image 50 50 -image $::ihead -tag head -anchor nw .c create image 30 50 -image $::idot -tag dot -anchor nw .c create image 40 50 -image $::idot -tag dot -anchor nw } proc checkApple {} { set apple [.c find withtag apple] set head [.c find withtag head] set l [.c bbox head] set overlap [eval .c find overlapping $l] foreach over $overlap { if {$over == $apple} { set crd [.c coords $apple] set x [lindex $crd 0] set y [lindex $crd 1] .c create image $x $y -image $::idot -anchor nw -tag dot locateApple } } } proc doMove {} { set dots [.c find withtag dot] set head [.c find withtag head] set items [concat $dots $head] set z 0 while {$z < [expr [llength $items] - 1]} { set c1 [.c coords [lindex $items $z]] set c2 [.c coords [lindex $items [expr $z+1]]] .c move [lindex $items $z] [expr [lindex $c2 0] - [lindex $c1 0] ] \ [expr [lindex $c2 1] - [lindex $c1 1] ] incr z } if { [string compare $::left true] == 0} { .c move head -$::DOT_SIZE 0 } if {[string compare $::right true] == 0} { .c move head $::DOT_SIZE 0 } if {[string compare $::up true] == 0} { .c move head 0 -$::DOT_SIZE } if {[string compare $::down true] == 0} { .c move head 0 $::DOT_SIZE } } proc checkCollisions {} { set dots [.c find withtag dot] set head [.c find withtag head] set l [.c bbox head] set overlap [eval .c find overlapping $l] foreach dot $dots { foreach over $overlap { if {$over == $dot} { set ::inGame false } } } set x1 [lindex $l 0] set y1 [lindex $l 1] if {$x1 < 0} { set ::inGame false } if {$x1 > [expr $::WIDTH - $::DOT_SIZE]} { set ::inGame false } if {$y1 < 0} { set ::inGame false } if {$y1 > [expr $::HEIGHT - $::DOT_SIZE]} { set ::inGame false } } proc locateApple {} { set apple [.c find withtag apple] .c delete lindex apple 0 set r [expr round(rand() * $::RAND_POS)] set ::apple_x [expr $r * $::DOT_SIZE] set r [expr round(rand() * $::RAND_POS)] set ::apple_y [expr $r * $::DOT_SIZE] .c create image $::apple_x $::apple_y -anchor nw \ -image $::iapple -tag apple } proc onKeyPressed {key} { set a1 [ expr [string compare $key Left] == 0] set a2 [ expr [string compare $::right true] != 0] if { $a1 && $a2 } { set ::left true set ::up false set ::down false } set b1 [ expr [string compare $key Right] == 0] set b2 [ expr [string compare $::left true] != 0] if { $b1 && $b2 } { set ::right true set ::up false set ::down false } set c1 [ expr [string compare $key Up] == 0] set c2 [ expr [string compare $::down true] != 0] if { $c1 && $c2 } { set ::up true set ::left false set ::right false } set d1 [ expr [string compare $key Down] == 0] set d2 [ expr [string compare $::up true] != 0] if { $d1 && $d2 } { set ::down true set ::left false set ::right false } } proc onTimer {} { if {$::inGame} { checkCollisions checkApple doMove after $::DELAY onTimer } else { gameOver } } proc gameOver {} { .c delete all set x [ expr [winfo width .] / 2 ] set y [ expr [winfo height .] / 2] .c create text $x $y -text "Game over" -fill white } initGame wm title . "Nibbles" wm geometry . +150+150
首先,我们定义游戏中使用的一些常量。
WIDTH
和 HEIGHT
常量确定 Board 的大小。DELAY
常量决定游戏的速度。DOT_SIZE
是苹果和蛇的点的尺寸。ALL_DOTS
常量定义了 Board 上可能存在的最大点数。RAND_POS
常量用于计算苹果的随机位置。
initGame
过程初始化变量,加载图像并启动超时过程。
set ::idot [image create photo img1 -file "dot.png"] set ::ihead [image create photo img2 -file "head.png"] set ::iapple [image create photo img3 -file "apple.png"]
在这些行中,我们加载我们的图像。贪吃蛇游戏中有三个图像。头部、点和苹果。
createObjects locateApple
createObjects
过程在 canvas 上创建项目。locateApple
将一个苹果随机放置在 canvas 上。
bind . "<Key>" "onKeyPressed %K"
我们将键盘事件绑定到 onKeyPressed
过程。游戏由键盘光标键控制。%K
是 Tk 中按键的符号名称。它被传递给 onKeyPressed
过程。
proc createObjects {} { .c create image $::apple_x $::apple_y \ -image $::iapple -tag apple -anchor nw .c create image 50 50 -image $::ihead -tag head -anchor nw .c create image 30 50 -image $::idot -tag dot -anchor nw .c create image 40 50 -image $::idot -tag dot -anchor nw }
在 createObjects 过程中,我们在 canvas 上创建游戏对象。这些是 canvas 项目。它们被赋予初始的 x、y 坐标。-image
选项提供了要显示的图像。-anchor 选项设置为 nw; 这样,canvas 项目的坐标就是项目的左上点。如果我们想能够在根窗口的边框旁边显示图像,这一点很重要。如果您不理解我们的意思,请尝试删除 anchor 选项。-tag
选项用于标识 canvas 上的项目。一个标签可用于多个 canvas 项目。
checkApple
过程检查蛇是否击中了苹果对象。如果是,我们添加另一个蛇关节并调用 locateApple
。
set apple [.c find withtag apple] set head [.c find withtag head]
find withtag
命令使用其标签在 canvas 上查找项目。我们需要两个项目。蛇的头和苹果。
set l [.c bbox head] set overlap [eval .c find overlapping $l]
bbox
命令返回项目的边界框点。find overlapping
命令查找给定坐标的碰撞项目。
foreach over $overlap { if {$over == $apple} { set crd [.c coords $apple] set x [lindex $crd 0] set y [lindex $crd 1] .c create image $x $y -image $::idot -anchor nw -tag dot locateApple } }
如果苹果与头部碰撞,我们将在苹果对象的坐标处创建一个新的点项目。我们调用 locateApple
过程,该过程从 canvas 中删除旧的苹果项目,并创建一个随机定位的新苹果项目。
在 doMove
过程中,我们有游戏的关键算法。要理解它,请查看蛇是如何移动的。你控制蛇的头。你可以用光标键改变它的方向。其余的关节向上移动一个位置。第二个关节移动到第一个关节的位置,第三个关节移动到第二个关节的位置,等等。
set z 0 while {$z < [expr [llength $items] - 1]} { set c1 [.c coords [lindex $items $z]] set c2 [.c coords [lindex $items [expr $z+1]]] .c move [lindex $items $z] [expr [lindex $c2 0] - [lindex $c1 0] ] \ [expr [lindex $c2 1] - [lindex $c1 1] ] incr z }
此代码沿链移动关节。
if { [string compare $::left true] == 0} { .c move head -$::DOT_SIZE 0 }
将头向左移动。
在 checkCollisions
过程中,我们确定蛇是否击中了自己的身体或墙壁之一。
set l [.c bbox head] set overlap [eval .c find overlapping $l] foreach dot $dots { foreach over $overlap { if {$over == $dot} { set ::inGame false } } }
如果蛇用头撞到自己的关节之一,我们结束游戏。
if {$y1 > [expr $::HEIGHT - $::DOT_SIZE]} { set ::inGame false }
如果蛇撞到 Board 的底部,我们结束游戏。
locateApple
过程在板上随机定位一个新的苹果并删除旧的苹果。
set apple [.c find withtag apple] .c delete lindex apple 0
在这里,我们找到并删除被蛇吃掉的苹果。
set r [expr round(rand() * $::RAND_POS)]
我们得到一个从 0 到 RAND_POS - 1 的随机数。
set ::apple_x [expr $r * $::DOT_SIZE] ... set ::apple_y [expr $r * $::DOT_SIZE]
这些行设置苹果对象的 x、y 坐标。
在 onKeyPressed
过程中,我们确定按下的键。
set a1 [ expr [string compare $key Left] == 0] set a2 [ expr [string compare $::right true] != 0] if { $a1 && $a2 } { set ::left true set ::up false set ::down false }
如果我们点击左光标键,我们将 left 变量设置为 true。此变量用于 doMove
过程中更改蛇对象的坐标。另请注意,当蛇向右移动时,我们不能立即向左转。
proc onTimer {} { if {$::inGame} { checkCollisions checkApple doMove after $::DELAY onTimer } else { gameOver } }
每 DELAY
毫秒,都会调用 onTimer 过程。如果我们在游戏中,我们调用三个构建游戏逻辑的过程。否则游戏结束。计时器基于 after 命令,该命令仅在 DELAY
毫秒后调用一个过程。为了重复调用计时器,我们递归地调用 onTimer
过程。
proc gameOver {} { .c delete all set x [ expr [winfo width .] / 2 ] set y [ expr [winfo height .] / 2] .c create text $x $y -text "Game over" -fill white }
如果游戏结束,我们删除 canvas 上的所有项目。然后我们在屏幕中心绘制“游戏结束”。

这是用 Tcl/Tk 创建的贪吃蛇电脑游戏。