成品及参考资料
使用go语言写了一个平面像素种田小游戏
最终成品图:
小游戏使用Ebiten引擎开发,完整素材及其代码已上传至gitee
使用git clone命令
git clone https://gitee.com/Mercury_blue/planting.git
或者直接在仓库Mercury/Planting (gitee.com)下载
你应该下载下来项目的图片素材(Image文件夹)以供后面的教程使用,项目中的素材比起下面网站提供的资源包,加入了窗口的图标(Image/icon.png)并且图片的命名略有不同
如果你使用官方网站提供的资源包或自己收集的图片素材,请保证后续代码中能正确的加载图片
参考资料:
一起用Go做一个小游戏(上) - 知乎 (zhihu.com)
Making a game with Raylib - YouTube
素材提供网站:
Sprout Lands - Asset Pack by Cup Nooble (itch.io)
Ebiten官网:
Ebitengine - 一款Go语言编写的超级简单2D游戏引擎 (ebitengine-zh.pages.dev)
hajimehoshi/ebiten: Ebitengine - A dead simple 2D game engine for Go (github.com)
ebiten package - github.com/hajimehoshi/ebiten/v2 - Go Packages
前言
本教程会分成几篇文章从头开始一步步去实现各种功能,由于网上go语言参考资料相对较少,加上自身水平有限,功能的实现逻辑可能不是很完美,但会尽量细致的说明如何实现
如果你是刚学完go语言的语法基础,需要一个作品去巩固基础语法,那本教程就再适合不过了!
目的
第一篇教程做的东西相对简单
运行出窗口,设置标题、图标,绘制角色初始图片到窗口上
安装
首先简单介绍一下所使用的库
Ebitengine (旧称 Ebiten) 是一款由Go语言开发的开源游戏引擎,Ebitengine 的简单 API 很适合简单的2D 游戏,并支持同时发布到多平台
ebitengine 要求Go版本 >= 1.15
首先在创建好的项目中使用go module下载这个包
go get -u github.com/hajimehoshi/ebiten/v2
显示窗口并打印文字
创建main.go文件,先写一个祖传的hello world
package main
import (
"github.com/hajimehoshi/ebiten/v2"
"github.com/hajimehoshi/ebiten/v2/ebitenutil"
"image"
"log"
)
type Game struct{}
func (g *Game) Update() error {
return nil
}
func (g *Game) Draw(screen *ebiten.Image) {
ebitenutil.DebugPrint(screen, "Hello, World!")
}
func (g *Game) Layout(outsideWidth, outsideHeight int) (screenWidth, screenHeight int) {
return 400, 350
}
func main() {
ebiten.SetWindowSize(800, 700)
ebiten.SetWindowTitle("Planting")
icon, _, err := ebitenutil.NewImageFromFile("../image/icon.png")
if err != nil {
log.Fatal(err)
}
ebiten.SetWindowIcon([]image.Image{icon})
if err := ebiten.RunGame(&Game{}); err != nil {
log.Fatal(err)
}
}
使用命令运行程序
go run main.go
运行完成后,会看到一个黑窗口,它有图标、标题,且窗口左上角打印出了"Hello, World!"
现在来简单的分析一下工作原理:
首先这个程序需要两个包,ebiten和ebitenutil
ebiten是Ebiten引擎的核心包,提供了绘图、输入等功能
ebitenutil是Ebiten引擎的实用程序包,在上面程序中,它用于加载图片和在屏幕上打印调试消息
从上往下看,程序首先定义了一个结构体
type Game struct{}
Ebiten引擎在运行时必须传入一个实现了ebiten.Game这个接口的游戏对象
// Game defines necessary functions for a game.
type Game interface {
Update() error
Draw(screen *Image)
Layout(outsideWidth, outsideHeight int) (screenWidth, screenHeight int)
}
ebiten.Game接口定义了游戏需要的三个方法,即Update、Draw和Layout
- Update:
func (g *Game) Update() error {
return nil
}
将游戏窗口的背景理解成一块画布,Update函数简单来说就是一个tick更新一次画布
tick是引擎更新的一个时间单位,默认为1/60s,tick的倒数一般称为帧,即游戏的更新频率,默认ebiten游戏是60帧,即每秒更新60次
Update函数主要用来更新游戏的逻辑状态,上面代码中我们没有更新的状态,所以此函数不执行任何操作
Update函数返回一个错误值,返回一个非空值时游戏停止,通常返回一个空值nil,这样只有当用户关闭窗口时游戏才会停止
- Draw:
func (g *Game) Draw(screen *ebiten.Image) {
ebitenutil.DebugPrint(screen, "Hello, World!")
}
Draw函数每一帧调用一次,依赖于显示器的刷新率,如果显示器的刷新率为60Hz,则每秒调用60次
Draw函数简单来说就是在画布上画东西,它接收一个类型为*ebiten.Image的screen对象,每次调用都在screen对象上面绘制各种元素
- Layout:
func (g *Game) Layout(outsideWidth, outsideHeight int) (screenWidth, screenHeight int) {
return 400, 350
}
该方法接收接受外部大小(即桌面上的窗口大小),并返回游戏的逻辑屏幕大小,后续所有的坐标计算都是基于逻辑屏幕大小进行
上面的代码中忽略了参数并返回固定值,这意味着无论窗口大小如何,游戏屏幕大小始终相同
在上面的例子中游戏窗口大小为(800, 700),Layout函数返回的逻辑大小为(400, 350),所以显示会放大1倍
在Draw函数中调用了ebitenutil.DebugPrint函数,它是一个实用程序函数,用于在映像上呈现调试消息
ebitenutil.DebugPrint(screen, "Hello, World!")
在main函数中:
func main() {
ebiten.SetWindowSize(800, 700)
ebiten.SetWindowTitle("Planting")
icon, _, err := ebitenutil.NewImageFromFile("../image/icon.png")
if err != nil {
log.Fatal(err)
}
ebiten.SetWindowIcon([]image.Image{icon})
if err := ebiten.RunGame(&Game{}); err != nil {
log.Fatal(err)
}
}
ebiten.SetWindowSize用于设置窗口大小,若没有设置则使用默认窗口大小
ebiten.SetWindowTitle设置窗口标题
ebiten.SetWindowIcon用于设置窗口的图标,它接收一个image.Image的切片
ebitenutil.NewImageFromFile用于获取图片,它返回三个参数,分别为*ebiten.Image、image.Image和error类型
ebiten.RunGame是运行Ebiten游戏主循环的函数,参数是一个对象,当错误发生时函数将返回一个非空的error,程序中当error非空时错误日志将会以fatal error输出
填充背景
ebiten.Imgae中定义了一个Fill方法,用于将背景填充为特定的颜色
在Draw函数中调用它,将背景填充为天蓝色(118,225,254)
func (g *Game) Draw(screen *ebiten.Image) {
// A代表透明度
screen.Fill(color.RGBA{R: 118, G: 225, B: 254, A: 255})
ebitenutil.DebugPrint(screen, "Hello, World!")
}
填充完后窗口为:
绘制角色
首先需要使用ebitenutil.NewImageFromFile函数,写入文件路径加载图片
// 通过文件路径获取图片 函数会返回三个返回值
imagePlayer, _, err := ebitenutil.NewImageFromFile("../Image/Characters/BasicCharakterSpritesheet.png")
// 处理异常
if err != nil {
log.Fatal(err)
}
ebiten.Image中的DrawImage方法用于在图像上绘制给定的图像,所以我们需要加载好的图片放进Draw函数中绘制,将Draw函数更新如下:
func (g *Game) Draw(screen *ebiten.Image) {
screen.Fill(color.RGBA{R: 118, G: 225, B: 254, A: 255})
imagePlayer, _, err := ebitenutil.NewImageFromFile("../Image/Characters/BasicCharakterSpritesheet.png")
if err != nil {
log.Fatal(err)
}
screen.DrawImage(imagePlayer, nil)
}
DrawImage方法需要传入两个参数
一个*Image的值,表示要绘制的图像
一个*DrawImageOptions类型的值,用于控制绘制的位置、规模等一系列选项值,我们这里先设置为nil,使用默认值
更新Draw函数后运行:
拆分角色图片
由于素材是各种小图片整合在一起的大精灵图,我们需要拆分出对应的小图片
素材图片可以以48*48像素拆分,我们选取左上角的一张图片作为起始图片
使用ebiten.Image中的SubImage方法,可以将一张图片拆分
imageNormal := imagePlayer.SubImage(image.Rect(0, 0, 48, 48)).(*ebiten.Image)
这一行有点长,一点一点看,首先SubImage方法需要传入一个image.Rectangle类型的值,表示如何将图片以矩形划分
然后我们使用image.Dect函数创建一个矩形
// Rect函数头
func Rect(x0 int, y0 int, x1 int, y1 int) Rectangle
Rect方法需要4个参数x0、y0、x1、y1,(x0, y0)代表矩形左上角的值,(x1, y1)代表矩形右下角的值,使用这四个参数就可以表示出一个矩形
这样看就清晰了,在被拆分的图片中原点为左上角,坐标为(0, 0)
用(0, 0)和(48, 48)这两点表示出一个矩形,这个矩形中的内容就是我们想要的子图片!
将Draw函数更新如下:
func (g *Game) Draw(screen *ebiten.Image) {
screen.Fill(color.RGBA{R: 118, G: 225, B: 254, A: 255})
imagePlayer, _, err := ebitenutil.NewImageFromFile("../Image/Characters/BasicCharakterSpritesheet.png")
if err != nil {
log.Fatal(err)
}
imageNormal := imagePlayer.SubImage(image.Rect(0, 0, 48, 48)).(*ebiten.Image)
screen.DrawImage(imageNormal, nil)
}
运行之后,想要的图片就显示在了窗口上
结语
至此本篇结束,实现了一些基础的功能
如果你有更好的主意去更高效的实现一些东西,例如上面的拆分图片,欢迎在评论区提出建议
如何你有其他问题,欢迎私信我,看到都会回的
下一篇教程增加了键盘事件,实现了移动和闲置动画效果
如果这篇文章对你有帮助,请务必务必点赞收藏,这对我真的很重要!!!