原文: http://zetcode.com/kotlin/snake/

Kotlin 贪食蛇游戏教程展示了如何使用 Swing 在 Kotlin 中创建蛇游戏。

源代码和图像可从作者的 Github Kotlin-Snake-Game 存储库中获得。

贪食蛇

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

Swing

Swing 是 Java 编程语言的主要 GUI 工具包。 它是 JFC(Java 基础类)的一部分,JFC 是用于为 Java 程序提供图形用户界面的 API。

Kotlin 贪食蛇游戏

蛇的每个关节的大小为 10 像素。 蛇由光标键控制。 最初,蛇具有三个关节。 如果游戏结束,则在面板中间显示"Game Over"消息。

Board.kt

  1. package com.zetcode
  2. import java.awt.*
  3. import java.awt.event.ActionEvent
  4. import java.awt.event.ActionListener
  5. import java.awt.event.KeyAdapter
  6. import java.awt.event.KeyEvent
  7. import javax.swing.ImageIcon
  8. import javax.swing.JPanel
  9. import javax.swing.Timer
  10. class Board : JPanel(), ActionListener {
  11. private val boardWidth = 300
  12. private val boardHeight = 300
  13. private val dotSize = 10
  14. private val allDots = 900
  15. private val randPos = 29
  16. private val delay = 140
  17. private val x = IntArray(allDots)
  18. private val y = IntArray(allDots)
  19. private var nOfDots: Int = 0
  20. private var appleX: Int = 0
  21. private var appleY: Int = 0
  22. private var leftDirection = false
  23. private var rightDirection = true
  24. private var upDirection = false
  25. private var downDirection = false
  26. private var inGame = true
  27. private var timer: Timer? = null
  28. private var ball: Image? = null
  29. private var apple: Image? = null
  30. private var head: Image? = null
  31. init {
  32. addKeyListener(TAdapter())
  33. background = Color.black
  34. isFocusable = true
  35. preferredSize = Dimension(boardWidth, boardHeight)
  36. loadImages()
  37. initGame()
  38. }
  39. private fun loadImages() {
  40. val iid = ImageIcon("src/main/resources/dot.png")
  41. ball = iid.image
  42. val iia = ImageIcon("src/main/resources/apple.png")
  43. apple = iia.image
  44. val iih = ImageIcon("src/main/resources/head.png")
  45. head = iih.image
  46. }
  47. private fun initGame() {
  48. nOfDots = 3
  49. for (z in 0 until nOfDots) {
  50. x[z] = 50 - z * 10
  51. y[z] = 50
  52. }
  53. locateApple()
  54. timer = Timer(delay, this)
  55. timer!!.start()
  56. }
  57. public override fun paintComponent(g: Graphics) {
  58. super.paintComponent(g)
  59. doDrawing(g)
  60. }
  61. private fun doDrawing(g: Graphics) {
  62. if (inGame) {
  63. g.drawImage(apple, appleX, appleY, this)
  64. for (z in 0 until nOfDots) {
  65. if (z == 0) {
  66. g.drawImage(head, x[z], y[z], this)
  67. } else {
  68. g.drawImage(ball, x[z], y[z], this)
  69. }
  70. }
  71. Toolkit.getDefaultToolkit().sync()
  72. } else {
  73. gameOver(g)
  74. }
  75. }
  76. private fun gameOver(g: Graphics) {
  77. val msg = "Game Over"
  78. val small = Font("Helvetica", Font.BOLD, 14)
  79. val fontMetrics = getFontMetrics(small)
  80. val rh = RenderingHints(
  81. RenderingHints.KEY_ANTIALIASING,
  82. RenderingHints.VALUE_ANTIALIAS_ON)
  83. rh[RenderingHints.KEY_RENDERING] = RenderingHints.VALUE_RENDER_QUALITY
  84. (g as Graphics2D).setRenderingHints(rh)
  85. g.color = Color.white
  86. g.font = small
  87. g.drawString(msg, (boardWidth - fontMetrics.stringWidth(msg)) / 2,
  88. boardHeight / 2)
  89. }
  90. private fun checkApple() {
  91. if (x[0] == appleX && y[0] == appleY) {
  92. nOfDots++
  93. locateApple()
  94. }
  95. }
  96. private fun move() {
  97. for (z in nOfDots downTo 1) {
  98. x[z] = x[z - 1]
  99. y[z] = y[z - 1]
  100. }
  101. if (leftDirection) {
  102. x[0] -= dotSize
  103. }
  104. if (rightDirection) {
  105. x[0] += dotSize
  106. }
  107. if (upDirection) {
  108. y[0] -= dotSize
  109. }
  110. if (downDirection) {
  111. y[0] += dotSize
  112. }
  113. }
  114. private fun checkCollision() {
  115. for (z in nOfDots downTo 1) {
  116. if (z > 4 && x[0] == x[z] && y[0] == y[z]) {
  117. inGame = false
  118. }
  119. }
  120. if (y[0] >= boardHeight) {
  121. inGame = false
  122. }
  123. if (y[0] < 0) {
  124. inGame = false
  125. }
  126. if (x[0] >= boardWidth) {
  127. inGame = false
  128. }
  129. if (x[0] < 0) {
  130. inGame = false
  131. }
  132. if (!inGame) {
  133. timer!!.stop()
  134. }
  135. }
  136. private fun locateApple() {
  137. var r = (Math.random() * randPos).toInt()
  138. appleX = r * dotSize
  139. r = (Math.random() * randPos).toInt()
  140. appleY = r * dotSize
  141. }
  142. override fun actionPerformed(e: ActionEvent) {
  143. if (inGame) {
  144. checkApple()
  145. checkCollision()
  146. move()
  147. }
  148. repaint()
  149. }
  150. private inner class TAdapter : KeyAdapter() {
  151. override fun keyPressed(e: KeyEvent?) {
  152. val key = e!!.keyCode
  153. if (key == KeyEvent.VK_LEFT && !rightDirection) {
  154. leftDirection = true
  155. upDirection = false
  156. downDirection = false
  157. }
  158. if (key == KeyEvent.VK_RIGHT && !leftDirection) {
  159. rightDirection = true
  160. upDirection = false
  161. downDirection = false
  162. }
  163. if (key == KeyEvent.VK_UP && !downDirection) {
  164. upDirection = true
  165. rightDirection = false
  166. leftDirection = false
  167. }
  168. if (key == KeyEvent.VK_DOWN && !upDirection) {
  169. downDirection = true
  170. rightDirection = false
  171. leftDirection = false
  172. }
  173. }
  174. }
  175. }

