此文写在golang游戏开辟学习条记-创建一个能自由探索的3D天下之后,感爱好可以先去那篇文章相识一些底子知识,在这篇文章里我们要创建一个简单的2D游戏场景以及配套的人物,并实现人物活动和碰撞检测功能,效果如下
文章目次- 一.参考资料
- 二.底子概念
- 三.依靠
- 四.资源准备
- 1.人物静止图
- 2.人物活动图(只展示第一帧)
- 2.方块纹理图
- 五.开始实现!
- 1.资源管理
- shader.go
- 两个着色器步伐
- texture2D.go
- resource.go
- 2.游戏对象
- 3.精灵
- 4.摄像头
一.参考资料
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);
}
着色器步伐中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 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 方法用于直接将视角转换到指定的位置
底子篇竣事,下一篇将会创建一个能自由疾驰的胖子和一张简单的舆图 ! |