golang游戏开发学习笔记-开发一个简单的2D游戏(基础篇)
此文寫在golang游戲開發學習筆記-創建一個能自由探索的3D世界之后,感興趣可以先去那篇文章了解一些基礎知識,在這篇文章里我們要創建一個簡單的2D游戲場景以及配套的人物,并實現人物運動和碰撞檢測功能,效果如下
文章目錄
- 一.參考資料
- 二.基礎概念
- 三.依賴
- 四.資源準備
- 1.人物靜止圖
- 2.人物運動圖(只展示第一幀)
- 2.方塊紋理圖
- 五.開始實現!
- 1.資源管理
- shader.go
- 兩個著色器程序
- 頂點
- 頂點著色器
- 片段著色器
- texture2D.go
- resource.go
- 2.游戲對象
- gameObj.go
- 3.精靈
- SpriteRenderer .go
- 4.攝像頭
- camera.go
一.參考資料
learnOpenGl 的中文翻譯,使用C++實現的。
go-gl example go-gl的示例代碼
二.基礎概念
這里涉及到的概念在之前的文章里基本上都有過介紹,不再贅述。不過大家有興趣可以去看一看碰撞檢測的一些算法實現
三.依賴
沒有新增任何依賴
四.資源準備
我們創建的游戲世界里有兩個地方需要用到紋理資源(貼圖),一是組成世界的方塊、二是游戲主角。由于方塊是靜態的,不需要動畫效果,所以只需要一張貼圖就可以了。而游戲主角則需要多張紋理圖像來組成運動時的動畫。要注意的是,為了只渲染一張圖片中我們需要的部分,紋理圖片必須是帶有alpha通道的圖片格式,并將圖片中不需要渲染的位置的透明度設置為0。為了簡便,這里統一使用png格式。
作者是直接百度搜索像素圖像資源找了一張gif然后用screentogif分解成靜態圖片,最后用開源圖形編輯軟件Krita直接扣出來的(肯定是不能用于商業用途)由于沒有人物靜態圖,我自己畫了一個,資源列表如下
1.人物靜止圖
2.人物運動圖(只展示第一幀)
2.方塊紋理圖
將資源準備完成之后,就能開始代碼的開發了
五.開始實現!
1.資源管理
在上一篇文章中我們將紋理和著色器分別封裝成了兩個類,這里我們創建一個資源管理類對這兩個類進行管理,由于golang中是沒有靜態變量的,需要用包內變量對其進行模擬
shader.go
package resourceimport("github.com/go-gl/gl/v4.1-core/gl""github.com/go-gl/mathgl/mgl32""strings""fmt" )type Shader struct{ID uint32 }func Compile(vertexString, fragmentString string) *Shader{vertexShader,err := compile(vertexString+"\x00", gl.VERTEX_SHADER)if err != nil{panic(err)}fragmentShader,err := compile(fragmentString+"\x00", gl.FRAGMENT_SHADER)if err != nil{panic(err)}progID := gl.CreateProgram()gl.AttachShader(progID, vertexShader)gl.AttachShader(progID, fragmentShader) gl.LinkProgram(progID)gl.DeleteShader(vertexShader)gl.DeleteShader(fragmentShader)return &Shader{ ID: progID} } func (shader *Shader) Use(){gl.UseProgram(shader.ID) }func (shader *Shader) SetBool(name string, value bool){var a int32 = 0;if(value){a = 1}gl.Uniform1i(gl.GetUniformLocation(shader.ID, gl.Str(name + "\x00")), a) }func (shader *Shader) SetInt(name string, value int32){gl.Uniform1i(gl.GetUniformLocation(shader.ID, gl.Str(name + "\x00")), value) }func (shader *Shader) SetFloat(name string, value float32){gl.Uniform1f(gl.GetUniformLocation(shader.ID, gl.Str(name + "\x00")), value) }func (shader *Shader) SetMatrix4fv(name string, value *float32){gl.UniformMatrix4fv(gl.GetUniformLocation(shader.ID, gl.Str(name + "\x00")), 1,false,value) }func (shader *Shader) SetVector3f(name string, vec3 mgl32.Vec3){gl.Uniform3f(gl.GetUniformLocation(shader.ID, gl.Str(name + "\x00")), vec3[0], vec3[1], vec3[2]); } func compile(sourceString string, shaderType uint32)(uint32, error){shader := gl.CreateShader(shaderType)source, free := gl.Strs(sourceString)gl.ShaderSource(shader, 1, source, nil)free()gl.CompileShader(shader)var status int32gl.GetShaderiv(shader, gl.COMPILE_STATUS, &status)if status == gl.FALSE {var logLength int32gl.GetShaderiv(shader, gl.INFO_LOG_LENGTH, &logLength)log := strings.Repeat("\x00", int(logLength+1))gl.GetShaderInfoLog(shader, logLength, nil, gl.Str(log))return 0, fmt.Errorf("failed to compile %v: %v", source, log)}return shader, nil }shader類從給定的兩個字符串中編譯出頂點著色器和片段著色器,同時提供幾個用于設置uniform變量的函數,唯一要說明的是字符串尾部要添加0X00來標識結尾(發現只有golang需要這樣)
兩個著色器程序
著色器程序的用途大家可以在參考資料或者之前的文章中找到詳細說明,這里只從代碼上大概講解一下。需要說明的是,著色器程序需要結合具體要繪制的頂點來看,在這個2D游戲中,所有的元素都是由兩個三角形組成的矩形構成的,因此在不使用EBO的情況下需要六個頂點
頂點
vertices := []float32{0.0, 1.0, 0.0, 1.0,1.0, 0.0, 1.0, 0.0,0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 1.0,1.0, 1.0, 1.0, 1.0,1.0, 0.0, 1.0, 0.0,}由于我們開發的是2D游戲,所有頂點的z分量都為0,所以用頂點中每一行的前兩位作為頂點坐標,后兩位作為紋理坐標,并將頂點坐標和紋理坐標合為一個變量傳入著色器中
頂點著色器
#version 410 core layout (location = 0) in vec4 vertex; out vec2 TexCoords; uniform mat4 model; uniform mat4 projection; uniform mat4 view; uniform int reverseX;void main() { if(reverseX == 1){TexCoords = vertex.zw;}else{TexCoords = vec2(1 - vertex.z, vertex.w);}gl_Position = projection * view * model * vec4(vertex.x, vertex.y, 0.0, 1.0); }著色器程序中model、projection,view這三個變量用于將物體從局部空間變換到世界空間并轉換為用戶視角,可以理解為負責對物體進行平移,變形,對畫面進行裁剪的工具。在前面的文章中已經講過,不再贅述。vertex變量就是前面頂點數據中的每一行,其中前兩位代表頂點坐標,后兩位代表紋理坐標,reverseX變量為unifom類型,同樣從外部傳入,負責控制圖像是否沿Y軸方向進行鏡像,具體作用后面會講到
片段著色器
#version 410 core in vec2 TexCoords; out vec4 FragColor;uniform sampler2D image; uniform vec3 spriteColor;void main() {vec4 texColor = texture(image, TexCoords);if(texColor.a < 0.1)discard;FragColor = vec4(spriteColor, 1.0) * texColor; }片段著色器與上一篇文章的基本相同,唯一區別是加入了一個判斷,在圖像區域的透明度小于0.1的時候,會放棄對這片區域的渲染。
texture2D.go
package resourceimport("os""image""image/png""image/draw""errors""github.com/go-gl/gl/v4.1-core/gl" )type Texture2D struct{ID uint32TEXTUREINDEX uint32 } func NewTexture2D(file string, TEXTUREINDEX uint32) *Texture2D{imgFile, err := os.Open(file)if err != nil {panic(err)}img, err := png.Decode(imgFile)if err != nil {panic(err)}rgba := image.NewRGBA(img.Bounds())if rgba.Stride != rgba.Rect.Size().X*4 {panic(errors.New("unsupported stride"))}draw.Draw(rgba, rgba.Bounds(), img, image.Point{0, 0}, draw.Src)var textureID uint32gl.GenTextures(1, &textureID)gl.BindTexture(gl.TEXTURE_2D, textureID)gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR)gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR)gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT)gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT)gl.TexImage2D(gl.TEXTURE_2D,0,gl.RGBA,int32(rgba.Rect.Size().X),int32(rgba.Rect.Size().Y),0,gl.RGBA,gl.UNSIGNED_BYTE,gl.Ptr(rgba.Pix))gl.BindTexture(gl.TEXTURE_2D, 0);return &Texture2D{ ID: textureID,TEXTUREINDEX:TEXTUREINDEX} }func (texture *Texture2D) Use(){gl.ActiveTexture(texture.TEXTUREINDEX)gl.BindTexture(gl.TEXTURE_2D, texture.ID) }texture2D類用于解析png格式的圖片并創建紋理綁定到上下文中
resource.go
package resourceimport ("io/ioutil" )var (textures = make(map[string]*Texture2D)shaders = make(map[string]*Shader) )func LoadShader(vShaderFile, fShaderFile, name string){vertexString, err := ioutil.ReadFile(vShaderFile)if err != nil{panic(err)}fragmentString, err := ioutil.ReadFile(fShaderFile)if err != nil{panic(err)}shaders[name] = Compile(string(vertexString), string(fragmentString)) } func GetShader(name string) *Shader{return shaders[name] }func LoadTexture(TEXTUREINDEX uint32, file, name string){texture := NewTexture2D(file, TEXTUREINDEX)textures[name] = texture } func GetTexture(name string) *Texture2D{return textures[name] }負責加載紋理和著色器的管理類
2.游戲對象
首先想一想游戲內所有元素都有的屬性有哪些,并對其進行封裝,創建一個游戲對象基類
gameObj.go
package model import("game2D/resource""game2D/sprite""github.com/go-gl/mathgl/mgl32" ) type GameObj struct{texture *resource.Texture2Dx float32y float32size *mgl32.Vec2rotate float32color *mgl32.Vec3isXReverse int32 } func(gameObj GameObj) GetPosition()mgl32.Vec2{return mgl32.Vec2{gameObj.x, gameObj.y} } func(gameObj *GameObj) SetPosition(position mgl32.Vec2){gameObj.x = position[0]gameObj.y = position[1] } func(gameObj GameObj) GetSize()mgl32.Vec2{return mgl32.Vec2{gameObj.size[0], gameObj.size[1]} } func(gameObj *GameObj) Draw(renderer *sprite.SpriteRenderer){renderer.DrawSprite(gameObj.texture, &mgl32.Vec2{gameObj.x,gameObj.y}, gameObj.size, gameObj.rotate, gameObj.color,gameObj.isXReverse) } func(gameObj *GameObj) ReverseX(){gameObj.isXReverse = -1 } func(gameObj *GameObj) ForWardX(){gameObj.isXReverse = 1 } func NewGameObj(texture *resource.Texture2D, x, y float32, size *mgl32.Vec2, rotate float32, color *mgl32.Vec3) *GameObj{return &GameObj{texture:texture,x:x,y:y,size:size,rotate:rotate,color:color,isXReverse:1} }一個游戲內的對象一定會有那些屬性?位置,大小,外觀(紋理),顏色,和旋轉的角度。同時為了效率,加入一個標識是否鏡像的變量。有了這些屬性,我們就能直接將其繪制到屏幕上。
3.精靈
我們已經有了可供繪制游戲對象,接下來自然就是將他繪制到屏幕上了,創建一個精靈繪制類
SpriteRenderer .go
package sprite import("github.com/go-gl/mathgl/mgl32""game2D/resource""github.com/go-gl/gl/v4.1-core/gl" )type SpriteRenderer struct{shader *resource.Shadervao uint32 } func NewSpriteRenderer(shader *resource.Shader) *SpriteRenderer{spriteRenderer := SpriteRenderer{shader:shader}spriteRenderer.initRenderData()return &spriteRenderer } func(spriteRenderer *SpriteRenderer) DrawSprite(texture *resource.Texture2D, position *mgl32.Vec2, size *mgl32.Vec2, rotate float32, color *mgl32.Vec3,isReverseX int32){model := mgl32.Translate3D(position[0], position[1], 0).Mul4(mgl32.Translate3D(0.5*size[0], 0.5*size[1], 0))model = model.Mul4(mgl32.HomogRotate3D(rotate, mgl32.Vec3{0, 0, 1}))model = model.Mul4(mgl32.Translate3D(-0.5*size[0], -0.5*size[1], 0))model = model.Mul4(mgl32.Scale3D(size[0], size[1], 1))spriteRenderer.shader.SetMatrix4fv("model", &model[0])spriteRenderer.shader.SetInt("reverseX", isReverseX)spriteRenderer.shader.SetVector3f("spriteColor", *color)texture.Use()gl.BindVertexArray(spriteRenderer.vao);gl.DrawArrays(gl.TRIANGLES, 0, 6);gl.BindVertexArray(0); } func(spriteRenderer *SpriteRenderer) initRenderData(){var vbo uint32vertices := []float32{0.0, 1.0, 0.0, 1.0,1.0, 0.0, 1.0, 0.0,0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 1.0,1.0, 1.0, 1.0, 1.0,1.0, 0.0, 1.0, 0.0,}gl.GenVertexArrays(1, &spriteRenderer.vao);gl.GenBuffers(1, &vbo);gl.BindBuffer(gl.ARRAY_BUFFER, vbo);gl.BufferData(gl.ARRAY_BUFFER, 4 * len(vertices), gl.Ptr(vertices), gl.STATIC_DRAW);gl.BindVertexArray(spriteRenderer.vao);gl.EnableVertexAttribArray(0);gl.VertexAttribPointer(0, 4, gl.FLOAT, false, 4 * 4, gl.PtrOffset(0));gl.BindBuffer(gl.ARRAY_BUFFER, 0);gl.BindVertexArray(0); }這個類中作用initRenderData方法基本上和上一篇文章里的makeVao函數一樣,創建頂點緩沖對象和頂點數組對象并將其綁定到上下文,而在DrawSprite中我們根據物體大小和位置對其進行縮放平移,并綁定物體紋理,將其繪制到屏幕上
4.攝像頭
在上一篇文章中我們將用戶視角變換的邏輯抽象成了一個攝像頭類,這里我們同樣需要
camera.go
package camera import("github.com/go-gl/mathgl/mgl32" ) type Camera2D struct{position,front,up mgl32.Vec3movementSpeed float32wordWidth,wordHeight,screenWidth,screenHeight float32 } func NewDefaultCamera(wordHeight ,wordWidth, screenWidth, screenHeight float32, position2D mgl32.Vec2) *Camera2D{position := mgl32.Vec3{position2D[0], position2D[1], 0}front := mgl32.Vec3{0, 0, -1}up := mgl32.Vec3{0, 1, 0}movementSpeed := float32(100)return &Camera2D{position:position, front:front, up:up, movementSpeed:movementSpeed,wordHeight:wordHeight,wordWidth:wordWidth,screenHeight:screenHeight,screenWidth:screenWidth} } //獲取攝像頭位置 func (camera *Camera2D) GetPosition() mgl32.Vec2{return mgl32.Vec2{camera.position[0], camera.position[1]} } //獲取view func (camera *Camera2D) GetViewMatrix() *float32{target := camera.position.Add(camera.front)view := mgl32.LookAtV(camera.position,target, camera.up)return &view[0] } //重置世界邊界 func (camera *Camera2D) resetWordSize(width,height float32){camera.wordWidth = widthcamera.wordHeight = height } //重設屏幕大小 func (camera *Camera2D) resetScreenSize(width,height float32){camera.screenWidth = widthcamera.screenHeight = height } //根據坐標轉換視野 func(camera *Camera2D) InPosition(x,y float32){if(x <= 0){camera.position[0] = 0}else if(x + camera.screenWidth > camera.wordWidth){x = camera.wordWidth - camera.screenWidth}else{camera.position[0] = x}if(y <= 0){camera.position[1] = 0}else if(y + camera.screenHeight > camera.wordHeight){camera.position[1] = camera.wordHeight - camera.screenHeight}else{camera.position[1] = y} }可以看到由于視角鎖死在2D空間里,攝像頭代碼相對于上一篇文章有了很大的簡化,而且加入了wordWidth,wordHeight,screenWidth,screenHeight四個變量用于標識攝像頭能看到的最大位置,同時新增InPosition方法用于直接將視角轉換到指定的位置
基礎篇結束,下一篇將會創建一個能自由奔跑的胖子和一張簡單的地圖
總結
以上是生活随笔為你收集整理的golang游戏开发学习笔记-开发一个简单的2D游戏(基础篇)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 关于Hex文件的解析和修改应用
- 下一篇: 使用集合转数组的方法,必须使用集合的 t