首先,我们将定义游戏中使用的属性。

  1. private val boardWidth = 300
  2. private val boardHeight = 300
  3. private val dotSize = 10
  4. private val allDots = 900
  5. private val randPos = 29
  6. private val delay = 140

boardWidthboardHeight属性确定电路板的大小。 dotSize是苹果的大小和蛇的点。 allDots属性定义了板上可能的最大点数(900 = (300 * 300) / (10 * 10))。 randPos属性用于计算苹果的随机位置。 delay属性确定游戏的速度。

  1. private val x = IntArray(allDots)
  2. private val y = IntArray(allDots)

这两个数组存储蛇的所有关节的xy坐标。

  1. private fun loadImages() {
  2. val iid = ImageIcon("src/main/resources/dot.png")
  3. ball = iid.image
  4. val iia = ImageIcon("src/main/resources/apple.png")
  5. apple = iia.image
  6. val iih = ImageIcon("src/main/resources/head.png")
  7. head = iih.image
  8. }

loadImages()方法中,我们获得了游戏的图像。 ImageIcon类用于显示 PNG 图像。

  1. private fun initGame() {
  2. nOfDots = 3
  3. for (z in 0 until nOfDots) {
  4. x[z] = 50 - z * 10
  5. y[z] = 50
  6. }
  7. locateApple()
  8. timer = Timer(delay, this)
  9. timer!!.start()
  10. }

