Go语言写小游戏的步骤

1分类文件包

比如自己要写游戏写Config、GameFrame、Image、Model、Music、Tool等文件夹以后后续要创建文件的时候把他们放置在对应的包中,这样后续管理维护、或者寻找代码的时候会方便很多

2实现游戏要的窗口

写游戏的第一步必然是要先把窗口给实现出来我用的是一起用Go做一个小游戏(上) - 知乎 (zhihu.com)这个网站的教学,直接看我的也可能看不懂,看一下呢个链接之后,然后以我的总结来做一个参考或者补充。接下来就继续看我的总结。可以先像他一样用一个Game结构体把update\draw\layout三个方法都实现,并赋予参数,首先用layout以及newGame里面的ebiten调用的一些方法,方法传递给ebiten,ebiten就能把窗口初始化出来,先把窗口显现出来,游戏就成功一半了。

想要使用ebiten的情况下

ebitengine 要求Go版本 >= 1.15。使用go module下载这个包:

$ go get -u github.com/hajimehoshi/ebiten/v2
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 1280, 750}
func main() {
  ebiten.SetWindowSize(1280, 750)
  ebiten.SetWindowTitle("低配版天天酷跑")
  if err := ebiten.RunGame(&Game{}); err != nil {
    log.Fatal(err)
  }}

3开始添加基本功能让人物展现在页面

人物场景的出现,Game结构体就是写在GameFrame他就是整个游戏的框架,也就是这个游戏中其他有关游戏的代码都要转接到这个框架结构体中,都是为他服务,他再为ebiten引擎服务,之后再出现游戏界面。

假设说我现在想要在窗口的右下角出现一个人物,那我就在Model包中创建human文件,并在文件中创建human结构体并将该结构体添加到Game结构体中,human结构体中书写这个人物的信息,之后用一个newHuman来初始化它的信息,再去初始化Game结构体中的newGame方法中用Game.human.newHuman()把human的初始化方法传到框架中,当人物结构体的参数已经初始化完成,在human方法中在写一个draw方法,这个方法中写用引擎如何接收人物结构体中的信息,  我这里是找了一个protect的名字代替了human了

type Protect struct {
   Image    *ebiten.Image
   LastTime time.Time
   Display  bool
   X        int
   Y        int
}

func NewProtect(screenWidth, screenHeight int) *Protect {
   protectpng, _, err := ebitenutil.NewImageFromFile("./src/images/protect.png")
   if err != nil {
      log.Fatal(err)
   }

   protect := &Protect{
      Image:    protectpng,
      LastTime: time.Now(),
      Display:  true,
      X:        0,
      Y:        0,
   }
   return protect
}

// 单独书写一个声明值的方法,方便后续管理
func (protect *Protect) Draw(screen *ebiten.Image, cfg *Config.Config, r *Realm) {
   op := &ebiten.DrawImageOptions{}
   protect.X--
   op.GeoM.Translate(float64(protect.X), float64(protect.Y))
   screen.DrawImage(protect.Image, op)
  
}

然后画在屏幕的哪一个地方。完成之后就把这个人物结构体的draw方法,再回调到Game结构体中的draw里面去使用这个方法,(后续图片有展示)这样子当游戏开始的时候就会从main方法到Game再到human,就可以成功把human里面的代码实现了。

4实现让人物可以移动就用到update方法

想要实现让人物移动,那么就要使用到update方法了,他和draw还有layout是一样的,都是这个ebiten引擎的首要部分,他们是不需要调用的。Ebiten引擎在启动游戏循环的时候会在合适时机调用实现了Game接口的类型的方法。它会在每一帧渲染之前调用Update方法,然后渲染帧之后调用Draw方法。所以Update方法就是用来更新其他结构体的参数的,比如我在Update中写human.X--那么游戏开始的时候这个人物的X值就会每一帧都--,每一帧X--了之后,human.draw方法都会根据human的X值重新渲染这个图像,也就是人眼所看到的human的移动了。 实现智能化,就是添加一个键盘监听,就能让人根据自己的意愿实现移动,或者说使用算法,让里面的场景实现智能化。

//将游戏核心逻辑转移到game.go文件中,定义游戏对象结构和创建游戏对象的方法

