Kotlin 贪吃蛇
最后修改于 2024 年 1 月 29 日
本文介绍了如何使用 Swing 在 Kotlin 中创建贪吃蛇游戏。
源代码和图像可在作者的 Github 存储库 Kotlin-Snake-Game 中找到。
贪吃蛇
贪吃蛇 是一款较早的经典电子游戏。 它于 70 年代后期首次创建。 后来被移植到个人电脑上。 在这款游戏中,玩家控制一条蛇。 游戏的目标是吃掉尽可能多的苹果。 当蛇吃掉一个苹果时,它的身体就会生长。 蛇必须避开墙壁和它自己的身体。 这个游戏有时被称为 Nibbles(贪吃)。
Swing
Swing 是 Java 编程语言的主要 GUI 工具包。 它是 JFC (Java Foundation Classes) 的一部分,JFC 是一个用于为 Java 程序提供图形用户界面的 API。
Kotlin 贪吃蛇游戏
蛇的每个关节的大小是 10 像素。蛇用光标键控制。最初,蛇有三个关节。如果游戏结束,"Game Over" 消息将显示在游戏板的中间。
package com.zetcode import java.awt.* import java.awt.event.ActionEvent import java.awt.event.ActionListener import java.awt.event.KeyAdapter import java.awt.event.KeyEvent import javax.swing.ImageIcon import javax.swing.JPanel import javax.swing.Timer class Board : JPanel(), ActionListener { private val boardWidth = 300 private val boardHeight = 300 private val dotSize = 10 private val allDots = 900 private val randPos = 29 private val delay = 140 private val x = IntArray(allDots) private val y = IntArray(allDots) private var nOfDots: Int = 0 private var appleX: Int = 0 private var appleY: Int = 0 private var leftDirection = false private var rightDirection = true private var upDirection = false private var downDirection = false private var inGame = true private var timer: Timer? = null private var ball: Image? = null private var apple: Image? = null private var head: Image? = null init { addKeyListener(TAdapter()) background = Color.black isFocusable = true preferredSize = Dimension(boardWidth, boardHeight) loadImages() initGame() } private fun loadImages() { val iid = ImageIcon("src/main/resources/dot.png") ball = iid.image val iia = ImageIcon("src/main/resources/apple.png") apple = iia.image val iih = ImageIcon("src/main/resources/head.png") head = iih.image } private fun initGame() { nOfDots = 3 for (z in 0 until nOfDots) { x[z] = 50 - z * 10 y[z] = 50 } locateApple() timer = Timer(delay, this) timer!!.start() } public override fun paintComponent(g: Graphics) { super.paintComponent(g) doDrawing(g) } private fun doDrawing(g: Graphics) { if (inGame) { g.drawImage(apple, appleX, appleY, this) for (z in 0 until nOfDots) { if (z == 0) { g.drawImage(head, x[z], y[z], this) } else { g.drawImage(ball, x[z], y[z], this) } } Toolkit.getDefaultToolkit().sync() } else { gameOver(g) } } private fun gameOver(g: Graphics) { val msg = "Game Over" val small = Font("Helvetica", Font.BOLD, 14) val fontMetrics = getFontMetrics(small) val rh = RenderingHints( RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON) rh[RenderingHints.KEY_RENDERING] = RenderingHints.VALUE_RENDER_QUALITY (g as Graphics2D).setRenderingHints(rh) g.color = Color.white g.font = small g.drawString(msg, (boardWidth - fontMetrics.stringWidth(msg)) / 2, boardHeight / 2) } private fun checkApple() { if (x[0] == appleX && y[0] == appleY) { nOfDots++ locateApple() } } private fun move() { for (z in nOfDots downTo 1) { x[z] = x[z - 1] y[z] = y[z - 1] } if (leftDirection) { x[0] -= dotSize } if (rightDirection) { x[0] += dotSize } if (upDirection) { y[0] -= dotSize } if (downDirection) { y[0] += dotSize } } private fun checkCollision() { for (z in nOfDots downTo 1) { if (z > 4 && x[0] == x[z] && y[0] == y[z]) { inGame = false } } if (y[0] >= boardHeight) { inGame = false } if (y[0] < 0) { inGame = false } if (x[0] >= boardWidth) { inGame = false } if (x[0] < 0) { inGame = false } if (!inGame) { timer!!.stop() } } private fun locateApple() { var r = (Math.random() * randPos).toInt() appleX = r * dotSize r = (Math.random() * randPos).toInt() appleY = r * dotSize } override fun actionPerformed(e: ActionEvent) { if (inGame) { checkApple() checkCollision() move() } repaint() } private inner class TAdapter : KeyAdapter() { override fun keyPressed(e: KeyEvent?) { val key = e!!.keyCode if (key == KeyEvent.VK_LEFT && !rightDirection) { leftDirection = true upDirection = false downDirection = false } if (key == KeyEvent.VK_RIGHT && !leftDirection) { rightDirection = true upDirection = false downDirection = false } if (key == KeyEvent.VK_UP && !downDirection) { upDirection = true rightDirection = false leftDirection = false } if (key == KeyEvent.VK_DOWN && !upDirection) { downDirection = true rightDirection = false leftDirection = false } } } }
首先,我们将定义游戏中使用的属性。
private val boardWidth = 300 private val boardHeight = 300 private val dotSize = 10 private val allDots = 900 private val randPos = 29 private val delay = 140
boardWidth
和 boardHeight
属性决定了棋盘的大小。 dotSize
是苹果和蛇的点的尺寸。 allDots
属性定义了棋盘上可能的最大点数 (900 = (300*300)/(10*10))。 randPos
属性用于计算苹果的随机位置。 delay
属性决定了游戏的速度。
private val x = IntArray(allDots) private val y = IntArray(allDots)
这两个数组存储了蛇的所有关节的 x 和 y 坐标。
private fun loadImages() { val iid = ImageIcon("src/main/resources/dot.png") ball = iid.image val iia = ImageIcon("src/main/resources/apple.png") apple = iia.image val iih = ImageIcon("src/main/resources/head.png") head = iih.image }
在 loadImages
方法中,我们获取游戏的图像。 ImageIcon
类用于显示 PNG 图像。
private fun initGame() { nOfDots = 3 for (z in 0 until nOfDots) { x[z] = 50 - z * 10 y[z] = 50 } locateApple() timer = Timer(delay, this) timer!!.start() }
在 initGame
方法中,我们创建蛇,在棋盘上随机放置一个苹果,并启动计时器。
private fun doDrawing(g: Graphics) { if (inGame) { g.drawImage(apple, appleX, appleY, this) for (z in 0 until nOfDots) { if (z == 0) { g.drawImage(head, x[z], y[z], this) } else { g.drawImage(ball, x[z], y[z], this) } } Toolkit.getDefaultToolkit().sync() } else { gameOver(g) } }
在 doDrawing
方法中,我们绘制苹果和蛇对象。 如果游戏结束,我们绘制游戏结束消息。
private fun gameOver(g: Graphics) { val msg = "Game Over" val small = Font("Helvetica", Font.BOLD, 14) val fontMetrics = getFontMetrics(small) val rh = RenderingHints( RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON) rh[RenderingHints.KEY_RENDERING] = RenderingHints.VALUE_RENDER_QUALITY (g as Graphics2D).setRenderingHints(rh) g.color = Color.white g.font = small g.drawString(msg, (boardWidth - fontMetrics.stringWidth(msg)) / 2, boardHeight / 2) }
gameOver
方法在窗口中间绘制“游戏结束”消息。 我们使用渲染提示来平滑地绘制消息。 我们使用字体度量来获取消息的大小。
private fun checkApple() { if (x[0] == appleX && y[0] == appleY) { nOfDots++ locateApple() } }
如果苹果与蛇头碰撞,我们增加蛇的关节数量。 我们调用 locateApple
方法,该方法随机放置一个新的苹果对象。
在 move
方法中,我们有游戏的关键算法。 要理解它,请看蛇是如何移动的。 我们控制蛇的头部。 我们可以使用光标键更改其方向。 其余的关节向上移动一个位置。 第二个关节移动到第一个关节所在的位置,第三个关节移动到第二个关节所在的位置,以此类推。
for (z in nOfDots downTo 1) { x[z] = x[z - 1] y[z] = y[z - 1] }
此代码沿链移动关节。
if (leftDirection) { x[0] -= dotSize }
此行将头部向左移动。
在 checkCollision
方法中,我们确定蛇是否撞到了它自己或墙壁。
for (z in nOfDots downTo 1) { if (z > 4 && x[0] == x[z] && y[0] == y[z]) { inGame = false } }
如果蛇用它的头撞到了它的一个关节,游戏就结束了。
if (y[0] >= boardHeight) { inGame = false }
如果蛇撞到了游戏板的底部,游戏就结束了。
package com.zetcode import java.awt.EventQueue import javax.swing.JFrame class Snake : JFrame() { init { initUI() } private fun initUI() { add(Board()) title = "Snake" isResizable = false pack() setLocationRelativeTo(null) defaultCloseOperation = JFrame.EXIT_ON_CLOSE } companion object { @JvmStatic fun main() { EventQueue.invokeLater { val ex = Snake() ex.isVisible = true } } } }
这是主类。
isResizable = false pack()
isResizable
属性会影响某些平台上 JFrame
容器的插图。 因此,在调用 pack
方法之前调用它很重要。 否则,蛇头与右边框和底边框的碰撞可能无法正常工作。

来源
这就是使用 Kotlin 和 Swing 编写的贪吃蛇游戏。
作者
列出 所有 Kotlin 教程。