Golang开发2D小游戏全套教程(已完结)

Golang开发2D小游戏全套教程(已完结)

本套教程将基于ebiten库开发一套2D小游戏。
全套代码:https://github.com/ziyifast/ziyifast-code_instruction/tree/main/game_demo
ebiten官网地址:https://github.com/hajimehoshi/ebiten

说明:本套教程将基于ebiten+darwin开发一套类似雷霆战机的2D小游戏。

1 环境准备 & Hello World

1.1 依赖库安装

OS:Darwin
Go Version大于等于:1.18
本教程基于Mac讲解,其他操作系统类似。

Go version >= 1.18
# 官网地址:https://ebitengine.org/en/documents/install.html?os=darwin
# 安装依赖库
go get -u github.com/hajimehoshi/ebiten/v2
# 验证是否安装成功,如果出现GUI页面表明环境初始成功
go run github.com/hajimehoshi/ebiten/v2/examples/rotate@latest

如果发现报错:build constraints exclude all Go files in xxx

  • 在命令行启用执行:CGO_ENABLED=1,启用CGO

1.2 demo

package mainimport ("github.com/hajimehoshi/ebiten/v2""github.com/hajimehoshi/ebiten/v2/ebitenutil""github.com/ziyifast/log"
)type Game struct {
}func (g *Game) Update() error {return nil
}func (g *Game) Draw(screen *ebiten.Image) {ebitenutil.DebugPrint(screen, "hi~")
}func (g *Game) Layout(outsideWidth, outsideHeight int) (int, int) {return 300, 240
}func main() {ebiten.SetWindowSize(640, 480)ebiten.SetWindowTitle("alien attack")if err := ebiten.RunGame(&Game{}); err != nil {log.Fatal("%v", err)}
}
# 运行程序
go run main.go

效果:
在这里插入图片描述

2 实战RUN Gopher

2.1 最终效果

  • 全套代码地址:https://github.com/ziyifast/ziyifast-code_instruction/tree/main/game_demo

在这里插入图片描述

2.2 思路

  1. 程序入口:ebiten.RunGame(model.NewGame())
  • 定义model里的Game类,需要实现ebiten中Game这个interface
    • Update() error:程序会每隔一定时间进行刷新,里面定义刷新逻辑,包括怪物的移动(调整每个怪物的x、y轴坐标),gopher(玩家)的移动,子弹的移动等
    • Draw(screen *Image):通过Update调整好坐标以后,再将怪物、子弹、以及玩家调用Draw方法画到屏幕上
    • Layout(outsideWidth, outsideHeight int) (screenWidth, screenHeight int):设置页面的布局及大小
  1. 定义model.Game结构体、同时实现Update、Draw等方法
  • input *Input:用于监听用户按键,比如:按下空格表示游戏开始
  • ship *Ship:玩家角色
  • config *config.Config:配置文件(定义玩家移动速度、怪物移动速度、游戏标题等)
  • bullets map[*Bullet]struct{}:存储游戏中的子弹
  • monsters map[*Monster]struct{}:存储游戏中的怪物
  • mode Mode:标识当前游戏是待开始、已开始、已结束
  • failedCountLimit int:最多能漏掉多少个怪物
  • failedCount int:当前已经漏掉的怪物个数
  • func1:添加init方法,包括初始化怪物的个数、玩家的位置等
  • func2:实现Update方法:用于更新怪物、玩家、子弹的位置
  • func3:实现Draw方法,重新渲染页面,实现页面动态效果
  1. 定义GameObj结构体(子弹、怪物、用户角色都需要用到宽高、以及x、y坐标,所以可以抽取出一个Obj)
  • width int
  • height int
  • x int
  • y int
  • func1:Width() int
  • func2:Height() int
  • func3:X() int
  • func4:Y() int
  1. 定义model.Bullet
  • GameObj 包含x、y坐标(方便后续移动子弹)
  • image:子弹的样式
  • speedFactor:子弹的移动速度
  • fun1:NewBullet
  • func2:实现自己的Draw方法
  • func3:outOfScreen,判断子弹是否移出了屏幕。当子弹超出屏幕时,应当删除,不再维护。
  1. 定义model.Monster(类比model.Bullet,此处包含怪物的样式、移动速度同时通过GameObj维护怪物坐标x、y)
  • GameObj
  • img *ebiten.Image
  • speedFactor int
  • fun1:NewMonster
  • func2:Draw
  • func3:OutOfScreen
  1. 定义model.Ship(类比model.Bullet,此处包含用户角色样式、移动速度)
  • GameObj
  • img *ebiten.Image
  • speedFactor int
  • fun1:NewShip
  • func2:Draw