// ebiten引擎传入的游戏对象必须要实现一个接口,让这个接口实现Game结构体的所有方法
// 这个接口说的是ebiten.RunGame(&GameFrame{})里面传参的这个Ganme类
// 让给Game结构体作为包含所有子结构体的,也算是项目的中间轴承一样的作用。
type Game struct {
	mode      Mode
	input     *Input
	realm     *model.Realm
	beijing   *model.Beijing
	live      *model.Live
	protect   *model.Protect
	obstacle  *model.Obstacle
	prop      *model.Prop
	Cfg       *Config.Config
	bullets   map[*model.Bullet]struct{}
	overMsg   string
	RealmDist float64
}

func NewGame() *Game {
	cfg := Config.LoadConfig()
	ebiten.SetWindowSize(cfg.ScreenWidth, cfg.ScreenHeight)
	ebiten.SetWindowTitle(cfg.Title)
	g := &Game{
		input:     &Input{Msg: "HelloWorld"},
		realm:     model.NewRealm(cfg.ScreenWidth, cfg.ScreenHeight),
		beijing:   model.NewBeijing(cfg.ScreenWidth, cfg.ScreenHeight),
		live:      model.NewLive(cfg.ScreenWidth, cfg.ScreenHeight),
		protect:   model.NewProtect(cfg.ScreenWidth, cfg.ScreenHeight),
		obstacle:  model.ObstacleInit(),
		prop:      model.NewProp(cfg.ScreenWidth, cfg.ScreenHeight),
		bullets:   make(map[*model.Bullet]struct{}),
		Cfg:       cfg,
		RealmDist: 0,
	}
	tool.NewMusic()
	tool.Play1music.Play()
	g.CreateFonts()
	return g
}

后续添加功能就是和第四个类似添加新的结构体,然后把这个结构体加入到主框架结构体Game里面,之后再把这个结构体的信息给初始化,这个结构体想要实现的相关功能就在自己的go文件内部实现。还有想要让它展现在屏幕上要实现的三个方法。Draw、updata。

5工具类的代码书写

当然也有一些go文件是没写结构体的。就是比如在工具中的,这个文件中仅仅放置了方法,方法内部传的参数也是其他结构体的内容,用来改变其他结构体的参数。所以它不需要自己的结构体。像是后面的音乐还有碰撞方法都是再tool内部放置的,可以反复使用的的功能就放置在哪里。比如碰撞把每个可能会发生碰撞的结构体内部都会有物体的x,y坐标以及宽度和高度,只要把结构体中的这些变量再用一个结构体包裹起来,然后碰撞方法里面面的参数类型就写呢个只有x,y,height,weight四个变量类型的结构体就可以把碰撞多次实现了。(我写的是因为碰撞只有人物和障碍物,所以就没有用结构体去包裹他们的内容,就直接把类型写了人物和障碍物的结构类型了)

//CheckCollision检查人物和障碍物之间是否有碰撞
//写这个碰撞检测,如果存在可能是一个障碍物完全包裹另外一个的情况的下,就需要把可能面积小的呢个物体
//放置在第一个参数,因为检测的是,第一个参数的四个角是否有一个存在于第二个参数之中
func CheckCollision(obstacle *model.Obstacle, i int, realm *model.Realm) bool {
	//障碍物的四个角的坐标
	obstacleLeftUpX, obstacleLeftUpY := obstacle.X[i], obstacle.Y[i]
	obstacleLeftDownX, obstacleLeftDownY := obstacle.X[i], obstacle.Y[i]+obstacle.Height[i]
	obstacleRightUPX, obstacleRightUPY := obstacle.X[i]+obstacle.Width[i], obstacle.Y[i]
	obstacleRightDownX, obstacleRightDownY := obstacle.X[i]+obstacle.Width[i], obstacle.Y[i]+obstacle.Height[i]
	//障碍物左上角
	if CheckColl(obstacleLeftUpX, obstacleLeftUpY, realm) {
		return true
	}
	//障碍物左下角
	if CheckColl(obstacleLeftDownX, obstacleLeftDownY, realm) {
		return true
	}
	//障碍物右上角
	if CheckColl(obstacleRightUPX, obstacleRightUPY, realm) {
		return true
	}
	//障碍物右下角
	if CheckColl(obstacleRightDownX, obstacleRightDownY, realm) {
		//fmt.Println(obstacleRightDownX, obstacleRightDownY)
		return true
	}

	return false
}

