golang游戏开发学习笔记-开发一个简单的2D游戏(完成篇)

先自我介绍一下,小编浙江大学毕业,去过华为、字节跳动等大厂,目前阿里P7

深知大多数程序员,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年最新Golang全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友。
img
img
img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上Go语言开发知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

如果你需要这些资料,可以添加V获取:vip1024b (备注go)
img

正文

const(

BlockHeight float32 = 15

BlockWidth float32 = 20

)

//方块类

type Block struct{

GameObj

}

目前为止,方块类和普通游戏类没有任何区别,但后期可能会添加诸如方块硬度之类的属性

接下来可以开始开发地图类了

package model

import(

“game2D/resource”

“game2D/sprite”

“math”

“fmt”

“github.com/go-gl/mathgl/mgl32”

)

type GameMap struct{

Height float32

Width float32

blocks [][] *Block

heightBlockNum int

widthBlockNum int

}

//一个简单的测试用的游戏地图生成函数

func NewGameMap(width,height float32, mapFile string) *GameMap{

heightBlockNum := int(math.Ceil(float64(height / BlockHeight)))

widthBlockNum := int(math.Ceil(float64(width / BlockWidth)))

grounds := heightBlockNum / 4

xGrounds := widthBlockNum / 4

fmt.Println(“map block size:”,heightBlockNum,widthBlockNum)

blocks := make([][]*Block,heightBlockNum)

for i := 0;i < heightBlockNum;i++{

rowBlocks := make([]*Block,widthBlockNum)

if(i < grounds || i > grounds*3){

for j := 0; j < widthBlockNum;j++{

if(j < xGrounds || j > xGrounds * 3 || i == 0){

gameObj := NewGameObj(resource.GetTexture(“soil”),float32(j) * BlockWidth,float32(i)*BlockHeight,&mgl32.Vec2{BlockWidth,BlockHeight},0,&mgl32.Vec3{1,1,1})

rowBlocks[j] = &Block{GameObj:*gameObj}

}

}

}

blocks[i] = rowBlocks

}

return &GameMap{Height:height,

Width:width,

blocks:blocks,

heightBlockNum:heightBlockNum,

widthBlockNum:widthBlockNum}

}

//将一个物体坐标转换为地图格子坐标范围

func (gameMap *GameMap) FetchBox(position,size mgl32.Vec2)(int,int,int,int){

startY := int(math.Floor(float64((position[0]) / gameMap.Width * float32(gameMap.widthBlockNum))))-1;

if(startY <= 0){

startY = 0

}

endY := int(math.Ceil(float64((position[0] + size[0]) / gameMap.Width * float32(gameMap.widthBlockNum)))) + 1

if(endY >= gameMap.widthBlockNum){

endY = gameMap.widthBlockNum - 1

}

startX := int(math.Floor(float64((position[1]) / gameMap.Height * float32(gameMap.heightBlockNum)))) -1

if(startX < 0){

startX = 0

}

endX := int(math.Ceil(float64((position[1] + size[1]) / gameMap.Height * float32(gameMap.heightBlockNum)))) +1

if(endX >= gameMap.heightBlockNum){

endX = gameMap.heightBlockNum - 1

}

return startX,endX,startY,endY

}

//渲染地图

func (gameMap *GameMap) Draw(position mgl32.Vec2, zoom mgl32.Vec2, renderer *sprite.SpriteRenderer){

startX,endX,startY,endY := gameMap.FetchBox(position,zoom)

for i:=startX;i<=endX;i++{

for j := startY;j<endY;j++{

block := gameMap.blocks[int(i)][int(j)]

if(block != nil){

block.Draw(renderer)

}

}

}

}

在上述的地图类中,我们用一个二维切片储存地图中的所有方块。用地图长宽除以方块长宽获得横向和竖向方块数量然后往二维数组中放入方块。要注意的是,虽然可以在渲染函数中简单的遍历并调用block来绘制地图,但试想一下如果地图长一千个方块宽一千个方块会怎么样?每一帧都要绘制一百万个方块!这显然是不合理的,所以在渲染函数中我们先将位置转化为方块位置,然后根据屏幕大小计算出我们能看到的方块位置和数量并对其进行渲染,这样地图无论多大我们都只用渲染屏幕内的方块,大大优化了效率

二.碰撞的艺术

单纯的碰撞检测其实并不复杂,简单的AABB模型(不发生旋转的矩形之间)只需要初中数学水平就能理解,判断两个矩形的x轴和y轴是否重叠即可,难点在于如何检测碰撞方向。

简单AABB碰撞模型

