原文: http://zetcode.com/gui/tcltktutorial/nibbles/

在 Tcl/Tk 教程的这一部分中,我们将创建一个贪食蛇游戏克隆。

贪食蛇是较旧的经典视频游戏。 它最初是在 70 年代后期创建的。 后来它被带到 PC 上。 在这个游戏中,玩家控制蛇。 目的是尽可能多地吃苹果。 蛇每次吃一个苹果,它的身体就会长大。 蛇必须避开墙壁和自己的身体。

开发

蛇的每个关节的大小为 10px。 蛇由光标键控制。 最初,蛇具有三个关节。 游戏立即开始。 游戏结束后,我们在窗口中心显示"Game Over"消息。

我们使用canvas小部件来创建游戏。 游戏中的对象是图像。 我们使用画布命令创建图像项。 我们使用画布命令使用标签在画布上查找项目并进行碰撞检测。

  1. #!/usr/bin/wish
  2. # ZetCode Tcl/Tk tutorial
  3. #
  4. # This is simple Nibbles game clone.
  5. #
  6. # author: Jan Bodnar
  7. # last modified: March 2011
  8. # website: www.zetcode.com
  9. package require Img
  10. set WIDTH 300
  11. set HEIGHT 300
  12. set DELAY 100
  13. set DOT_SIZE 10
  14. set ALL_DOTS [expr $WIDTH * $HEIGHT / ($DOT_SIZE * $DOT_SIZE)]
  15. set RAND_POS 27
  16. canvas .c -width $WIDTH -height $HEIGHT -background black
  17. pack .c
  18. proc initGame {} {
  19. set ::left false
  20. set ::right true
  21. set ::up false
  22. set ::down false
  23. set ::inGame true
  24. set dots 3
  25. set ::apple_x 100
  26. set ::apple_y 190
  27. for {set i 0} {$i<$dots} {incr i} {
  28. set x($i) [expr 50 - $i * 10]
  29. set y($i) 50
  30. }
  31. set ::idot [image create photo img1 -file "dot.png"]
  32. set ::ihead [image create photo img2 -file "head.png"]
  33. set ::iapple [image create photo img3 -file "apple.png"]
  34. createObjects
  35. locateApple
  36. bind . "<Key>" "onKeyPressed %K"
  37. after $::DELAY onTimer
  38. }
  39. proc createObjects {} {
  40. .c create image $::apple_x $::apple_y \
  41. -image $::iapple -tag apple -anchor nw
  42. .c create image 50 50 -image $::ihead -tag head -anchor nw
  43. .c create image 30 50 -image $::idot -tag dot -anchor nw
  44. .c create image 40 50 -image $::idot -tag dot -anchor nw
  45. }
  46. proc checkApple {} {
  47. set apple [.c find withtag apple]
  48. set head [.c find withtag head]
  49. set l [.c bbox head]
  50. set overlap [eval .c find overlapping $l]
  51. foreach over $overlap {
  52. if {$over == $apple} {
  53. set crd [.c coords $apple]
  54. set x [lindex $crd 0]
  55. set y [lindex $crd 1]
  56. .c create image $x $y -image $::idot -anchor nw -tag dot
  57. locateApple
  58. }
  59. }
  60. }
  61. proc doMove {} {
  62. set dots [.c find withtag dot]
  63. set head [.c find withtag head]
  64. set items [concat $dots $head]
  65. set z 0
  66. while {$z < [expr [llength $items] - 1]} {
  67. set c1 [.c coords [lindex $items $z]]
  68. set c2 [.c coords [lindex $items [expr $z+1]]]
  69. .c move [lindex $items $z] [expr [lindex $c2 0] - [lindex $c1 0] ] \
  70. [expr [lindex $c2 1] - [lindex $c1 1] ]
  71. incr z
  72. }
  73. if { [string compare $::left true] == 0} {
  74. .c move head -$::DOT_SIZE 0
  75. }
  76. if {[string compare $::right true] == 0} {
  77. .c move head $::DOT_SIZE 0
  78. }
  79. if {[string compare $::up true] == 0} {
  80. .c move head 0 -$::DOT_SIZE
  81. }
  82. if {[string compare $::down true] == 0} {
  83. .c move head 0 $::DOT_SIZE
  84. }
  85. }
  86. proc checkCollisions {} {
  87. set dots [.c find withtag dot]
  88. set head [.c find withtag head]
  89. set l [.c bbox head]
  90. set overlap [eval .c find overlapping $l]
  91. foreach dot $dots {
  92. foreach over $overlap {
  93. if {$over == $dot} {
  94. set ::inGame false
  95. }
  96. }
  97. }
  98. set x1 [lindex $l 0]
  99. set y1 [lindex $l 1]
  100. if {$x1 < 0} {
  101. set ::inGame false
  102. }
  103. if {$x1 > [expr $::WIDTH - $::DOT_SIZE]} {
  104. set ::inGame false
  105. }
  106. if {$y1 < 0} {
  107. set ::inGame false
  108. }
  109. if {$y1 > [expr $::HEIGHT - $::DOT_SIZE]} {
  110. set ::inGame false
  111. }
  112. }
  113. proc locateApple {} {
  114. set apple [.c find withtag apple]
  115. .c delete lindex apple 0
  116. set r [expr round(rand() * $::RAND_POS)]
  117. set ::apple_x [expr $r * $::DOT_SIZE]
  118. set r [expr round(rand() * $::RAND_POS)]
  119. set ::apple_y [expr $r * $::DOT_SIZE]
  120. .c create image $::apple_x $::apple_y -anchor nw \
  121. -image $::iapple -tag apple
  122. }
  123. proc onKeyPressed {key} {
  124. set a1 [ expr [string compare $key Left] == 0]
  125. set a2 [ expr [string compare $::right true] != 0]
  126. if { $a1 && $a2 } {
  127. set ::left true
  128. set ::up false
  129. set ::down false
  130. }
  131. set b1 [ expr [string compare $key Right] == 0]
  132. set b2 [ expr [string compare $::left true] != 0]
  133. if { $b1 && $b2 } {
  134. set ::right true
  135. set ::up false
  136. set ::down false
  137. }
  138. set c1 [ expr [string compare $key Up] == 0]
  139. set c2 [ expr [string compare $::down true] != 0]
  140. if { $c1 && $c2 } {
  141. set ::up true
  142. set ::left false
  143. set ::right false
  144. }
  145. set d1 [ expr [string compare $key Down] == 0]
  146. set d2 [ expr [string compare $::up true] != 0]
  147. if { $d1 && $d2 } {
  148. set ::down true
  149. set ::left false
  150. set ::right false
  151. }
  152. }
  153. proc onTimer {} {
  154. if {$::inGame} {
  155. checkCollisions
  156. checkApple
  157. doMove
  158. after $::DELAY onTimer
  159. } else {
  160. gameOver
  161. }
  162. }
  163. proc gameOver {} {
  164. .c delete all
  165. set x [ expr [winfo width .] / 2 ]
  166. set y [ expr [winfo height .] / 2]
  167. .c create text $x $y -text "Game over" -fill white
  168. }
  169. initGame
  170. wm title . "Nibbles"
  171. wm geometry . +150+150