// 判断障碍物的四个角是否有一个出现在人的内部,
//但是人站立的时候面积是大于障碍物,如果人是比障碍物小的(下蹲)那就有bug了
//这样障碍物就会包裹住人,也就是虽然有接触,但是障碍物的四个点都不在人的内部,也就无法成功
//碰撞检测
// 解决这个bug,让人下蹲的时候,只要比障碍物高就行了
func CheckColl(X, Y float64, realm *model.Realm) bool {
	//realm的四个角的坐标
	LeftX := realm.X + 40
	RightX := realm.X + float64(realm.Width[realm.State]) - 40
	UPY := realm.Y[realm.State]
	DownY := realm.Y[realm.State] + float64(realm.Height[realm.State]) - 40

	//判断这个点是否在Realm的长方体之内
	if X >= LeftX && X <= RightX {
		if Y <= DownY && Y >= UPY {
			return true
		}
	}
	return false
}

6游戏的趣味性书写

然后之后就是用自己的算法来让这个游戏的功能更加丰富,比如说我的跑酷游戏就是想要让后面的场景实现无线循环,直接找到一个无限长度的背景图片过于困难,我就找了一张首尾呢个够无缝衔接的图片,然后让这两张图片给渲染连接起来,然后因为同一时间,窗口只能显示一张图片,所以就让当图片改出现第三张的时候,就让两张连接起来的图片给整体右移一个图片大小的宽度,这样就是第一张变成第二张,第二张变成第三张了。之后在遇到第四张的时候就再同样的原理改变背景的X坐标。

比如说一张图片时400宽度,两张图片合起来就是800,我设置图片的最左边为x坐标,在每一帧的时候x就能右移动一部分。当x坐标移动到400的时候做一个判断,在判断里面在x=0,也就是说场景每移动到第二张的时候,就会有判断条件把它改成第一张,但是第一张和第二张的图片时一样的。一帧内的切换,人眼也看不出来,这样也就实现了场景无线循环的功能。

其他陆续的功能都是这样,首先用算法把想法实现,把实现的结果调用到update方法中用来更新结果,之后在draw里面把更新的参数给展示出来。这样新的功能就实现了。

//一个用于后续使用随机数的两个方法
var randSeed = rand.NewSource(time.Now().UnixNano())
var randGen = rand.New(randSeed)

type Obstacle struct {
	Image        []*ebiten.Image
	Width        []float64
	Height       []float64
	X            []float64
	Y            []float64
	ObstacleType []int
}

func ObstacleInit() *Obstacle {
	img1, _, err := ebitenutil.NewImageFromFile("./src/images/dimian.png")
	if err != nil {
		log.Fatal(err)
	}
	img2, _, err := ebitenutil.NewImageFromFile("./src/images/kongzhong.png")
	if err != nil {
		log.Fatal(err)
	}
	width1, height1 := img1.Size()
	width2, height2 := img2.Size()
	//fmt.Println(width1, height1)240 258
	//fmt.Println(width2, height2)	240 420
	obs := &Obstacle{
		Image:        []*ebiten.Image{img1, img2},
		Width:        []float64{float64(width1), float64(width2)},
		Height:       []float64{float64(height1), float64(height2)},
		X:            []float64{550, 1100},
		Y:            []float64{500, 120}, //315
		ObstacleType: []int{0, 1},
	}
	return obs
}
func (Obstacle *Obstacle) Draw(screen *ebiten.Image, cfg *Config.Config) {
	op1 := &ebiten.DrawImageOptions{}
	//index为0的障碍物
	op1.GeoM.Translate(Obstacle.X[0], Obstacle.Y[0])
	Obstacle.X[0] -= cfg.ObstacleSpeed
	//如果障碍物完全离开屏幕,则重新生成障碍物
	if Obstacle.X[0]+Obstacle.Width[1] <= 0 {
		Obstacle.X[0] = float64(randGen.Intn(600)) + 900 //1		280--1680
		juddis(&Obstacle.X[0], &Obstacle.X[1])
	}

	//index为1的障碍物
	op2 := &ebiten.DrawImageOptions{}
	op2.GeoM.Translate(Obstacle.X[1], Obstacle.Y[1])
	Obstacle.X[1] -= cfg.ObstacleSpeed
	//如果障碍物完全离开屏幕,则重新生成障碍物
	if Obstacle.X[1]+Obstacle.Width[1] <= 0 {
		Obstacle.X[1] = float64(randGen.Intn(600)) + 900 //1280---2080
		juddis(&Obstacle.X[1], &Obstacle.X[0])
	}
	screen.DrawImage(Obstacle.Image[0], op1)
	screen.DrawImage(Obstacle.Image[1], op2)

}