tips:

  • 游戏胜负判定规则:
  1. 胜利win:
    • 遗漏的怪物数<=N(配置文件配置)
  2. 失败lose:
    • 飞船(用户角色碰到怪物)
    • 遗漏掉太多怪物

项目结构
在这里插入图片描述

2.3 代码

①game_demo/config/config.go
package configimport ("encoding/json""github.com/ziyifast/log""image/color""os"
)type Config struct {ScreenWidth        int        `json:"screen_width"`ScreenHeight       int        `json:"screen_height"`Title              string     `json:"title"`BgColor            color.RGBA `json:"bg_color"`MoveSpeed          int        `json:"move_speed"`BulletWidth        int        `json:"bullet_width"`BulletHeight       int        `json:"bullet_height"`BulletSpeed        int        `json:"bullet_speed"`BulletColor        color.RGBA `json:"bullet_color"`MaxBulletNum       int        `json:"max_bullet_num"`  //页面中最多子弹数量BulletInterval     int64      `json:"bullet_interval"` //发射子弹间隔时间MonsterSpeedFactor int        `json:"monster_speed_factor"`TitleFontSize      int        `json:"title_font_size"`FontSize           int        `json:"font_size"`SmallFontSize      int        `json:"small_font_size"`FailedCountLimit   int        `json:"failed_count_limit"` //最多能遗漏多少怪物
}func LoadConfig() *Config {file, err := os.Open("./config.json")if err != nil {log.Fatalf("%v", err)}defer file.Close()config := new(Config)err = json.NewDecoder(file).Decode(config)if err != nil {log.Fatalf("%v", err)}return config
}
②game_demo/model/bullet.go
package modelimport ("github.com/hajimehoshi/ebiten/v2""image""ziyi.game.com/config"
)type Bullet struct {GameObjimage       *ebiten.ImagespeedFactor int
}// NewBullet 添加子弹
func NewBullet(cfg *config.Config, ship *Ship) *Bullet {rect := image.Rect(0, 0, cfg.BulletWidth, cfg.BulletHeight)img := ebiten.NewImageWithOptions(rect, nil)img.Fill(cfg.BulletColor)b := &Bullet{image:       img,speedFactor: cfg.BulletSpeed,}b.GameObj.width = cfg.BulletWidthb.GameObj.height = cfg.BulletHeightb.GameObj.y = ship.Y() + (ship.Height()-cfg.BulletHeight)/2b.GameObj.x = ship.X() + (ship.Width()-cfg.BulletWidth)/2return b
}func (b *Bullet) Draw(screen *ebiten.Image) {op := &ebiten.DrawImageOptions{}op.GeoM.Translate(float64(b.X()), float64(b.Y()))screen.DrawImage(b.image, op)
}func (b *Bullet) outOfScreen() bool {return b.Y() < -b.Height()
}
③game_demo/model/entity.go
package modeltype Entity interface {Width() intHeight() intX() intY() int
}
④game_demo/model/game.go
package modelimport ("github.com/hajimehoshi/ebiten/v2""github.com/hajimehoshi/ebiten/v2/examples/resources/fonts""github.com/hajimehoshi/ebiten/v2/text""github.com/ziyifast/log""golang.org/x/image/font""golang.org/x/image/font/opentype""image/color""math/rand""time""ziyi.game.com/config"
)type Mode intconst (ModeTitle Mode = iotaModeGameModeOver
)var r *rand.Randfunc init() {source := rand.NewSource(time.Now().UnixMicro())r = rand.New(source)
}type Game struct {input            *Inputship             *Shipconfig           *config.Configbullets          map[*Bullet]struct{}monsters         map[*Monster]struct{}mode             ModefailedCountLimit intfailedCount      int
}func (g *Game) init() {g.mode = ModeTitleg.failedCount = 0g.bullets = make(map[*Bullet]struct{})g.monsters = make(map[*Monster]struct{})g.ship = NewShip(g.config.ScreenWidth, g.config.ScreenHeight)g.createMonsters()
}func NewGame() *Game {c := config.LoadConfig()//set window size & titleebiten.SetWindowSize(c.ScreenWidth, c.ScreenHeight)ebiten.SetWindowTitle(c.Title)g := &Game{input:            &Input{},ship:             NewShip(c.ScreenWidth, c.ScreenHeight),config:           c,bullets:          make(map[*Bullet]struct{}),monsters:         make(map[*Monster]struct{}),failedCount:      0,failedCountLimit: c.FailedCountLimit,}//初始化外星人g.createMonsters()g.CreateFonts()return g
}func (g *Game) Draw(screen *ebiten.Image) {var titleTexts []stringvar texts []stringswitch g.mode {case ModeTitle:titleTexts = []string{"RUN GOPHER"}texts = []string{"", "", "", "", "", "", "", "PRESS SPACE KEY", "", "OR LEFT MOUSE"}case ModeGame://set screen colorscreen.Fill(g.config.BgColor)//draw gopherg.ship.Draw(screen, g.config)//draw bulletfor b := range g.bullets {b.Draw(screen)}//draw monstersfor a := range g.monsters {a.Draw(screen)}case ModeOver:screen.Fill(color.Black)g.Update()texts = []string{"", "GAME OVER!"}}for i, l := range titleTexts {x := (g.config.ScreenWidth - len(l)*g.config.TitleFontSize) / 2text.Draw(screen, l, titleArcadeFont, x, (i+4)*g.config.TitleFontSize, color.RGBA{R: 0,G: 100,B: 0,A: 0,})}for i, l := range texts {x := (g.config.ScreenWidth - len(l)*g.config.FontSize) / 2text.Draw(screen, l, arcadeFont, x, (i+4)*g.config.FontSize, color.RGBA{R: 0,G: 100,B: 0,A: 0,})}
}func (g *Game) Layout(outsideWidth, outsideHeight int) (int, int) {return g.config.ScreenWidth, g.config.ScreenHeight
}func (g *Game) Update() error {switch g.mode {case ModeTitle:if g.input.IsKeyPressed() {g.mode = ModeGame}case ModeGame:g.input.Update(g)//更新子弹位置for b := range g.bullets {if b.outOfScreen() {delete(g.bullets, b)}b.y -= b.speedFactor}//更新敌人位置for a := range g.monsters {a.y += a.speedFactor}//检查是否击相撞(击中敌人)g.CheckKillMonster()//外星人溜走 或者 是否飞机碰到外星人if g.failedCount >= g.failedCountLimit || g.CheckShipCrashed() {g.mode = ModeOverlog.Warnf("over..........")}go func() {if len(g.monsters) < 0 {//下一波怪物g.createMonsters()}}()case ModeOver://游戏结束,恢复初始状态if g.input.IsKeyPressed() {g.init()g.mode = ModeTitle}}return nil}func (g *Game) addBullet(bullet *Bullet) {g.bullets[bullet] = struct{}{}
}func (g *Game) createMonsters() {a := NewMonster(g.config)//怪物之间需要有间隔availableSpaceX := g.config.ScreenWidth - 2*a.Width()numMonsters := availableSpaceX / (2 * a.Width())//预设怪物数量for i := 0; i < numMonsters; i++ {monster := NewMonster(g.config)monster.x = monster.Width() + 2*monster.Width()*imonster.y = monster.Height() + r.Intn(g.config.ScreenHeight/10)g.addMonsters(monster)}
}func (g *Game) addMonsters(monster *Monster) {g.monsters[monster] = struct{}{}
}func (g *Game) CheckKillMonster() {for monster := range g.monsters {for bullet := range g.bullets {if checkCollision(bullet, monster) {delete(g.monsters, monster)delete(g.bullets, bullet)}}if monster.OutOfScreen(g.config) {g.failedCount++delete(g.monsters, monster)}}
}func (g *Game) CheckShipCrashed() bool {for monster := range g.monsters {if checkCollision(g.ship, monster) {return true}}return false
}// 检测子弹是否击中敌人
func checkCollision(entity1 Entity, entity2 Entity) bool {//只需要计算子弹顶点在敌人矩形之中,就认为击中敌人entity2Top := entity2.Y()entity2Left := entity2.X()entity2Bottom := entity2.Y() + entity2.Height()entity2Right := entity2.X() + entity2.Width()x, y := entity1.X(), entity1.Y()//击中敌人左上角if x > entity2Left && x < entity2Right && y > entity2Top && y < entity2Bottom {return true}//击中敌人右上角x, y = entity1.X(), entity1.Y()+entity1.Height()if x > entity2Left && x < entity2Right && y > entity2Bottom && y < entity2Top {return true}//左下角x, y = entity1.X()+entity1.Width(), entity1.Y()if y > entity2Top && y < entity2Bottom && x > entity2Left && x < entity2Right {return true}//右下角x, y = entity1.X()+entity1.Width(), entity1.Y()+entity1.Height()if y > entity2Top && y < entity2Bottom && x > entity2Left && x < entity2Right {return true}return false
}//加载页面字体var (titleArcadeFont font.FacearcadeFont      font.FacesmallArcadeFont font.Face
)// CreateFonts 初始化页面字体信息
func (g *Game) CreateFonts() {tt, err := opentype.Parse(fonts.PressStart2P_ttf)if err != nil {log.Fatalf("%v", err)}const dpi = 72titleArcadeFont, err = opentype.NewFace(tt, &opentype.FaceOptions{Size:    float64(g.config.TitleFontSize),DPI:     dpi,Hinting: font.HintingFull,})if err != nil {log.Fatal(err)}arcadeFont, err = opentype.NewFace(tt, &opentype.FaceOptions{Size:    float64(g.config.FontSize),DPI:     dpi,Hinting: font.HintingFull,})if err != nil {log.Fatal(err)}smallArcadeFont, err = opentype.NewFace(tt, &opentype.FaceOptions{Size:    float64(g.config.SmallFontSize),DPI:     dpi,Hinting: font.HintingFull,})if err != nil {log.Fatal(err)}
}
⑤game_demo/model/game_obj.go
package model// GameObj 后续除了普通敌人还可能有其他小boss,因此我们直接将所有物体抽象出来
type GameObj struct {width  intheight intx      inty      int
}func (o *GameObj) Width() int {return o.width
}func (o *GameObj) Height() int {return o.height
}func (o *GameObj) X() int {return o.x
}func (o *GameObj) Y() int {return o.y
}
⑥game_demo/model/input.go
package modelimport ("github.com/hajimehoshi/ebiten/v2""time"
)type Input struct {lastBulletTime time.Time //上次子弹发射时间,避免用户一直按着连续发子弹
}func (i *Input) IsKeyPressed() bool {//按下空格或者鼠标左键,游戏开始if ebiten.IsKeyPressed(ebiten.KeySpace) || ebiten.IsMouseButtonPressed(ebiten.MouseButtonLeft) {return true}return false
}func (i *Input) Update(g *Game) {cfg := g.configs := g.ship//listen the key eventif ebiten.IsKeyPressed(ebiten.KeyLeft) {s.GameObj.x -= cfg.MoveSpeed//防止飞船跑出页面 prevents movement out of the pageif s.X() < -s.Width()/2 {s.x = -s.Width() / 2}} else if ebiten.IsKeyPressed(ebiten.KeyRight) {s.GameObj.x += cfg.MoveSpeedif s.X() > cfg.ScreenWidth-s.Width()/2 {s.GameObj.x = cfg.ScreenWidth - s.Width()/2}}if ebiten.IsKeyPressed(ebiten.KeySpace) {if len(g.bullets) < cfg.MaxBulletNum && time.Since(i.lastBulletTime).Milliseconds() > cfg.BulletInterval {//发射子弹bullet := NewBullet(cfg, s)g.addBullet(bullet)i.lastBulletTime = time.Now()}}}
⑦game_demo/model/monster.go
package modelimport ("github.com/hajimehoshi/ebiten/v2""github.com/hajimehoshi/ebiten/v2/ebitenutil""github.com/ziyifast/log""ziyi.game.com/config"
)type Monster struct {GameObjimg         *ebiten.ImagespeedFactor int
}func NewMonster(cfg *config.Config) *Monster {image, _, err := ebitenutil.NewImageFromFile("/Users/ziyi2/GolandProjects/MyTest/demo_home/game_demo/images/monster.bmp")if err != nil {log.Fatal("%v", err)}width, height := image.Bounds().Dx(), image.Bounds().Dy()a := &Monster{img:         image,speedFactor: cfg.MonsterSpeedFactor,}a.GameObj.width = widtha.GameObj.height = heighta.GameObj.x = 0a.GameObj.y = 0return a
}func (a *Monster) Draw(screen *ebiten.Image) {op := &ebiten.DrawImageOptions{}op.GeoM.Translate(float64(a.X()), float64(a.Y()))screen.DrawImage(a.img, op)
}func (a *Monster) OutOfScreen(cfg *config.Config) bool {if a.Y()+a.Height() > cfg.ScreenHeight {return true}return false
}
⑧game_demo/model/ship.go
package modelimport ("github.com/hajimehoshi/ebiten/v2""github.com/hajimehoshi/ebiten/v2/ebitenutil""github.com/ziyifast/log"_ "golang.org/x/image/bmp""ziyi.game.com/config"
)type Ship struct {GameObjimage *ebiten.Image
}func NewShip(screenWidth, screenHeight int) *Ship {image, _, err := ebitenutil.NewImageFromFile("/Users/ziyi2/GolandProjects/MyTest/demo_home/game_demo/images/ship.bmp")if err != nil {log.Fatalf("%v", err)}width, height := image.Bounds().Dx(), image.Bounds().Dy()s := &Ship{image: image,}s.GameObj.width = widths.GameObj.height = heights.GameObj.x = screenWidth / 2s.GameObj.y = screenHeight - heightreturn s
}func (ship *Ship) Draw(screen *ebiten.Image, cfg *config.Config) {// draw by selfop := &ebiten.DrawImageOptions{}//init ship at the screen centerop.GeoM.Translate(float64(ship.X()), float64(ship.Y()))screen.DrawImage(ship.image, op)
}
⑨game_demo/config.json
{"screen_width": 640,"screen_height": 480,"title": "ziyi game","bg_color": {"r": 255,"g": 255,"b": 255,"a": 0},"move_speed": 2,"bullet_speed": 5,"bullet_width": 5,"bullet_height": 7,"bullet_color": {"r": 80,"g": 80,"b": 80,"a": 255},"max_bullet_num": 10,"bullet_interval": 50,"monster_speed_factor": 1,"title_font_size": 15,"font_size": 8,"small_font_size": 3,"failed_count_limit": 5
}
⑩game_demo/main.go
package mainimport ("github.com/hajimehoshi/ebiten/v2""github.com/ziyifast/log""ziyi.game.com/model"
)func main() {err := ebiten.RunGame(model.NewGame())if err != nil {log.Fatal("%v", err)}
}

运行项目:

go run main.go

参考文章:https://juejin.cn/post/7174070809864962055

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/596421.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

单调栈(LeetCode-下一个更大元素)

每日一题 今天刷到了一道用到单调栈来解决的题目&#xff0c;想到自己没有总结过单调栈的知识点&#xff0c;因此想总结一下。 介绍 什么是单调栈&#xff1f; 单调栈的定义其实很简单&#xff0c;所谓单调栈就是指一个单调递增或是单调递减的栈。 那单调栈有什么用呢&#x…

在单交换机局域网中,不同网段的主机通信探秘

在理解局域网中不同网段主机之间的通信之前&#xff0c;我们首先要明白网络的基本组成和工作原理。局域网&#xff08;LAN&#xff09;是一个封闭的网络环境&#xff0c;通常由交换机&#xff08;Switch&#xff09;作为核心设备连接网络中的各个主机。当我们谈论不同网段的主机…

基于Socket简单的TCP网络程序

⭐小白苦学IT的博客主页 ⭐初学者必看&#xff1a;Linux操作系统入门 ⭐代码仓库&#xff1a;Linux代码仓库 ❤关注我一起讨论和学习Linux系统 TCP单例模式的多线程版本的英汉互译服务器 我们先来认识一下与udp服务器实现的不同的接口&#xff1a; TCP服务器端 socket()&…

阿里云8核32G云服务器租用优惠价格表,包括腾讯云和京东云

8核32G云服务器租用优惠价格表&#xff0c;云服务器吧yunfuwuqiba.com整理阿里云8核32G服务器、腾讯云8核32G和京东云8C32G云主机配置报价&#xff0c;腾讯云和京东云是轻量应用服务器&#xff0c;阿里云是云服务器ECS&#xff1a; 阿里云8核32G服务器 阿里云8核32G服务器价格…

通用爬虫的概念简述

一、&#x1f308;什么是通用爬虫 通用爬虫&#xff08;General Purpose Web Crawler或Scalable Web Crawler&#xff09;是一种网络爬虫&#xff0c;其设计目标是对整个互联网或尽可能广泛的网络空间进行数据抓取。通用爬虫主要用于搜索引擎构建其庞大的网页索引数据库&#…

《债务与国家的崛起》西方民主制度的金融起源 - 三余书屋 3ysw.net

债务与国家的崛起&#xff1a;西方民主制度的金融起源 你好&#xff0c;今天我们来聊聊由英国知名经济与金融历史学家詹姆斯麦克唐纳所著的《债务与国家的崛起》这本书。19世纪世界历史上发生了一次巨变&#xff0c;即“大分流”。当时西方通过工业革命实现了科技和经济的飞速…

【开源语音项目OpenVoice](一)——实操演示

目录 一、前菜 1、Python选择 2、pip源切换 3、ffmpeg配置问题 4、VSCode添加Jupyter扩展 二、配置虚拟环境 1、下载源码 方法一 直接下载源码压缩包 方法二 使用git 1&#xff09;git加入鼠标右键 2&#xff09;git clone源码 2、VSCode出场 1&#xff09;创建pyth…

Bayes-RF,基于贝叶斯Bayes优化算法优化随机森林RF分类预测(二分类及多分类皆可)-附代码

Bayesian Optimization&#xff08;贝叶斯优化&#xff09;是一种用于超参数调优的技术&#xff0c;对于类似随机森林&#xff08;Random Forest&#xff0c;简称RF&#xff09;的机器学习算法非常重要。随机森林是一种集成学习方法&#xff0c;它在训练过程中构建多个决策树&a…

How-Google-Tests-Software_Google软件测试之道_英文版pdf

我看How-Google-Tests-Software_Google软件测试之道_英文版pdf挺难找的 在这里分享一下 链接: https://pan.baidu.com/s/1bTafUY4CFcMVxrNrdBp7Zg 提取码: 5q4p 复制这段内容后打开百度网盘手机App&#xff0c;操作更方便哦

【Python使用】嘿马头条完整开发md笔记第4篇:数据库,1 方案选择【附代码文档】

嘿马头条项目从到完整开发笔记总结完整教程&#xff08;附代码资料&#xff09;主要内容讲述&#xff1a;课程简介&#xff0c;ToutiaoWeb虚拟机使用说明1 产品介绍,2 原型图与UI图,3 技术架构,4 开发,1 需求,2 注意事项。数据库&#xff0c;理解ORM1 简介,2 安装,3 数据库连接…

计算机网络——37认证

认证 目标&#xff1a;Bob需要Alice证明他的身份 Protocol ap1.0&#xff1a;Alice说"A am Alice" 可能出现的问题&#xff1a; 在网络上Bob看不到Alice&#xff0c;因此Trudy可以简单的声称他是Alice 认证&#xff1a;重新尝试 Protocol ap2.0&#xff1a;Alice…

Unity和Android的交互

Unity和Android的交互 一、前言二、Android导出jar/aar包到Unity2.1 版本说明2.2 拷贝Unity的classes.jar给Android工程2.2.1 classes.jar的位置2.2.2 Android Studio创建module2.2.3 拷贝classes.jar 到 Android工程并启用 2.3 编写Android工程代码2.3.1 创建 MainActivity2.…