首先,我们将定义一些在游戏中使用的常量。

WIDTHHEIGHT常数确定电路板的大小。 DELAY常数确定游戏的速度。 DOT_SIZE是苹果的大小和蛇的点。 ALL_DOTS常数定义了板上可能的最大点数。 RAND_POS常数用于计算苹果的随机位置。

initGame过程初始化变量,加载图像并启动超时过程。

  1. set ::idot [image create photo img1 -file "dot.png"]
  2. set ::ihead [image create photo img2 -file "head.png"]
  3. set ::iapple [image create photo img3 -file "apple.png"]

在这些行中,我们加载图像。 贪食蛇游戏中有三个图像。 头,圆点和苹果。

  1. createObjects
  2. locateApple

createObjects过程在画布上创建项目。 locateApple在画布上随机放置一个苹果。

  1. bind . "<Key>" "onKeyPressed %K"

我们将键盘事件绑定到onKeyPressed过程。 游戏由键盘光标键控制。 %K是所按下键的 Tk 符号名称。 它被传递到onKeyPressed过程。

  1. proc createObjects {} {
  2. .c create image $::apple_x $::apple_y \
  3. -image $::iapple -tag apple -anchor nw
  4. .c create image 50 50 -image $::ihead -tag head -anchor nw
  5. .c create image 30 50 -image $::idot -tag dot -anchor nw
  6. .c create image 40 50 -image $::idot -tag dot -anchor nw
  7. }

createObjects过程中,我们在画布上创建游戏对象。 这些是帆布物品。 它们被赋予初始的 x,y 坐标。 -image选项提供要显示的图像。 -anchor选项设置为nw; 这样,画布项目的坐标就是项目的左上角。 如果我们希望能够在根窗口的边框旁边显示图像,这很重要。 如果您不理解我们的意思,请尝试删除锚点选项。 -tag选项用于识别画布上的项目。 一个标签可用于多个画布项目。