如图所示,根据xy轴是否重叠能很简单的判断出某个状态下两个物体是否已经发生了碰撞,如果我们是要做一个类似flappy bird或者跑酷类游戏,那到这一步就可以了。但如果我们要做的是一个类似泰拉瑞亚的游戏,这样显然不符合要求。可以想一下,如果一个游戏人物向屏幕左下方移动,在某个时刻撞到了一堵垂直的墙,这时候虽然人物无法向左继续移动,但应该能向上或向下移动,如果撞到的是一堵水平的墙,那就应该反过来。我相信一定会有一个巧妙的数学方法能解决这个问题,但作者数学水平实在太次,只能想个笨办法了。

首先要明确,在游戏中,每个时刻每个物体的运动都是可预测的,在每一帧绘制之前,我们会根据物体的运动速度和方向计算好该物体在这一帧的位移,然后才会交由显卡绘制。我的笨办法是,在每个位移发生之前先获取位移方向并将物体沿着这个方向迭代,每次迭代里移动一小段的距离,如果在某次迭代中发生碰撞,那上一次的迭代位置就应该是物体最终所处的方向,明白了这一点,我们可以开始写代码了

在这里插入图片描述

package physic

import(

“github.com/go-gl/mathgl/mgl32”

“math”

)

//检测两个矩形是否发生碰撞

func IsCollidingAABB(thisGameObj,anotherObj React) bool{

tPosition := thisGameObj.GetPosition()

tSize := thisGameObj.GetSize()

aPosition := anotherObj.GetPosition()

aSize := anotherObj.GetSize()

return isCollidingReact(tPosition,tSize,aPosition,aSize);

}

type React interface{

GetPosition() mgl32.Vec2

GetSize() mgl32.Vec2

}

func isCollidingReact(position1,size1,position2,size2 mgl32.Vec2) bool{

// x轴方向碰撞?

collisionX := position1[0] + size1[0] >= position2[0] && position2[0] + size2[0] >= position1[0]

// y轴方向碰撞?

collisionY := position1[1] + size1[1] >= position2[1] && position2[1] + size2[1] >= position1[1]

return collisionX && collisionY

}

//检测两个矩形运动后是否会发生碰撞

func WillCollidingAABB(thisGameObj,anotherObj React,dt mgl32.Vec2) bool{

tPosition := thisGameObj.GetPosition().Sub(dt)

tSize := thisGameObj.GetSize()

aPosition := anotherObj.GetPosition()

aSize := anotherObj.GetSize()

return isCollidingReact(tPosition,tSize,aPosition,aSize);

}

//检测两个矩形的碰撞,并获取碰撞位置

func ColldingAABBPlace(thisGameObj,anotherObj React,shift mgl32.Vec2) (bool,mgl32.Vec2){

position := thisGameObj.GetPosition()

if(shift[0] == 0 && shift[1] == 0){

return false, position

}

colldingShift := mgl32.Vec2{0.0}

colldingDt := shift.Normalize()

for math.Abs(float64(colldingShift[0])) <= math.Abs(float64(shift[0])) && math.Abs(float64(colldingShift[1])) <= math.Abs(float64(shift[1])){

tempColldingShift := colldingShift.Sub(colldingDt)

if(WillCollidingAABB(thisGameObj,anotherObj,tempColldingShift)){

return true,thisGameObj.GetPosition().Sub(colldingShift)

}

colldingShift = tempColldingShift

}

return false,thisGameObj.GetPosition()

}

现在我们有了能检测两个物体是否发生碰撞和碰撞位置的检测方法,然后该怎么用到我们的应用中?物体运动之前把地图中的每个方块都检测一次显然不现实,那只检测屏幕内的的方块合理吗?答案是不合理,因为不考虑bug(比如速度太快穿过去了)的情况下,运动物体只可能和他周围的方块发生碰撞,所以我们应该只对物体周围的方块进行碰撞检测

我们将碰撞逻辑加入到地图类中,添加方法

//检测一个物体是否与地图中的方块发生碰撞

func (gameMap *GameMap) IsColl(gameObj GameObj,shift mgl32.Vec2)(bool,mgl32.Vec2){

position := gameObj.GetPosition();

size := gameObj.GetSize()

startX,endX,startY,endY := gameMap.FetchBox(mgl32.Vec2{position[0],position[1]},mgl32.Vec2{size[0],size[1]})

for i:=startX;i<=endX;i++{

for j := startY;j<endY;j++{

block := gameMap.blocks[int(i)][int(j)]

if(block != nil){

isCol,position := physic.ColldingAABBPlace(gameObj,block,shift)

if(isCol){

return isCol,position

}

}

}

}

return false,gameObj.GetPosition()

}

三.主角的诞生

现在要来创造我们的主角了,首先创建一个类代表游戏中所有可移动的物体

package model

import(

“game2D/resource”

“game2D/constant”

“github.com/go-gl/mathgl/mgl32”

)

//可移动的游戏对象

