用go语言实现一个平面像素小游戏(一)

成品及参考资料

使用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)
}

 运行之后,想要的图片就显示在了窗口上

结语

至此本篇结束,实现了一些基础的功能

如果你有更好的主意去更高效的实现一些东西,例如上面的拆分图片,欢迎在评论区提出建议

如何你有其他问题,欢迎私信我,看到都会回的

下一篇教程增加了键盘事件,实现了移动和闲置动画效果

用go语言实现一个平面像素小游戏(二)-CSDN博客

如果这篇文章对你有帮助,请务必务必点赞收藏,这对我真的很重要!!!

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值