当前位置: 首页 > article >正文

Go Ebiten小游戏开发:2048

2048

项目简介

这是一个使用Go语言和Ebiten游戏引擎开发的2048游戏。2048是一款经典的数字滑块游戏,玩家通过上下左右移动合并相同数字,目标是获得2048数字方块。

核心算法实现

游戏数据结构

const (
    gridSize = 4  // 4x4网格
)

var (
    grid [gridSize][gridSize]int  // 游戏主网格
    score int                     // 游戏分数
    gameOver bool                 // 游戏结束标志
)

游戏使用一个4x4的二维数组存储每个格子的数值,0表示空格子。

随机生成方块算法

func addRandomTile() {
    var emptyTiles [][2]int
    for y := 0; y < gridSize; y++ {
        for x := 0; x < gridSize; x++ {
            if grid[y][x] == 0 {
                emptyTiles = append(emptyTiles, [2]int{x, y})
            }
        }
    }
    if len(emptyTiles) > 0 {
        pos := emptyTiles[rand.Intn(len(emptyTiles))]
        value := 2
        if rand.Float32() < 0.1 {
            value = 4
        }
        grid[pos[1]][pos[0]] = value
    }
}

算法实现原理:

  1. 扫描整个网格,找出所有空格子(值为0)的坐标
  2. 从空格子列表中随机选择一个位置
  3. 90%的概率生成数字2,10%的概率生成数字4
  4. 在选定的位置放置新数字

移动和合并算法

移动和合并是2048游戏的核心,通过一个通用函数实现四个方向的操作:

func moveAndMerge(transform func(x, y int) (int, int)) {
    // 合并相同数字
    // 移动数字
}

实现原理:

  1. 使用转换函数将四个方向的操作抽象为统一的逻辑
  2. 先处理合并:查找相邻的相同数字并合并,更新分数
  3. 再处理移动:将非空格子向特定方向移动,填补空格
  4. 如果有移动或合并操作,则添加一个随机数字

转换函数的巧妙设计使得四个方向的操作可以共用一套逻辑:

  • 向上移动:func(x, y int) (int, int) { return x, y }
  • 向下移动:func(x, y int) (int, int) { return x, gridSize - 1 - y }
  • 向左移动:func(x, y int) (int, int) { return y, x }
  • 向右移动:func(x, y int) (int, int) { return gridSize - 1 - y, x }

游戏结束判断

游戏结束的条件是:棋盘已满且无法进行任何合并操作。

func isGridFull() bool {
    // 检查网格是否已满
}

func canMerge() bool {
    // 检查是否有可以合并的相邻数字
}

实现原理:

  1. 首先检查网格是否已满(没有值为0的格子)
  2. 然后检查是否存在相邻的相同数字(可以合并)
  3. 两个条件都满足时,游戏才结束

UI渲染

游戏使用Ebiten的绘图API实现界面渲染:

func (g *Game) Draw(screen *ebiten.Image) {
    // 绘制背景和网格
    // 绘制每个格子和数字
    // 显示分数和游戏状态
}

特点:

  1. 使用不同颜色区分不同数值的方块
  2. 根据数字位数自动调整字体大小,保证美观
  3. 文字居中显示,增强视觉体验

总结

2048游戏的关键在于数据结构和移动合并算法的实现。通过抽象转换函数,代码实现了简洁统一的移动逻辑。使用Ebiten引擎提供的图形API,使游戏具有良好的视觉效果和交互体验。

这个项目展示了Go语言在游戏开发中的应用,以及如何使用函数式编程思想简化游戏逻辑实现。

完整代码

需要在该文件夹添加一个字体文件font.oft才能运行

package main

import (
	"fmt"
	"image/color"
	"log"
	"math/rand"
	"os"

	"github.com/hajimehoshi/ebiten/v2"
	"github.com/hajimehoshi/ebiten/v2/inpututil"
	"github.com/hajimehoshi/ebiten/v2/text"
	"github.com/hajimehoshi/ebiten/v2/vector"
	"golang.org/x/image/font"
	"golang.org/x/image/font/opentype"
)

const (
	screenWidth  = 400
	screenHeight = 500
	gridSize     = 4
	tileSize     = 100
	fontSize     = 48 // 更大的字体大小
)

