ZetCode

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" 消息将显示在游戏板的中间。

Board.kt
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

boardWidthboardHeight 属性决定了棋盘的大小。 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
}

如果蛇撞到了游戏板的底部,游戏就结束了。

Snake.kt
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 方法之前调用它很重要。 否则,蛇头与右边框和底边框的碰撞可能无法正常工作。

Snake
图:贪吃蛇

来源

贪吃蛇游戏

这就是使用 Kotlin 和 Swing 编写的贪吃蛇游戏。

作者

我叫 Jan Bodnar,是一位热情的程序员,拥有丰富的编程经验。 自 2007 年以来,我一直在撰写编程文章。 迄今为止,我撰写了 1,400 多篇文章和 8 本电子书。 我拥有超过十年的编程教学经验。

列出 所有 Kotlin 教程