// 写一个判断方法,如果新生成的障碍物和另外一个障碍物的距离过近的话,就让他改变位置
func juddis(a *float64, b *float64) {
	if *a >= *b {
		if *a-*b < 400 {
			*a = 560 + *b
			fmt.Printf("1")
		}
	} else {
		//主要是新创建的又概率是在上一个创建的前面,
		//如果是这样的话,就要进行接下来的操作。
		if *b-*a < 400 {
			*b = 600 + *a
			fmt.Printf("2")
		}
	}
}

6当游戏功能完成实现的时候,完善游戏进入前进入后的画面渲染

就开始些整体结构的问题了,就是游戏完成了,但是游戏的进入前,以及游戏结束后,窗口又应该展现出什么东西?所以就可以把刚才实现的游戏功能可以理解为是封装起来了。然后再把游戏的状态再分个游戏前、游戏中、游戏后分三个比如我写的就是  用一个switch  case三个状态:ModeTitle、ModeGame、ModeOver三个,然后游戏的中心还是ebiten引擎的update和draw方法,就把update和draw里面都是相互照应的,我把之前写的都是游戏中的功能,把功能给写道case : ModeGame   把游戏前的东西放置到ModeTitle这个时候不会调用ModeGame的内容也就不会显示它的东西,这个时候就显示游戏前的标题或者介绍,再写一个键盘监听,如果摁下空格那么g.mode 就会便变为ModeGame了。那么下一帧的时候就不会再调用游戏前的功能,而是游戏中的。当人物的血耗尽的时候也是一样一个判断内部把g.mode给改成ModeOver,在对应dupdata中的ModeOver写键盘监听,触发就让g.mode再发生改变。因为update和draw都是会每一帧都运行嘛,所以只要有条件判断更改g.mode 的时候,那么下一帧调用update和draw的时候就会根据switch去选择性的调用相应的功能啦。

func (g *Game) Draw(screen *ebiten.Image) {
	screen.Fill(g.Cfg.BgColor)
	/**
	绘制一下游戏开始以及结束时候的文字显示
	启动时游戏处于ModeTitle状态,处于ModeTitle和ModeOver时只需要在屏幕上显示一些文字即可。
	只有在ModeGame状态才需要显示飞船和外星人:
	*/
	var titleTexts []string //它是一个切片  未定义数组长度
	var texts []string
	switch g.mode {
	case ModeTitle:
		titleTexts = []string{"ALIEN INVASION"}
		texts = []string{"", "", "", "", "", "", "", "PRESS SPACE KEY", "", "OR LEFT MOUSE"}
	case ModeGame:
		g.beijing.Draw(screen, g.Cfg, g.realm)
		g.obstacle.Draw(screen, g.Cfg)
		g.prop.Draw(screen, g.Cfg, g.realm)
		g.live.Draw(screen, g.Cfg, g.realm)
		g.protect.Draw(screen, g.Cfg, g.realm)
		g.realm.Draw(screen, g.Cfg)
		for bullet := range g.bullets {
			bullet.Draw(screen)
		}
		Str := "distant:" + strconv.FormatFloat(g.RealmDist, 'f', -1, 64)
		x := g.Cfg.ScreenWidth - len(Str)*g.Cfg.TitleFontSize
		//fmt.Println(x)
		text.Draw(screen, Str, titleArcadeFont, x, 100, color.White)
	case ModeOver:
		texts = []string{"", g.overMsg}
	}
	for i, l := range titleTexts {
		//。x 变量用于计算文本的水平位置,以便将文本居中对齐
		x := (g.Cfg.ScreenWidth - len(l)*g.Cfg.TitleFontSize) / 2
		//。text.Draw 函数用于在屏幕上绘制文本   i表示的是索引,l才是元素对象
		/**
		text.Draw 函数的第一个参数是要绘制的文本,第二个参数是字体对象,
		第三个参数是字体大小,第四个参数是文本的水平位置,第五个参数
		是文本的垂直位置,第六个参数是文本的颜色
		*/
		text.Draw(screen, l, titleArcadeFont, x, (i+4)*g.Cfg.TitleFontSize, color.White)
	}
	for i, l := range texts {
		x := (g.Cfg.ScreenWidth - len(l)*g.Cfg.FontSize) / 2
		text.Draw(screen, l, arcadeFont, x, (i+4)*g.Cfg.FontSize, color.White)
	}
}