var (
	grid     [gridSize][gridSize]int
	score    int
	gameOver bool
	colors   = map[int]color.RGBA{
		0:    {240, 240, 240, 255}, // 空白格子
		2:    {255, 255, 204, 255}, // 2
		4:    {255, 204, 153, 255}, // 4
		8:    {255, 153, 102, 255}, // 8
		16:   {255, 102, 102, 255}, // 16
		32:   {255, 51, 51, 255},   // 32
		64:   {255, 0, 0, 255},     // 64
		128:  {255, 255, 102, 255}, // 128
		256:  {255, 255, 51, 255},  // 256
		512:  {255, 255, 0, 255},   // 512
		1024: {255, 204, 0, 255},   // 1024
		2048: {255, 153, 0, 255},   // 2048
	}
	face      font.Face
	fontFaces map[int]font.Face // 不同位数的数字使用不同大小的字体
	fontTT    *opentype.Font    // 保存字体对象以供重复使用
)

type Game struct{}

func (g *Game) Update() error {
	if gameOver {
		return nil
	}

	if inpututil.IsKeyJustPressed(ebiten.KeyArrowUp) {
		moveUp()
	} else if inpututil.IsKeyJustPressed(ebiten.KeyArrowDown) {
		moveDown()
	} else if inpututil.IsKeyJustPressed(ebiten.KeyArrowLeft) {
		moveLeft()
	} else if inpututil.IsKeyJustPressed(ebiten.KeyArrowRight) {
		moveRight()
	}

	if isGridFull() && !canMerge() {
		gameOver = true
	}

	return nil
}

func (g *Game) Draw(screen *ebiten.Image) {
	screen.Fill(color.RGBA{187, 173, 160, 255}) // 背景色

	// 绘制棋盘
	for y := 0; y < gridSize; y++ {
		for x := 0; x < gridSize; x++ {
			value := grid[y][x]
			tileColor := colors[value]
			drawTile(screen, x, y, tileColor, fmt.Sprintf("%d", value))
		}
	}

	// 绘制分数
	text.Draw(screen, fmt.Sprintf("Score: %d", score), face, 10, screenHeight-20, color.White)

	// 游戏结束提示
	if gameOver {
		text.Draw(screen, "Game Over!", face, screenWidth/2-120, screenHeight/2, color.White)
	}
}

func (g *Game) Layout(outsideWidth, outsideHeight int) (int, int) {
	return screenWidth, screenHeight
}

func main() {
	initGrid()

	// 加载字体
	var err error
	tt, err := opentype.Parse(embeddedFont)
	if err != nil {
		log.Fatal(err)
	}
	fontTT = tt // 保存字体对象以供创建不同大小的字体

	// 创建默认字体和分数显示用字体
	face, err = opentype.NewFace(tt, &opentype.FaceOptions{
		Size:    fontSize,
		DPI:     72,
		Hinting: font.HintingFull,
	})
	if err != nil {
		log.Fatal(err)
	}

	// 初始化不同大小的字体
	initFontFaces()

	game := &Game{}
	ebiten.SetWindowSize(screenWidth, screenHeight)
	ebiten.SetWindowTitle("2048")
	if err := ebiten.RunGame(game); err != nil {
		log.Fatal(err)
	}
}

// 初始化不同大小的字体
func initFontFaces() {
	fontFaces = make(map[int]font.Face)

	// 不同位数对应的字体大小
	fontSizes := map[int]float64{
		1: 48, // 个位数 (1-9)
		2: 40, // 两位数 (10-99)
		3: 34, // 三位数 (100-999)
		4: 28, // 四位数 (1000-9999)
	}

	// 创建不同大小的字体
	for digits, size := range fontSizes {
		f, err := opentype.NewFace(fontTT, &opentype.FaceOptions{
			Size:    size,
			DPI:     72,
			Hinting: font.HintingFull,
		})
		if err != nil {
			log.Printf("创建字体大小 %f 失败: %v", size, err)
			continue
		}
		fontFaces[digits] = f
	}
}

// 获取数字的位数
func getDigits(n int) int {
	if n == 0 {
		return 1
	}
	digits := 0
	for n > 0 {
		digits++
		n /= 10
	}
	return digits
}

// 绘制一个格子
func drawTile(screen *ebiten.Image, x, y int, tileColor color.RGBA, textStr string) {
	vector.DrawFilledRect(screen, float32(x*tileSize+10), float32(y*tileSize+10), tileSize-20, tileSize-20, tileColor, true)
	if textStr != "0" {
		// 根据数字位数选择合适的字体
		digits := len(textStr)
		tileFace := face // 默认使用标准字体

		// 如果有对应位数的字体,则使用对应的字体
		if f, ok := fontFaces[digits]; ok {
			tileFace = f
		}

		// 计算文本的宽度和高度
		bounds, _ := font.BoundString(tileFace, textStr)
		textWidth := (bounds.Max.X - bounds.Min.X).Ceil()
		textHeight := (bounds.Max.Y - bounds.Min.Y).Ceil()

		// 计算居中的位置
		textX := x*tileSize + (tileSize-textWidth)/2
		textY := y*tileSize + (tileSize+textHeight)/2

		// 绘制文本
		text.Draw(screen, textStr, tileFace, textX, textY, color.Black)
	}
}

