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 教程。