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

此文写在golang游戏开发学习笔记-创建一个能自由探索的3D世界之后,感兴趣可以先去那篇文章了解一些基础知识,在这篇文章里我们要创建一个简单的2D游戏场景以及配套的人物,并实现人物运动和碰撞检测功能,效果如下
在这里插入图片描述

一.参考资料

learnOpenGl 的中文翻译,使用C++实现的。
go-gl example go-gl的示例代码

二.基础概念

这里涉及到的概念在之前的文章里基本上都有过介绍,不再赘述。不过大家有兴趣可以去看一看碰撞检测的一些算法实现

三.依赖

没有新增任何依赖

四.资源准备

我们创建的游戏世界里有两个地方需要用到纹理资源(贴图),一是组成世界的方块、二是游戏主角。由于方块是静态的,不需要动画效果,所以只需要一张贴图就可以了。而游戏主角则需要多张纹理图像来组成运动时的动画。要注意的是,为了只渲染一张图片中我们需要的部分,纹理图片必须是带有alpha通道的图片格式,并将图片中不需要渲染的位置的透明度设置为0。为了简便,这里统一使用png格式。
作者是直接百度搜索像素图像资源找了一张gif然后用screentogif分解成静态图片,最后用开源图形编辑软件Krita直接扣出来的(肯定是不能用于商业用途)由于没有人物静态图,我自己画了一个,资源列表如下

1.人物静止图

在这里插入图片描述

2.人物运动图(只展示第一帧)

在这里插入图片描述

2.方块纹理图

在这里插入图片描述
将资源准备完成之后,就能开始代码的开发了

五.开始实现!

1.资源管理

在上一篇文章中我们将纹理和着色器分别封装成了两个类,这里我们创建一个资源管理类对这两个类进行管理,由于golang中是没有静态变量的,需要用包内变量对其进行模拟

shader.go
package resource


import(
	"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 int32
	gl.GetShaderiv(shader, gl.COMPILE_STATUS, &status)
	if status == gl.FALSE {
        var logLength int32
        gl.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);
}

着色器程序中modelprojectionview这三个变量用于将物体从局部空间变换到世界空间并转换为用户视角,可以理解为负责对物体进行平移,变形,对画面进行裁剪的工具。在前面的文章中已经讲过,不再赘述。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 resource

import(
	"os"
	"image"
	"image/png"
	"image/draw"
	"errors"
	"github.com/go-gl/gl/v4.1-core/gl"
)

type Texture2D struct{
	ID uint32
	TEXTUREINDEX 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 uint32
	gl.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 resource

import (
	"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.Texture2D
	x float32
	y float32
	size *mgl32.Vec2
	rotate float32
	color *mgl32.Vec3
	isXReverse 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.Shader
	vao 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 uint32
	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,
	}
	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.Vec3
	movementSpeed         						   float32
	wordWidth,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 = width
	camera.wordHeight = height
}
//重设屏幕大小
func (camera *Camera2D) resetScreenSize(width,height float32){
	camera.screenWidth = width
	camera.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方法用于直接将视角转换到指定的位置

基础篇结束,下一篇将会创建一个能自由奔跑的胖子和一张简单的地图

展开阅读全文

没有更多推荐了,返回首页