// 初始化棋盘
func initGrid() {
	addRandomTile()
	addRandomTile()
}

// 在随机位置添加一个 2 或 4
func addRandomTile() {
	var emptyTiles [][2]int
	for y := 0; y < gridSize; y++ {
		for x := 0; x < gridSize; x++ {
			if grid[y][x] == 0 {
				emptyTiles = append(emptyTiles, [2]int{x, y})
			}
		}
	}
	if len(emptyTiles) > 0 {
		pos := emptyTiles[rand.Intn(len(emptyTiles))]
		value := 2
		if rand.Float32() < 0.1 {
			value = 4
		}
		grid[pos[1]][pos[0]] = value
	}
}

// 移动和合并逻辑(上下左右)
func moveUp() {
	moveAndMerge(func(x, y int) (int, int) { return x, y })
}

func moveDown() {
	moveAndMerge(func(x, y int) (int, int) { return x, gridSize - 1 - y })
}

func moveLeft() {
	moveAndMerge(func(x, y int) (int, int) { return y, x })
}

func moveRight() {
	moveAndMerge(func(x, y int) (int, int) { return gridSize - 1 - y, x })
}

// 通用的移动和合并函数
func moveAndMerge(transform func(x, y int) (int, int)) {
	moved := false
	for x := 0; x < gridSize; x++ {
		// 合并相同数字
		for y := 0; y < gridSize-1; y++ {
			tx, ty := transform(x, y)
			if grid[ty][tx] == 0 {
				continue
			}
			for yy := y + 1; yy < gridSize; yy++ {
				txx, tyy := transform(x, yy)
				if grid[tyy][txx] == 0 {
					continue
				}
				if grid[ty][tx] == grid[tyy][txx] {
					grid[ty][tx] *= 2
					score += grid[ty][tx]
					grid[tyy][txx] = 0
					moved = true
				}
				break
			}
		}
		// 移动数字
		for y := 0; y < gridSize; y++ {
			tx, ty := transform(x, y)
			if grid[ty][tx] == 0 {
				for yy := y + 1; yy < gridSize; yy++ {
					txx, tyy := transform(x, yy)
					if grid[tyy][txx] != 0 {
						grid[ty][tx] = grid[tyy][txx]
						grid[tyy][txx] = 0
						moved = true
						break
					}
				}
			}
		}
	}
	if moved {
		addRandomTile()
	}
}

// 检查棋盘是否已满
func isGridFull() bool {
	for y := 0; y < gridSize; y++ {
		for x := 0; x < gridSize; x++ {
			if grid[y][x] == 0 {
				return false
			}
		}
	}
	return true
}

// 检查是否可以合并
func canMerge() bool {
	for y := 0; y < gridSize; y++ {
		for x := 0; x < gridSize; x++ {
			if y < gridSize-1 && grid[y][x] == grid[y+1][x] {
				return true
			}
			if x < gridSize-1 && grid[y][x] == grid[y][x+1] {
				return true
			}
		}
	}
	return false
}

// 嵌入一个字体文件(例如 TTF 文件)
var embeddedFont, _ = os.ReadFile("font.otf") // 这里需要嵌入一个字体文件,例如 TTF 文件


http://www.kler.cn/a/590596.html

相关文章:

  • Qt5.15.2实现Qt for WebAssembly与示例
  • 学习单片机需要多长时间才能进行简单的项目开发?
  • 04 1个路由器配置一个子网的dhcp服务
  • Java爬虫如何处理动态加载的内容?
  • RTSP/Onvif安防视频EasyNVR平台 vs.多协议接入视频汇聚EasyCVR平台:设备分组的区别
  • 在Android中,子线程可以更新UI吗
  • Vue相关面试题
  • 单片机开发资源分析的实战——以STM32F103C8T6为例子的单片机资源分析
  • 【Pycharm】Pycharm创建.py文件时自动生成文件头
  • 主流开源大模型能力对比矩阵
  • 【HDLBits】Module合集(练习均用两种方法解决)
  • 关于软航OFFICE文档控件软件在Chrome 133版本上提示扩展已停用的原因及处理办法
  • 【C++标准库类型】深入理解vector类型(1):从基础到实践
  • WebSocket生命周期和vue中使用
  • 基于 Prometheus + Grafana 监控微服务和数据库
  • Docker 容器指标搜集工具cAdvisor
  • 基于图神经网络(GNN)的节点分类实战:从GCN到GraphSAGE
  • MySQL 批量插入 vs 逐条插
  • UNI-APP uts插件 支持ANDROID 监听手机状态
  • 【mysql】不允许来自主机的链接错误解决方案