checkApple过程检查蛇是否击中了苹果对象。 如果是这样,我们添加另一个蛇形接头并称为locateApple

  1. set apple [.c find withtag apple]
  2. set head [.c find withtag head]

find withtag命令使用其标签在画布上找到一个项目。 我们需要两个项目。 蛇和苹果的头。

  1. set l [.c bbox head]
  2. set overlap [eval .c find overlapping $l]

bbox命令返回项目的边界框点。 find overlapping命令查找给定坐标的冲突项。

  1. foreach over $overlap {
  2. if {$over == $apple} {
  3. set crd [.c coords $apple]
  4. set x [lindex $crd 0]
  5. set y [lindex $crd 1]
  6. .c create image $x $y -image $::idot -anchor nw -tag dot
  7. locateApple
  8. }
  9. }

如果苹果与头部碰撞,我们将在苹果对象的坐标处创建一个新的点项目。 我们调用locateApple过程,该过程将从画布上删除旧的苹果项目,然后创建并随机放置一个新的项目。

doMove过程中,我们有了游戏的关键算法。 要了解它,请看一下蛇是如何运动的。 您控制蛇的头。 您可以使用光标键更改其方向。 其余关节在链上向上移动一个位置。 第二关节移动到第一个关节的位置,第三关节移动到第二个关节的位置,依此类推。

  1. set z 0
  2. while {$z < [expr [llength $items] - 1]} {
  3. set c1 [.c coords [lindex $items $z]]
  4. set c2 [.c coords [lindex $items [expr $z+1]]]
  5. .c move [lindex $items $z] [expr [lindex $c2 0] - [lindex $c1 0] ] \
  6. [expr [lindex $c2 1] - [lindex $c1 1] ]
  7. incr z
  8. }

该代码将关节向上移动。

  1. if { [string compare $::left true] == 0} {
  2. .c move head -$::DOT_SIZE 0
  3. }

将头向左移动。

checkCollisions程序中,我们确定蛇是否击中了自己或撞墙之一。

  1. set l [.c bbox head]
  2. set overlap [eval .c find overlapping $l]
  3. foreach dot $dots {
  4. foreach over $overlap {
  5. if {$over == $dot} {
  6. set ::inGame false
  7. }
  8. }
  9. }

如果蛇用头撞到关节之一,我们就结束游戏。

  1. if {$y1 > [expr $::HEIGHT - $::DOT_SIZE]} {
  2. set ::inGame false
  3. }

如果蛇击中了棋盘的底部,我们将结束游戏。

locateApple过程会在板上随机找到一个新苹果,然后删除旧的苹果。

  1. set apple [.c find withtag apple]
  2. .c delete lindex apple 0

在这里,我们找到并删除了被蛇吃掉的苹果。

  1. set r [expr round(rand() * $::RAND_POS)]

我们得到一个从 0 到RAND_POS-1的随机数。

  1. set ::apple_x [expr $r * $::DOT_SIZE]
  2. ...
  3. set ::apple_y [expr $r * $::DOT_SIZE]

这些行设置了apple对象的 x,y 坐标。

onKeyPressed程序中,我们确定所按下的键。

  1. set a1 [ expr [string compare $key Left] == 0]
  2. set a2 [ expr [string compare $::right true] != 0]
  3. if { $a1 && $a2 } {
  4. set ::left true
  5. set ::up false
  6. set ::down false
  7. }

如果按左光标键,则将left变量设置为true。 在doMove过程中使用此变量来更改蛇对象的坐标。 还要注意,当蛇向右行驶时,我们不能立即向左转。

  1. proc onTimer {} {
  2. if {$::inGame} {
  3. checkCollisions
  4. checkApple
  5. doMove
  6. after $::DELAY onTimer
  7. } else {
  8. gameOver
  9. }
  10. }

DELAY ms,将调用onTimer过程。 如果我们参与了游戏,我们将调用三个构建游戏逻辑的过程。 否则,游戏结束。 计时器基于after命令,该命令仅在DELAY ms 之后调用一次过程。 要重复调用计时器,我们递归调用onTimer过程。

  1. proc gameOver {} {
  2. .c delete all
  3. set x [ expr [winfo width .] / 2 ]
  4. set y [ expr [winfo height .] / 2]
  5. .c create text $x $y -text "Game over" -fill white
  6. }

如果游戏结束,我们将删除画布上的所有项目。 然后,在屏幕中央绘制"Game Over"

贪食蛇 - 图1

图:贪食蛇

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