说到用编程语言实现一个游戏,这恐怕就是儿时最大的梦想了,记得还在用翻盖手机的时候,那时手机里有一个游戏程序就叫Java,但当时哪懂Java是什么,只知道这个游戏和手机里其他的自带游戏不一样,需要手机开流量,再后来就到了智能机,游戏可以作为单独的APP放在手机里,互联网逐渐普及后,各种PC端的网页游戏、大型网络游戏等就开始如潮水般涌出,玩游戏的心也越来也重,直到有一天,我最喜欢的一个网页游戏发布了停服通知,那应该是我童年时期最喜欢的游戏之一,而在收到这个停服通知的时候我还没有上大学,更没有接触编程,但让这个游戏能回来是我一只的梦想。
后来上了大学,对编程有了越来越多的认识,才知道做一个游戏是多么复杂,要牵扯到的东西会很多很多,而技术只是最基本的要求,正因为如此,在做游戏的时候可以用到和学到很多知识,比如我们这篇文章要讲的《拼图二维码》小游戏,就牵扯到了Go语言的很多技术以及前端技术,下面我们话不多说,开始分享这个游戏的制作过程。
编程语言:Go、HTML+CSS、JavaScript
二维码生成:github.com/yeqown/go-qrcode
配置文件读取:github.com/spf13/viper
后续还可能在Docker镜像打包
、服务监控
等方向扩展
先看整个业务的流程图,游戏的核心就是拼图+二维码生成,拼图由前端实现,二维码则由后端实现:
体验地址(目前仅支持PC端):
http://yankaka.chat:8081/static/
游戏流程:
1)开始输入你的用户名,并选择你要拼图的图片,然后点击开始:
2)选择图片完成后会出现游戏页面,进行拼图游戏:
3)拼图完成后会进行提示,点击确定页面跳转到二维码:
4)扫码,获取证书
在代码实现层面,主要是采用HTTP接口进行前后端的交互,后端主要提供了两个HTTP接口,分别是:
/qrcode/gen
:根据图片生成二维码,返回二维码地址。
/success
:根据请求的用户名、拼图时间和二维码文件生成证书。
除此之外,后端进行了静态资源地址的配置,下面我们就从HTTP接口、配置文件读取、二维码生成、证书生成这几个流程进行分别的讲解。
Go实现HTTP服务相对简单,只需要启动一个tcp的Listener,绑定端口,与HTTP Server关联,然后启动HTTP服务,如下代码,是我们提供的两个HTTP接口和静态资源地址的指定。
func runHttp() {
listen, err := net.Listen("tcp", ":8081")
if err != nil {
panic(err)
}
mux := http.NewServeMux()
mux.HandleFunc("/qrcode/gen", uploadFileHandler)
mux.HandleFunc("/success", success)
mux.Handle("/static/", http.StripPrefix("/", http.FileServer(http.Dir("."))))
_ = http.Serve(listen, mux)
}
func main() {
runHttp()
}
Viper应该算是Go项目中最常用的插件之一,主要是提供Go对不同类型配置文件的读取,可以是ini、conf、yaml等等类型,使用方式首先引入依赖:
go get -u github.com/spf13/viper
然后在项目中增加config.yaml文件,进行配置的定义:
server:
port: 8081
tmp-path: /tmp
domain: http://localhost:8081
其中:
然后使用config.go进行对配置文件的读取:
package main
import (
"github.com/spf13/viper"
)
var globalConfig *GlobalConfig
type GlobalConfig struct {
Port int
TmpPath string
Domain string
}
func GetGlobalConfig() *GlobalConfig {
return globalConfig
}
func InitConfig() {
viper.SetConfigFile("config.yaml")
if err := viper.ReadInConfig(); err != nil {
panic(err)
}
globalConfig = &GlobalConfig{
Port: viper.GetInt("server.port"),
TmpPath: viper.GetString("server.tmp-path"),
Domain: viper.GetString("server.domain"),
}
}
接下来我们在启动项目的时候就可以先读取配置文件,例如:
func main() {
InitConfig()
log.Infof("starting server config: %+v", GetGlobalConfig())
runHttp()
}
启动项目后的效果:
INFO[0000] starting server config: &{Port:8081 TmpPath:/tmp Domain:http://localhost:8081}
这一步应该是整个二维码游戏的核心功能之一,需要我们先引入两个三方包:
go get -u github.com/yeqown/go-qrcode/v2
go get -u github.com/disintegration/imaging
如果是初次了解,可以先看下它的官方说明,先知道大概是怎么用的,比如:
https://github.com/yeqown/go-qrcode/blob/main/README.md
和
https://github.com/yeqown/go-qrcode/blob/main/example/simple/main.go
这两个文档中我们可以知道,使用该包生成二维码的大概流程就是三步:
1)实例化一个对象,并赋值二维码内容、编码格式等属性
2)指定要生成的二维码的文件名称
3)执行生成二维码的方法
阅读一下后续的文档,我们还可以了解到可以根据halftone、logo等不同的配置生成不同类型的二维码,那么我们就先定义一个结构体,它的属性就是生成二维码所需要的属性:
type QrCodeGen struct {
Name string // 文件名称
Content string // 二维码内容
LogoFile string // logo文件
LogoWidth LogoWidth // logo大小
HalftoneSrcFile string // Halftone源文件
Width OutputSize // 二维码大小
OutputFileType FileType // 输出文件格式
}
此外,为了有相对的规范,我们将logo大小、二维码大小和输出文件格式都定义了枚举:
type LogoWidth int
type FileType string
type OutputSize int
const (
MINI LogoWidth = 4
MEDIUM LogoWidth = 3
BIG LogoWidth = 2
NOT FileType = ""
JPG FileType = "jpg"
PNG FileType = "png"
OutputMini OutputSize = 200
OutputMedium OutputSize = 500
OutputBig OutputSize = 1000
)
什么是Optional模式?它有什么好处?大家可以阅读下这篇文章,在这里就不做额外的讲解:
https://time.geekbang.org/column/article/330212
使用Optional模式实现一个NewQuCodeGen函数:
type Option func(*QrCodeGen)
const (
DefaultLogoWidth = MEDIUM
DefaultOutputSize = OutputMedium
DefaultFileType = JPG
)
type QrCodeGen struct {
Name string
Content string
LogoFile string
LogoWidth LogoWidth
HalftoneSrcFile string
Width OutputSize
OutputFileType FileType
Path string
}
func NewQuCodeGen(content string, opts ...Option) *QrCodeGen {
gen := &QrCodeGen{
Content: content,
Width: DefaultOutputSize,
OutputFileType: DefaultFileType,
LogoWidth: DefaultLogoWidth,
}
for _, opt := range opts {
opt(gen)
}
return gen
}
func WithLogoFile(fileName string) Option {
return func(c *QrCodeGen) {
c.LogoFile = fileName
}
}
func WithLogoWidth(width LogoWidth) Option {
return func(c *QrCodeGen) {
c.LogoWidth = width
}
}
func WithHalftoneSrcFile(fileName string) Option {
return func(c *QrCodeGen) {
c.HalftoneSrcFile = fileName
}
}
func WithName(name string) Option {
return func(c *QrCodeGen) {
c.Name = name
}
}
func WithOutputFileType(fileType FileType) Option {
return func(c *QrCodeGen) {
c.OutputFileType = fileType
}
}
func WithOutputFileSize(size OutputSize) Option {
return func(c *QrCodeGen) {
c.Width = size
}
}
func WithPath(path string) Option {
return func(c *QrCodeGen) {
c.Path = path
}
}
最重点的就是GenQrCode方法,是生成二维码的主要方法:
func (g *QrCodeGen) GenQrCode() (string, error) {
// 确认文件名称
qrFileName := fmt.Sprintf("%d.%s", time.Now().UnixMilli(), g.OutputFileType)
if g.Name != "" {
qrFileName = fmt.Sprintf("%s.%s", g.Name, g.OutputFileType)
}
// 内容
qrc, err := qrcode.NewWith(g.Content,
qrcode.WithEncodingMode(qrcode.EncModeByte),
qrcode.WithErrorCorrectionLevel(qrcode.ErrorCorrectionMedium),
)
if err != nil {
log.Errorf("qrcode.NewWith error: %v", err)
return "", err
}
// 基本内容
imageOptions := make([]standard.ImageOption, 0)
imageOptions = append(imageOptions, standard.WithQRWidth(uint8(g.Width/10)))
if g.LogoFile != "" {
var resizeImg *image.NRGBA
logoSrc, err := imaging.Open(g.LogoFile)
if err != nil {
log.Errorf("imaging.Open error: %v", err)
return "", err
}
logoWidth, logoHeight := logoSrc.Bounds().Dx(), logoSrc.Bounds().Dy()
log.Infof("logofile size width: %d ,height: %d", logoWidth, logoHeight)
if g.LogoWidth > 0 {
switch g.LogoWidth {
case MINI:
resizeImg = imaging.Resize(logoSrc, int(g.Width)/int(MINI), int(g.Width)/int(MINI), imaging.Lanczos)
case MEDIUM:
resizeImg = imaging.Resize(logoSrc, int(g.Width)/int(MEDIUM), int(g.Width)/int(MEDIUM), imaging.Lanczos)
case BIG:
resizeImg = imaging.Resize(logoSrc, int(g.Width)/int(BIG), int(g.Width)/int(BIG), imaging.Lanczos)
}
} else {
resizeImg = imaging.Resize(logoSrc, int(g.Width)/int(MEDIUM), int(g.Width)/int(MEDIUM), imaging.Lanczos)
}
g.LogoFile = fmt.Sprintf("%s_tmp.%s", GetFileName(g.LogoFile), JPG)
if err = imaging.Save(resizeImg, g.LogoFile); err != nil {
log.Errorf("imaging.Save: %v", err)
return "", err
}
imageOptions = append(imageOptions, standard.WithLogoImageFileJPEG(g.LogoFile))
imageOptions = append(imageOptions, standard.WithLogoSizeMultiplier(int(g.LogoWidth)))
}
// Halftone
if g.HalftoneSrcFile != "" {
imageOptions = append(imageOptions, []standard.ImageOption{
standard.WithHalftone(g.HalftoneSrcFile),
standard.WithBgTransparent(),
}...)
}
w, err := standard.New(fmt.Sprintf("%s/%s", "./static", qrFileName), imageOptions...)
if err != nil {
log.Errorf("qrcode.NewWith error: %v", err)
return "", err
}
if err = qrc.Save(w); err != nil {
log.Errorf("qrc.Save: %v", err)
return "", err
}
return qrFileName, nil
}
在这个方法里主要用到了两个Go的三方依赖:
其中github.com/disintegration/imaging
是一个功能强大的Go语言图像处理库,它提供了丰富的图像处理函数和方法,可以满足大多数图像处理需求。通过简单的API调用,开发者可以轻松地在Go项目中实现图像的打开、缩放、裁剪、旋转、滤镜效果添加以及保存等操作。
github.com/yeqown/go-qrcode
是一个功能强大且易于使用的Go语言二维码生成库。它提供了丰富的自定义选项和高效的性能,支持广泛的应用场景。通过简单的API调用,开发者可以轻松地在Go项目中生成各种样式的二维码。
源码获取方式,关注公众号【扯编程的淡】后回复【qrcode】
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。