写一个用Golang运行的小游戏打发时间---贪吃蛇
为了实现可视化,我使用了Go的GUI库fyne
思路:
定义一个结构体Snake表示贪吃蛇,包含贪吃蛇的长度、方向、身体坐标等属性。
定义一个结构体Game表示游戏状态,包含贪吃蛇、食物、得分等属性。
实现游戏画面的显示,使用fyne库中的Canvas组件,绘制贪吃蛇和食物的图形。
实现键盘事件的监听,根据不同的按键来改变贪吃蛇的方向。
实现贪吃蛇的移动逻辑,每个时间间隔更新贪吃蛇的位置,判断是否吃到食物或碰到墙壁或自己的身体,更新得分并重新生成食物。
完成游戏结束的处理,当贪吃蛇碰到墙壁或自己的身体时,游戏结束,显示得分和重新开始按钮。
代码:
package main
import (
"fmt"
"image/color"
"math/rand"
"time"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/canvas"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/widget"
)
const (
cellSize = 20 // 每个格子的大小
gridWidth = 30 // 游戏区域的宽度(格子数)
gridHeight = 20 // 游戏区域的高度(格子数)
)
var (
directions = []fyne.Key{
fyne.KeyUp,
fyne.KeyDown,
fyne.KeyLeft,
fyne.KeyRight,
}
)
// Snake表示贪吃蛇
type Snake struct {
Body []fyne.Position // 身体坐标
Length int // 长度
Direction fyne.Key // 方向
lastMoveAt time.Time // 上次移动时间
}
// NewSnake创建一个新的贪吃蛇
func NewSnake() Snake {
head := fyne.NewPos(cellSize*gridWidth/2, cellSize*gridHeight/2)
return Snake{
Body: []fyne.Position{head},
Length: 1,
Direction: fyne.KeyRight,
lastMoveAt: time.Now(),
}
}
// Move移动贪吃蛇
func (s *Snake) Move() {
if time.Since(s.lastMoveAt) < 100*time.Millisecond {
return
}
head := s.Body[0]
var newHead fyne.Position
switch s.Direction {
case fyne.KeyUp:
newHead = fyne.NewPos(head.X, head.Y-cellSize)
case fyne.KeyDown:
newHead = fyne.NewPos(head.X, head.Y+cellSize)
case fyne.KeyLeft:
newHead = fyne.NewPos(head.X-cellSize, head.Y)
case fyne.KeyRight:
newHead = fyne.NewPos(head.X+cellSize, head.Y)
}
s.Body = append([]fyne.Position{newHead}, s.Body[:s.Length-1]...)
s.lastMoveAt = time.Now()
}
// EatFood吃掉食物
func (s *Snake) EatFood() {
s.Length++
s.Body = append([]fyne.Position{s.Body[0]}, s.Body...)
}
// CollideWith自己的身体
func (s *Snake) CollideWithSelf() bool {
head := s.Body[0]
for _, b := range s.Body[1:] {
if head == b {
return true
}
}
return false
}
// CollideWithWall撞到墙壁
func (s *Snake) CollideWithWall() bool {
head := s.Body[0]
return head.X < 0 || head.X >= cellSize*gridWidth || head.Y < 0 || head.Y >= cellSize*gridHeight
}
// Game表示游戏状态
type Game struct {
Snake Snake // 贪吃蛇
Food fyne.Position // 食物坐标
Score int // 得分
Over bool // 游戏是否结束
WinSize fyne.Size // 窗口大小
}
// NewGame创建一个新的游戏状态
func NewGame(winSize fyne.Size) *Game {
return &Game{
Snake: NewSnake(),
Food: fyne.NewPos(rand.Intn(gridWidth)*cellSize, rand.Intn(gridHeight)*cellSize),
WinSize: winSize,
}
}
// Update更新游戏状态
func (g *Game) Update() {
if g.Over {
return
}
g.Snake.Move()
if g.Snake.CollideWithSelf() || g.Snake.CollideWithWall() {
g.Over = true
return
}
if g.Snake.Body[0] == g.Food {
g.Snake.EatFood()
g.Food = fyne.NewPos(rand.Intn(gridWidth)*cellSize, rand.Intn(gridHeight)*cellSize)
g.Score++
}
}
// Draw绘制游戏画面
func (g *Game) Draw() fyne.CanvasObject {
snakeColor := color.RGBA{0, 255, 0, 255}
foodColor := color.RGBA{255, 0, 0, 255}
objects := []fyne.CanvasObject{}
// 绘制贪吃蛇
for _, b := range g.Snake.Body {
objects = append(objects, canvas.NewRectangle(snakeColor).SetMinSize(fyne.NewSize(cellSize, cellSize)).SetPosition(b))
}
// 绘制食物
objects = append(objects, canvas.NewCircle(foodColor, cellSize/2).SetPosition(g.Food).SetMinSize(fyne.NewSize(cellSize, cellSize)))
// 绘制得分
scoreLabel := widget.NewLabel(fmt.Sprintf("Score: %d", g.Score))
scoreLabel.Move(fyne.NewPos(g.WinSize.Width-scoreLabel.MinSize().Width-10, 10))
objects = append(objects, scoreLabel)
return container.NewWithoutLayout(objects...)
}
// SetDirection设置贪吃蛇的方向
func (g *Game) SetDirection(direction fyne.Key) {
if direction == fyne.KeyReturn && g.Over {
*g = *NewGame(g.WinSize)
return
}
if contains(directions, direction) {
g.Snake.Direction = direction
}
}
// contains检查是否包含指定元素
func contains(keys []fyne.Key, key fyne.Key) bool {
for _, k := range keys {
if k == key {
return true
}
}
return false
}
在这个游戏状态中,我们定义了贪吃蛇和食物的坐标、得分、游戏是否结束以及窗口大小。我们还定义了NewGame
函数,用于创建一个新的游戏状态,并在其中创建一个新的贪吃蛇和一个随机的食物。我们还定义了Update
函数和Draw
函数,用于绘制游戏界面,并将贪吃蛇、食物和得分标签添加到画布对象中。最后,我们还定义了SetDirection
函数,用于设置贪吃蛇的移动方向,并在游戏结束后通过按下回车键来重新开始游戏
现在我们需要在main
函数中创建一个游戏对象,并将其传递给fyne框架,让它处理游戏的渲染和事件
func main() {
// 创建一个新的游戏对象
game := NewGame(fyne.NewSize(gridWidth*cellSize, gridHeight*cellSize))
// 创建一个窗口
app := app.New()
window := app.NewWindow("Snake Game")
window.Resize(game.WinSize)
// 设置事件处理程序
window.SetContent(canvas.NewRasterWithPixels(func(w, h int) []byte {
img := image.NewRGBA(image.Rect(0, 0, w, h))
draw.Draw(img, img.Bounds(), &image.Uniform{color.Black}, image.ZP, draw.Src)
// 绘制游戏画面
canvasObj := game.Draw()
canvasObj.Paint(img)
return img.Pix
}))
// 监听按键事件
window.Canvas().SetOnKeyDown(func(event *fyne.KeyEvent) {
game.SetDirection(event.Name)
})
// 定期更新游戏状态
go func() {
for {
time.Sleep(time.Second / fps)
game.Update()
window.Canvas().Refresh(game.Draw)
}
}()
// 显示窗口
window.ShowAndRun()
}
在这个main
函数中,我们首先创建一个新的游戏对象,并将窗口的大小设置为网格的大小乘以每个单元格的大小。然后我们创建一个fyne
的窗口对象,并将其内容设置为一个用于绘制游戏界面的函数。这个函数将在每个帧中被调用,并使用game.Draw
函数返回的画布对象来渲染游戏界面。
我们还设置了一个按键事件处理程序,它将接收用户按下的键,并使用game.SetDirection
函数来更新贪吃蛇的移动方向。最后,我们使用一个无限循环来定期更新游戏状态并重新绘制游戏界面,然后调用window.ShowAndRun()
函数来显示窗口并开始运行游戏。
这就是一个简单的贪吃蛇游戏的实现。由于篇幅限制,这里只是提供了一个基本的实现,并未对游戏进行优化或添加更多的功能。