initGame()方法中,我们创建蛇,在板上随机放置一个苹果,然后启动计时器。

  1. private fun doDrawing(g: Graphics) {
  2. if (inGame) {
  3. g.drawImage(apple, appleX, appleY, this)
  4. for (z in 0 until nOfDots) {
  5. if (z == 0) {
  6. g.drawImage(head, x[z], y[z], this)
  7. } else {
  8. g.drawImage(ball, x[z], y[z], this)
  9. }
  10. }
  11. Toolkit.getDefaultToolkit().sync()
  12. } else {
  13. gameOver(g)
  14. }
  15. }

doDrawing()方法中,我们绘制了苹果和蛇对象。 如果游戏结束,我们将根据消息绘制游戏。

  1. private fun gameOver(g: Graphics) {
  2. val msg = "Game Over"
  3. val small = Font("Helvetica", Font.BOLD, 14)
  4. val fontMetrics = getFontMetrics(small)
  5. val rh = RenderingHints(
  6. RenderingHints.KEY_ANTIALIASING,
  7. RenderingHints.VALUE_ANTIALIAS_ON)
  8. rh[RenderingHints.KEY_RENDERING] = RenderingHints.VALUE_RENDER_QUALITY
  9. (g as Graphics2D).setRenderingHints(rh)
  10. g.color = Color.white
  11. g.font = small
  12. g.drawString(msg, (boardWidth - fontMetrics.stringWidth(msg)) / 2,
  13. boardHeight / 2)
  14. }

gameOver()方法在窗口中间绘制"Game Over"消息。 我们使用渲染提示来平滑地绘制消息。 我们使用字体指标来获取消息的大小。

  1. private fun checkApple() {
  2. if (x[0] == appleX && y[0] == appleY) {
  3. nOfDots++
  4. locateApple()
  5. }
  6. }

如果苹果与头部碰撞,我们会增加蛇的关节数。 我们称locateApple()方法为随机放置一个新的Apple对象。

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

  1. for (z in nOfDots downTo 1) {
  2. x[z] = x[z - 1]
  3. y[z] = y[z - 1]
  4. }

该代码将关节向上移动。

  1. if (leftDirection) {
  2. x[0] -= dotSize
  3. }

这条线将头向左移动。

checkCollision()方法中,我们确定蛇是否击中了自己或撞墙之一。

  1. for (z in nOfDots downTo 1) {
  2. if (z > 4 && x[0] == x[z] && y[0] == y[z]) {
  3. inGame = false
  4. }
  5. }

如果蛇用头撞到其关节之一,则游戏结束。

  1. if (y[0] >= boardHeight) {
  2. inGame = false
  3. }

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

Snake.kt

  1. package com.zetcode
  2. import java.awt.EventQueue
  3. import javax.swing.JFrame
  4. class Snake : JFrame() {
  5. init {
  6. initUI()
  7. }
  8. private fun initUI() {
  9. add(Board())
  10. title = "Snake"
  11. isResizable = false
  12. pack()
  13. setLocationRelativeTo(null)
  14. defaultCloseOperation = JFrame.EXIT_ON_CLOSE
  15. }
  16. companion object {
  17. @JvmStatic
  18. fun main(args: Array<String>) {
  19. EventQueue.invokeLater {
  20. val ex = Snake()
  21. ex.isVisible = true
  22. }
  23. }
  24. }
  25. }

这是主要的类。

  1. isResizable = false
  2. pack()

isResizable属性会影响某些平台上JFrame容器的插入。 因此,在pack()方法之前调用它很重要。 否则,蛇的头部与右边界和底边界的碰撞可能无法正常进行。

Kotlin 贪食蛇 - 图1

图:贪食蛇

这是 Kotlin 和 Swing 中的贪食蛇游戏。 您可能也对相关教程感兴趣: Kotlin Swing 教程Kotlin 读取文件教程Kotlin 写入文件教程