type MoveObj struct{

GameObj

//在上下左右方向是否可移动

stockUp,stockDown,stockLeft,stockRight bool

//水平移动速度

movementSpeed float32

//飞行速度

fallSpeed float32

//下坠速度

flySpeed float32

//移动时的动画纹理

moveTextures []*resource.Texture2D

//静止时的纹理

stantTexture *resource.Texture2D

//游戏地图

gameMap *GameMap

//当前运动帧

moveIndex int

//运动帧之间的切换阈值

moveDelta float32

}

func NewMoveObject(gameObj GameObj,movementSpeed,flySpeed float32, moveTextures []*resource.Texture2D,gameMap *GameMap) *MoveObj{

moveObj := &MoveObj{GameObj:gameObj,

movementSpeed:movementSpeed,

fallSpeed:100,

gameMap:gameMap,

moveTextures:moveTextures,

flySpeed:flySpeed,

moveIndex:0,

moveDelta:0,

stantTexture:gameObj.texture}

return moveObj

}

//恢复静止

func (moveObj *MoveObj) Stand(){

moveObj.texture = moveObj.stantTexture

}

//由用户主动发起的运动

func(moveObj *MoveObj) Move(direction constant.Direction, delta float32){

shift := mgl32.Vec2{0,0}

if(direction ==constant. DOWN){

if(!moveObj.stockDown && moveObj.y + moveObj.size[1] < moveObj.gameMap.Height){

shift[1] += moveObj.flySpeed * delta

}

}

if(direction == constant.UP){

if(!moveObj.stockUp && moveObj.y > 0){

shift[1] -= moveObj.flySpeed * delta

}

}

if(direction == constant.LEFT){

moveObj.ReverseX()

if(moveObj.moveIndex >= len(moveObj.moveTextures)){

moveObj.moveIndex = 0

}

moveObj.moveDelta += delta

if(moveObj.moveDelta > 0.1){

moveObj.moveDelta = 0

moveObj.texture = moveObj.moveTextures[moveObj.moveIndex]

moveObj.moveIndex += 1

}

if(!moveObj.stockLeft && moveObj.x > 0){

shift[0] -= moveObj.movementSpeed * delta

}

}

if(direction == constant.RIGHT){

moveObj.ForWardX()

if(moveObj.moveIndex >= len(moveObj.moveTextures)){

moveObj.moveIndex = 0

}

moveObj.moveDelta += delta

if(moveObj.moveDelta > 0.1){

moveObj.moveDelta = 0

moveObj.texture = moveObj.moveTextures[moveObj.moveIndex]

moveObj.moveIndex += 1

}

if(!moveObj.stockRight && moveObj.x + moveObj.size[0] < moveObj.gameMap.Width){

shift[0] += moveObj.movementSpeed * delta

}

}

isCol,position := moveObj.gameMap.IsColl(moveObj.GameObj,shift)

if(isCol){

moveObj.SetPosition(position)

}else{

moveObj.x += shift[0]

moveObj.y += shift[1]

}

}

这个类只有一个特殊的move方法,传入一个代表方向的常量和帧与帧之间的延迟,当往左或往右运动时会将自身纹理切换为运动的纹理并在度过指定时间之后切换运动帧来形成动画效果,当静止时调用Stand方法会将纹理帧切换为静止时的图像。注意,在往左或又运动时我们只需要将纹理动画直接求镜像而不用创建新的纹理。当然这种动画的实现方式是不太优雅的,更好的方法是调用gl.SubTextImage方法将纹理图像直接替换为指定的图像而不是切换纹理,或者将多张图片拼成为一张,在运动时调整纹理坐标来显示不同图像。不过理解了这一种,其他两种也不会有问题。

4.还不够抽象

目前为止,我们已经有了地图,游戏角色,摄像头和精灵渲染器,但在man方法里直接创建和修改这些对象似乎不太优雅,我们创建一个Game类来进一步封装

package game

import(

“game2D/resource”

“game2D/sprite”

“game2D/camera”

“game2D/model”

“game2D/constant”

“github.com/go-gl/mathgl/mgl32”

“github.com/go-gl/gl/v4.1-core/gl”

“github.com/go-gl/glfw/v3.2/glfw”

)

type GameState int

const(

GAME_ACTIVE GameState = 0

GAME_MENU GameState = 1

)

type Game struct{

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以添加V获取:vip1024b (备注Go)
img

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
接创建和修改这些对象似乎不太优雅,我们创建一个Game类来进一步封装

package game

import(

“game2D/resource”

“game2D/sprite”

“game2D/camera”

“game2D/model”

“game2D/constant”

“github.com/go-gl/mathgl/mgl32”

“github.com/go-gl/gl/v4.1-core/gl”

“github.com/go-gl/glfw/v3.2/glfw”

)

type GameState int

const(

GAME_ACTIVE GameState = 0

GAME_MENU GameState = 1

)

type Game struct{

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以添加V获取:vip1024b (备注Go)
[外链图片转存中…(img-KOLQ4yYE-1713131718919)]

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

  • 18
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值