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

一.参考资料

C++

二.基础概念

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

三.依赖

没有新增任何依赖

四.资源准备

alphapng像素图像资源gifscreentogifKrita

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
}
shaderuniform0X00golang
两个着色器程序
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);
}
modelprojectionviewvertexreverseXunifom
片段着色器
#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
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);
}
initRenderDatamakeVaoDrawSprite

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
	}
}
wordWidth,wordHeight,screenWidth,screenHeightInPosition

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