OpenGL 與 Go 教程(三)實現遊戲
該教程的完整源代碼可以從 GitHub 上找到。
歡迎回到《OpenGL 與 Go 教程》!如果你還沒有看過 第一節 和 第二節,那就要回過頭去看一看。
到目前為止,你應該懂得如何創建網格系統以及創建代表方格中每一個單元的格子陣列。現在可以開始把網格當作遊戲面板實現 康威生命遊戲 。
開始吧!
實現康威生命遊戲
康威生命遊戲的其中一個要點是所有 細胞 必須同時基於當前細胞在面板中的狀態確定下一個細胞的狀態。也就是說如果細胞 (X=3,Y=4)
在計算過程中狀態發生了改變,那麼鄰近的細胞 (X=4,Y=4)
必須基於 (X=3,Y=4)
的狀態決定自己的狀態變化,而不是基於自己現在的狀態。簡單的講,這意味著我們必須遍歷細胞,確定下一個細胞的狀態,而在繪製之前不改變他們的當前狀態,然後在下一次循環中我們將新狀態應用到遊戲里,依此循環往複。
為了完成這個功能,我們需要在 cell
結構體中添加兩個布爾型變數:
type cell struct {
drawable uint32
alive bool
aliveNext bool
x int
y int
}
這裡我們添加了 alive
和 aliveNext
,前一個是細胞當前的專題,後一個是經過計算後下一回合的狀態。
現在添加兩個函數,我們會用它們來確定 cell 的狀態:
// checkState 函數決定下一次遊戲循環時的 cell 狀態
func (c *cell) checkState(cells [][]*cell) {
c.alive = c.aliveNext
c.aliveNext = c.alive
liveCount := c.liveNeighbors(cells)
if c.alive {
// 1. 當任何一個存活的 cell 的附近少於 2 個存活的 cell 時,該 cell 將會消亡,就像人口過少所導致的結果一樣
if liveCount < 2 {
c.aliveNext = false
}
// 2. 當任何一個存活的 cell 的附近有 2 至 3 個存活的 cell 時,該 cell 在下一代中仍然存活。
if liveCount == 2 || liveCount == 3 {
c.aliveNext = true
}
// 3. 當任何一個存活的 cell 的附近多於 3 個存活的 cell 時,該 cell 將會消亡,就像人口過多所導致的結果一樣
if liveCount > 3 {
c.aliveNext = false
}
} else {
// 4. 任何一個消亡的 cell 附近剛好有 3 個存活的 cell,該 cell 會變為存活的狀態,就像重生一樣。
if liveCount == 3 {
c.aliveNext = true
}
}
}
// liveNeighbors 函數返回當前 cell 附近存活的 cell 數
func (c *cell) liveNeighbors(cells [][]*cell) int {
var liveCount int
add := func(x, y int) {
// If we're at an edge, check the other side of the board.
if x == len(cells) {
x = 0
} else if x == -1 {
x = len(cells) - 1
}
if y == len(cells[x]) {
y = 0
} else if y == -1 {
y = len(cells[x]) - 1
}
if cells[x][y].alive {
liveCount++
}
}
add(c.x-1, c.y) // To the left
add(c.x+1, c.y) // To the right
add(c.x, c.y+1) // up
add(c.x, c.y-1) // down
add(c.x-1, c.y+1) // top-left
add(c.x+1, c.y+1) // top-right
add(c.x-1, c.y-1) // bottom-left
add(c.x+1, c.y-1) // bottom-right
return liveCount
}
在 checkState
中我們設置當前狀態(alive
) 等於我們最近迭代結果(aliveNext
)。接下來我們計數鄰居數量,並根據遊戲的規則來決定 aliveNext
狀態。該規則是比較清晰的,而且我們在上面的代碼當中也有說明,所以這裡不再贅述。
更加值得注意的是 liveNeighbors
函數里,我們返回的是當前處於存活(alive
)狀態的細胞的鄰居個數。我們定義了一個叫做 add
的內嵌函數,它會對 X
和 Y
坐標做一些重複性的驗證。它所做的事情是檢查我們傳遞的數字是否超出了範圍——比如說,如果細胞 (X=0,Y=5)
想要驗證它左邊的細胞,它就得驗證面板另一邊的細胞 (X=9,Y=5)
,Y 軸與之類似。
在 add
內嵌函數後面,我們給當前細胞附近的八個細胞分別調用 add
函數,示意如下:
[
[-, -, -],
[N, N, N],
[N, C, N],
[N, N, N],
[-, -, -]
]
在該示意中,每一個叫做 N 的細胞是 C 的鄰居。
現在是我們的 main
函數,這裡我們執行核心遊戲循環,調用每個細胞的 checkState
函數進行繪製:
func main() {
...
for !window.ShouldClose() {
for x := range cells {
for _, c := range cells[x] {
c.checkState(cells)
}
}
draw(cells, window, program)
}
}
現在我們的遊戲邏輯全都設置好了,我們需要修改細胞繪製函數來跳過繪製不存活的細胞:
func (c *cell) draw() {
if !c.alive {
return
}
gl.BindVertexArray(c.drawable)
gl.DrawArrays(gl.TRIANGLES, 0, int32(len(square)/3))
}
如果我們現在運行這個遊戲,你將看到一個純黑的屏幕,而不是我們辛苦工作後應該看到生命模擬。為什麼呢?其實這正是模擬在工作。因為我們沒有活著的細胞,所以就一個都不會繪製出來。
現在完善這個函數。回到 makeCells
函數,我們用 0.0
到 1.0
之間的一個隨機數來設置遊戲的初始狀態。我們會定義一個大小為 0.15
的常量閾值,也就是說每個細胞都有 15% 的幾率處於存活狀態。
import (
"math/rand"
"time"
...
)
const (
...
threshold = 0.15
)
func makeCells() [][]*cell {
rand.Seed(time.Now().UnixNano())
cells := make([][]*cell, rows, rows)
for x := 0; x < rows; x++ {
for y := 0; y < columns; y++ {
c := newCell(x, y)
c.alive = rand.Float64() < threshold
c.aliveNext = c.alive
cells[x] = append(cells[x], c)
}
}
return cells
}
我們首先增加兩個引入:隨機(math/rand
)和時間(time
),並定義我們的常量閾值。然後在 makeCells
中我們使用當前時間作為隨機種子,給每個遊戲一個獨特的起始狀態。你也可也指定一個特定的種子值,來始終得到一個相同的遊戲,這在你想重放某個有趣的模擬時很有用。
接下來在循環中,在用 newCell
函數創造一個新的細胞時,我們根據隨機浮點數的大小設置它的存活狀態,隨機數在 0.0
到 1.0
之間,如果比閾值(0.15
)小,就是存活狀態。再次強調,這意味著每個細胞在開始時都有 15% 的幾率是存活的。你可以修改數值大小,增加或者減少當前遊戲中存活的細胞。我們還把 aliveNext
設成 alive
狀態,否則在第一次迭代之後我們會發現一大片細胞消亡了,這是因為 aliveNext
將永遠是 false
。
現在繼續運行它,你很有可能看到細胞們一閃而過,但你卻無法理解這是為什麼。原因可能在於你的電腦太快了,在你能夠看清楚之前就運行了(甚至完成了)模擬過程。
讓我們降低遊戲速度,在主循環中引入一個幀率(FPS)限制:
const (
...
fps = 2
)
func main() {
...
for !window.ShouldClose() {
t := time.Now()
for x := range cells {
for _, c := range cells[x] {
c.checkState(cells)
}
}
if err := draw(prog, window, cells); err != nil {
panic(err)
}
time.Sleep(time.Second/time.Duration(fps) - time.Since(t))
}
}
現在你能給看出一些圖案了,儘管它變換的很慢。把 FPS 加到 10,把方格的尺寸加到 100x100,你就能看到更真實的模擬:
const (
...
rows = 100
columns = 100
fps = 10
...
)
試著修改常量,看看它們是怎麼影響模擬過程的 —— 這是你用 Go 語言寫的第一個 OpenGL 程序,很酷吧?
進階內容?
這是《OpenGL 與 Go 教程》的最後一節,但是這不意味著到此而止。這裡有些新的挑戰,能夠增進你對 OpenGL (以及 Go)的理解。
- 給每個細胞一種不同的顏色。
- 讓用戶能夠通過命令行參數指定格子尺寸、幀率、種子和閾值。在 GitHub 上的 github.com/KyleBanks/conways-gol 里你可以看到一個已經實現的程序。
- 把格子的形狀變成其它更有意思的,比如六邊形。
- 用顏色表示細胞的狀態 —— 比如,在第一幀把存活狀態的格子設成綠色,如果它們存活了超過三幀的時間,就變成黃色。
- 如果模擬過程結束了,就自動關閉窗口,也就是說所有細胞都消亡了,或者是最後兩幀里沒有格子的狀態有改變。
- 將著色器源代碼放到單獨的文件中,而不是把它們用字元串的形式放在 Go 的源代碼中。
總結
希望這篇教程對想要入門 OpenGL (或者是 Go)的人有所幫助!這很有趣,因此我也希望理解學習它也很有趣。
正如我所說的,OpenGL 可能是非常恐怖的,但只要你開始著手了就不會太差。你只用制定一個個可達成的小目標,然後享受每一次成功,因為儘管 OpenGL 不會總像它看上去的那麼難,但也肯定有些難懂的東西。我發現,當遇到一個難於理解用 go-gl 生成的代碼的 OpenGL 問題時,你總是可以參考一下在網上更流行的當作教程的 C 語言代碼,這很有用。通常 C 語言和 Go 語言的唯一區別是在 Go 中,gl 函數的前綴是 gl.
而不是 gl
,常量的前綴是 gl
而不是 GL_
。這可以極大地增加了你的繪製知識!
該教程的完整源代碼可從 GitHub 上獲得。
回顧
這是 main.go 文件最終的內容:
package main
import (
"fmt"
"log"
"math/rand"
"runtime"
"strings"
"time"
"github.com/go-gl/gl/v4.1-core/gl" // OR: github.com/go-gl/gl/v2.1/gl
"github.com/go-gl/glfw/v3.2/glfw"
)
const (
width = 500
height = 500
vertexShaderSource = `
#version 410
in vec3 vp;
void main() {
gl_Position = vec4(vp, 1.0);
}
` + "x00"
fragmentShaderSource = `
#version 410
out vec4 frag_colour;
void main() {
frag_colour = vec4(1, 1, 1, 1.0);
}
` + "x00"
rows = 100
columns = 100
threshold = 0.15
fps = 10
)
var (
square = []float32{
-0.5, 0.5, 0,
-0.5, -0.5, 0,
0.5, -0.5, 0,
-0.5, 0.5, 0,
0.5, 0.5, 0,
0.5, -0.5, 0,
}
)
type cell struct {
drawable uint32
alive bool
aliveNext bool
x int
y int
}
func main() {
runtime.LockOSThread()
window := initGlfw()
defer glfw.Terminate()
program := initOpenGL()
cells := makeCells()
for !window.ShouldClose() {
t := time.Now()
for x := range cells {
for _, c := range cells[x] {
c.checkState(cells)
}
}
draw(cells, window, program)
time.Sleep(time.Second/time.Duration(fps) - time.Since(t))
}
}
func draw(cells [][]*cell, window *glfw.Window, program uint32) {
gl.Clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT)
gl.UseProgram(program)
for x := range cells {
for _, c := range cells[x] {
c.draw()
}
}
glfw.PollEvents()
window.SwapBuffers()
}
func makeCells() [][]*cell {
rand.Seed(time.Now().UnixNano())
cells := make([][]*cell, rows, rows)
for x := 0; x < rows; x++ {
for y := 0; y < columns; y++ {
c := newCell(x, y)
c.alive = rand.Float64() < threshold
c.aliveNext = c.alive
cells[x] = append(cells[x], c)
}
}
return cells
}
func newCell(x, y int) *cell {
points := make([]float32, len(square), len(square))
copy(points, square)
for i := 0; i < len(points); i++ {
var position float32
var size float32
switch i % 3 {
case 0:
size = 1.0 / float32(columns)
position = float32(x) * size
case 1:
size = 1.0 / float32(rows)
position = float32(y) * size
default:
continue
}
if points[i] < 0 {
points[i] = (position * 2) - 1
} else {
points[i] = ((position + size) * 2) - 1
}
}
return &cell{
drawable: makeVao(points),
x: x,
y: y,
}
}
func (c *cell) draw() {
if !c.alive {
return
}
gl.BindVertexArray(c.drawable)
gl.DrawArrays(gl.TRIANGLES, 0, int32(len(square)/3))
}
// checkState 函數決定下一次遊戲循環時的 cell 狀態
func (c *cell) checkState(cells [][]*cell) {
c.alive = c.aliveNext
c.aliveNext = c.alive
liveCount := c.liveNeighbors(cells)
if c.alive {
// 1. 當任何一個存活的 cell 的附近少於 2 個存活的 cell 時,該 cell 將會消亡,就像人口過少所導致的結果一樣
if liveCount < 2 {
c.aliveNext = false
}
// 2. 當任何一個存活的 cell 的附近有 2 至 3 個存活的 cell 時,該 cell 在下一代中仍然存活。
if liveCount == 2 || liveCount == 3 {
c.aliveNext = true
}
// 3. 當任何一個存活的 cell 的附近多於 3 個存活的 cell 時,該 cell 將會消亡,就像人口過多所導致的結果一樣
if liveCount > 3 {
c.aliveNext = false
}
} else {
// 4. 任何一個消亡的 cell 附近剛好有 3 個存活的 cell,該 cell 會變為存活的狀態,就像重生一樣。
if liveCount == 3 {
c.aliveNext = true
}
}
}
// liveNeighbors 函數返回當前 cell 附近存活的 cell 數
func (c *cell) liveNeighbors(cells [][]*cell) int {
var liveCount int
add := func(x, y int) {
// If we're at an edge, check the other side of the board.
if x == len(cells) {
x = 0
} else if x == -1 {
x = len(cells) - 1
}
if y == len(cells[x]) {
y = 0
} else if y == -1 {
y = len(cells[x]) - 1
}
if cells[x][y].alive {
liveCount++
}
}
add(c.x-1, c.y) // To the left
add(c.x+1, c.y) // To the right
add(c.x, c.y+1) // up
add(c.x, c.y-1) // down
add(c.x-1, c.y+1) // top-left
add(c.x+1, c.y+1) // top-right
add(c.x-1, c.y-1) // bottom-left
add(c.x+1, c.y-1) // bottom-right
return liveCount
}
// initGlfw 初始化 glfw,返回一個可用的 Window
func initGlfw() *glfw.Window {
if err := glfw.Init(); err != nil {
panic(err)
}
glfw.WindowHint(glfw.Resizable, glfw.False)
glfw.WindowHint(glfw.ContextVersionMajor, 4)
glfw.WindowHint(glfw.ContextVersionMinor, 1)
glfw.WindowHint(glfw.OpenGLProfile, glfw.OpenGLCoreProfile)
glfw.WindowHint(glfw.OpenGLForwardCompatible, glfw.True)
window, err := glfw.CreateWindow(width, height, "Conway's Game of Life", nil, nil)
if err != nil {
panic(err)
}
window.MakeContextCurrent()
return window
}
// initOpenGL 初始化 OpenGL 並返回一個已經編譯好的著色器程序
func initOpenGL() uint32 {
if err := gl.Init(); err != nil {
panic(err)
}
version := gl.GoStr(gl.GetString(gl.VERSION))
log.Println("OpenGL version", version)
vertexShader, err := compileShader(vertexShaderSource, gl.VERTEX_SHADER)
if err != nil {
panic(err)
}
fragmentShader, err := compileShader(fragmentShaderSource, gl.FRAGMENT_SHADER)
if err != nil {
panic(err)
}
prog := gl.CreateProgram()
gl.AttachShader(prog, vertexShader)
gl.AttachShader(prog, fragmentShader)
gl.LinkProgram(prog)
return prog
}
// makeVao 初始化並從提供的點裡面返回一個頂點數組
func makeVao(points []float32) uint32 {
var vbo uint32
gl.GenBuffers(1, &vbo)
gl.BindBuffer(gl.ARRAY_BUFFER, vbo)
gl.BufferData(gl.ARRAY_BUFFER, 4*len(points), gl.Ptr(points), gl.STATIC_DRAW)
var vao uint32
gl.GenVertexArrays(1, &vao)
gl.BindVertexArray(vao)
gl.EnableVertexAttribArray(0)
gl.BindBuffer(gl.ARRAY_BUFFER, vbo)
gl.VertexAttribPointer(0, 3, gl.FLOAT, false, 0, nil)
return vao
}
func compileShader(source string, shaderType uint32) (uint32, error) {
shader := gl.CreateShader(shaderType)
csources, free := gl.Strs(source)
gl.ShaderSource(shader, 1, csources, 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
}
請在 Twitter @kylewbanks 告訴我這篇文章對你是否有幫助,或者在 Twitter 下方關注我以便及時獲取最新文章!
via: https://kylewbanks.com/blog/tutorial-opengl-with-golang-part-3-implementing-the-game
作者:kylewbanks 譯者:GitFuture 校對:wxy
本文轉載來自 Linux 中國: https://github.com/Linux-CN/archive