前言
hi,大家好,这里是白泽。今天给大家分享一个 GitHub 🌟9.8k 的 Go 语言 2D 游戏引擎。
https://github.com/hajimehoshi/ebiten
引擎的贡献者依旧在积极维护,是一个兼具学习 & 娱乐的项目!
为此我也用这个引擎写了一个生存游戏: avoid-the-enemies【如下图】:https://github.com/BaiZe1998/avoid-the-enemies:西部世界vp
当然详细的游戏设计我还是想再通过下一期文章讲解,这期的内容主要讲解游戏引擎提供的能力,一些官方有趣的 demo,以及以我的开发为例,讲解如何使用这个引擎快速上手开发属于自己的游戏。
avoid-the-enemies 玩法
尽可能存活是你唯一要做的事!
游戏的体验实况已经录制并上传了,欢迎你的关注 📺 B站:白泽talk,QQ群:622383022。
🌟 当然,如果您是一位 Go 学习的新手,您可以在我开源的学习仓库:https://github.com/BaiZe1998/go-learning 中,找到我往期翻译的英文书籍,或者Go学习路线,等一系列精彩内容。
有趣的 Demo
在这个游戏引擎的 examples 目录下集成很多的游戏小 demo 以及素材,只要安装了 go 开发环境就能直接编译运行了。
而且它支持跨平台运行的:Win、macOS、Linux、FreeBSD、Android、IOS、Xbox、Siwtch 都不在话下。(支持手柄外设)
Flappy
复刻经典 flappy bird 小游戏,只是鼠鼠实在是太大了,难度指数:5🌟。
粒子效果
酷炫指数:5🌟。
钢琴
娱乐指数:5🌟,午休的时候来一曲吧。
引擎核心流程
由于引擎本身帮助开发者完成了多平台适配,以及最重要的 UI 的渲染,因此开发游戏只需要关注左侧这张图的三个核心方法即可。
整个引擎就是一个循环,这个循环每秒大约可以执行60次,那就是达到了所谓的 FPS 60。
这个循环中要做的事有两件事,一部分写数值,一部分绘制角色。(调用上图左侧的两个 Func)。开发者需要编写 Game 结构体的 Draw 和 Update 函数,然后交由引擎每秒调用大约 60 次。
avoid-the-enemies 拆解
接下来将以我的生存小游戏为例,讲解一下这个游戏引擎的正确打开方式。
绘制游戏窗口
给定一个宽和高,就可以初始化一个游戏窗口,所有的绘制内容都将以窗口的左上角作为 (0, 0),右为x正轴,下为y正轴控制角色位置。
func (g *Game) Layout(outsideWidth, outsideHeight int) (int, int) {
return screenWidth, screenHeight
}
画面绘制
下面以我编写的游戏为例,讲解一下游戏开发流程。
以像素小人的移动来说,只要我在一秒内不断更换连续的像素图片,遍历对应的索引,并绘制连贯动作的角色图片,就能达到动画的效果了
当然在具体的实现时,我们需要去素材网站上下载对应的图片,在并且了解对应图片的像素大小,如上图每个小人是32px*32px,因此为了达到播放跑动的效果,需要通过循环,不断获取子图,然后在一秒内快速替换。
func (g *Game) Draw(screen *ebiten.Image) {
// ...
// 绘制角色
op := &ebiten.DrawImageOptions{}
op.GeoM.Translate(g.player.x, g.player.y)
i := (g.player.count / 5) % frameCount
sx, sy := frameOX+i*frameWidth, frameOY
screen.DrawImage(runnerImage.SubImage(image.Rect(sx, sy, sx+frameWidth, sy+frameHeight)).(*ebiten.Image), op)
// ...
}
数值状态变更
假设需要实现武器每五秒钟刷新,只需要写一个武器生成器,并在 Update 方法中调用,即可得到武器的数据。再配合上述 Draw 方法,将武器渲染出来即可。
// 生成武器
func GenerateWeapon(g *Game) {
if time.Since(g.weaponTimer) > time.Second*5 {
g.weaponTimer = time.Now()
if len(g.weapons) < 2 {
g.uniqueId++
weapon := weaponList[rand.Intn(len(weaponList))]
switch weapon.(type) {
case *MeleeWeapon:
newWeapon := weapon.(*MeleeWeapon).Copy()
// 使用指针类型有拷贝的bug,当两个人获得同一把武器的时候,旋转会draw两次,所以看起来转速快了一倍
g.weapons[g.uniqueId] = newWeapon
g.weaponPosition[g.uniqueId] = f64.Vec2{rand.Float64() * (screenWidth - frameWidth/2), rand.Float64() * (screenHeight - frameHeight/2)}
case *RangedWeapon:
newWeapon := weapon.(*RangedWeapon).Copy()
g.weapons[g.uniqueId] = newWeapon
g.weaponPosition[g.uniqueId] = f64.Vec2{rand.Float64() * (screenWidth - frameWidth/2), rand.Float64() * (screenHeight - frameHeight/2)}
}
}
}
}
func (g *Game) Update() error {
GenerateWeapon(g)
return nil
}
比如需要绘制子弹的移动轨迹,可以通过一个 map 记录地图上所有子弹起始位置,目标位置,移动速度,子弹贴图等内容,通过计时器配合 Update 方法每秒多次更新子弹的数据,最后再借助 Draw 方法,完成子弹的绘制,达到子弹移动的效果。
小节
游戏开发可能是很多人小时候的一个愿望,白泽也一样,今天实现了,很有趣很开心~
有更多好的 idea 欢迎积极评论。