7至此游戏就结束啦

然后就可以写一篇总结看自己学到的知识。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
### 回答1: 这是一个简单的Go语言弹球小游戏: package main import ( "fmt" "time" ) func main() { var x, y int fmt.Println("欢迎来到弹球小游戏!") for { fmt.Println("请输入x坐标:") fmt.Scan(&x) fmt.Println("请输入y坐标:") fmt.Scan(&y) if x == 0 || y == 0 { fmt.Println("游戏结束!") break } // 延时1秒 time.Sleep(time.Second) // 判断x和y坐标是否在边界内 if x < 0 || x > 9 || y < 0 || y > 9 { fmt.Println("坐标超出边界,请重新输入!") continue } // 判断是否碰到边界 if x == 0 || x == 9 || y == 0 || y == 9 { fmt.Println("你碰到了边界,游戏结束!") break } // 判断是否碰到障碍物 if (x == 1 && y == 1) || (x == 8 && y == 1) || (x == 1 && y == 8) || (x == 8 && y == 8) { fmt.Println("你碰到了障碍物,游戏结束!") break } fmt.Println("你的坐标是:", x, y) } } ### 回答2: 弹球小游戏是一种经典的游戏,在开发中可以使用Go语言来实现。Go语言是一门简洁、高效的编程语言,具有并发性能优秀的特点,非常适合用于开发小型游戏。 在Go语言中,可以使用一些库来实现弹球小游戏的功能,例如使用`github.com/nsf/termbox-go`库来创建一个终端界面,并处理用户输入。此外,还可以使用Go语言的内置功能来处理游戏的物理引擎和游戏逻辑。 游戏的主要功能包括: - 创建一个终端界面,并初始化游戏场景。 - 在界面中绘制一个球和一个挡板,分别代表弹球和玩家控制的挡板。 - 实现球在界面中的移动和碰撞反弹效果。 - 实现挡板的移动,通过控制挡板来夹击球。 - 添加得分系统和游戏结束判断。 游戏的具体逻辑如下: 1. 初始化终端界面,并绘制游戏场景,球和挡板的位置。 2. 不断监听玩家输入,根据输入控制挡板的左右移动。 3. 更新球的位置,判断球是否与边界或挡板发生碰撞。如果碰撞,根据碰撞位置和角度来计算反弹的方向和角度。 4. 当球与底部边界碰撞时,游戏结束,显示得分。 5. 根据游戏规则,计算得分并显示。 6. 重复步骤2到5,直到玩家主动退出游戏。 通过使用Go语言的相关库和语言特性,可以很容易地实现一个简单而有趣的弹球小游戏。 ### 回答3: 弹球小游戏是一种经典的游戏,通过控制一个滑板反弹球体,以尽可能长时间保持球体不掉落而获得得分。下面是用Go语言一个简单的弹球小游戏步骤。 首先,我们需要导入必要的Go包。例如,使用`fmt`包来处理输入和输出,使用`github.com/nsf/termbox-go`包来处理终端界面。 然后,我们需要定义游戏所需的结构体和变量。这可能包括球体的位置和速度,滑板的位置,分数和游戏状态等。 接下来,我们需要初始化游戏界面,包括设置终端的大小和绘制游戏元素,如球体和滑板。 然后,我们需要编游戏的主循环。在每一次循环中,我们需要处理用户的输入(例如通过键盘控制滑板的移动),更新游戏元素的状态(例如更新球体的位置)并重新绘制游戏界面。 在游戏的主循环中,我们还需要处理与边界的碰撞检测,如果球体触碰到边界,则需要改变球体的速度方向。 我们还需要处理球体与滑板的碰撞检测。如果球体触碰到滑板,则需要改变球体的速度方向。 在游戏的主循环中,我们还需要处理游戏的结束条件,例如当球体掉落到底部时,游戏结束,显示最终的得分。 最后,我们需要运行游戏的主循环并保持运行,直到游戏结束。在游戏结束后,我们可以处理用户的输入,询问是否重新开始游戏。 通过以上步骤,我们就可以用Go语言一个简单的弹球小游戏了。当然,这只是一个简单的思路,实际的实现过程中还需要一些其他的具体细节和处理逻辑。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值