2. 畫布、顏色與矩形——在 Golang 透過 Ebiten(炸蝦)來製作 8-Bit 遊戲!

在上一章我們提到了如何建立一個基本的遊戲框架,但那還不足以構成一個遊戲(畢竟只有一串文字嘛),在那之前,我們需要知道更多有關遊戲畫面的資訊,所以接下來要提到的就是「螢幕畫布」。

螢幕畫布是整個遊戲中最重要的地方,螢幕畫布用來呈現任何你能夠看見的東西,在你不注意的時候螢幕畫布可能已經更新了數百次,但是因為速度夠快所以你並不會察覺

現在讓我們接續上次的程式碼吧!如果你忘記了上次的程式碼長怎樣,我已經幫你帶到這邊來了,順帶一提,我稍稍加上了一些註釋 :)

package main

import (  
    "github.com/hajimehoshi/ebiten"
    "github.com/hajimehoshi/ebiten/ebitenutil"
)

func update(screen *ebiten.Image) error {  
    // 顯示一串除錯用的文字
    ebitenutil.DebugPrint(screen, "Our first game in Ebiten!")

    return nil
}

func main() {  
    // 初始化 Ebiten,並不斷呼叫 update()
    ebiten.Run(update, 320, 240, 2, "Hello world!")
}

還記得這會出現下面這樣的畫面,對吧?

姆⋯⋯這看起來有點無聊呢,稍微色彩一下畫布背景如何?

顏色建構體

在替你的畫布著色之前,你需要建立一個顏色建構體,這很簡單,如果你先前有用過相片軟體或是網頁設計,你就會知道所謂的 Hex 色碼#FF0000

但是等一下!為了要建立一個顏色建構體供稍後著色用,你會需要用到 image/color 套件,想要新增一個套件你需要修改程式碼中 import 的區塊,像下面這樣:

import (  
    "github.com/hajimehoshi/ebiten"
    "github.com/hajimehoshi/ebiten/ebitenutil"
    "image/color"
)

棒呆了!現在是時候新增一個顏色了,嗯⋯⋯紅色如何?紅色的色碼是 #FF0000,換成十六進制就是 0xff, 0x00, 0x00,所以就會像這樣(最後面的 0xff 不用動):

color.NRGBA{0xff, 0x00, 0x00, 0xff}  

好了,你已經知道怎麼建立顏色了,接著就是來幫畫布著色了。

畫布填色

在 Ebiten 中,畫布有個 Fill(clr color.Color) 函式,這個意思是你需要傳入一個資料型態是 color.Color 的值,這剛好是我們剛才建立的顏色建構體的資料型態!事不宜遲,趕快來看看如何替我們的畫布著色。

還記得我們的 update 函式嗎?就是那個會不斷被執行的函式,我們要在這裡替我們的畫布著色(你總不會希望顏色閃一下就沒了,對吧?)。

func update(screen *ebiten.Image) error {

    // 先幫畫布填上 #FF0000 顏色
    screen.Fill(color.NRGBA{0xff, 0x00, 0x00, 0xff})

    // 顯示一串除錯用的文字
    ebitenutil.DebugPrint(screen, "Our first game in Ebiten!")

    return nil
}

在這裡你需要注意執行的優先順序,你必須先替畫布著色,然後再顯示文字,不然你的文字會被顏色給蓋過去喔!

儲存你的檔案,透過跟上次一樣的指令 go run ./main.go 執行看看是不是長得像下面這樣了呢?

建立矩形

接下來我們要在畫布上畫幾個矩形,這樣才比較像遊戲,不是嗎?雖然它不會動,但相信我,之後的幾個章節我們會讓它動起來的 ;)。

矩形其實是個新畫布

在這裡希望你先釐清一個觀念,Ebiten 其實沒有矩形任何東西都是一個畫布只是遊戲背景的那一個比較大而已),所以我們等一下要建立的其實就是一個新的畫布,但是比起畫面背景的那個畫布還要小就是了,所以看起來很像矩形或方塊。

新建畫布並將其填色

像剛才說的,其實矩形就只是畫布而已,所以在這裡我會稱其為「畫布」,記住這一點,然後我們繼續往下看。

在 Ebiten 裡透過 ebiten.NewImage(width, height int, filter Filter) 你可以建立另一個畫布,其中你要提供的值有:

  • width 是新畫布的寬度,以像素表示。
  • height 是新畫布的高度,同樣以像素表示。
  • filter 這個是渲染的過濾器,這個我們可以不要管它,等一下照做就好了。

現在像下面這樣建立一個 16x16 的新畫布,長得就像一般的正方形,然後程式會將它儲存至 square 變數中供稍後使用,實際上你愛怎麼稱呼這個變數都行,不過記得,你只能夠用英文混合數字來替他命名。

// 建立一個 16x16 的新畫布
square, _ := ebiten.NewImage(16, 16, ebiten.FilterNearest)

// 替 square 畫布填上白色
square.Fill(color.White)  

在尾端我們用了跟剛才所學到的著色方法來替我們的新畫布上了 color.White 顏色。你總不希望他是透明的,對吧?

你可能會好奇 color.White 是什麼用法?其實在 image/color 套件中有提供兩個預設的顏色,分別是color.Black)與color.White)。

調整位置然後渲染至主畫布

當你建立了新畫布之後,你需要將它渲染到主畫布上,不然他不會出現在螢幕上,但在渲染之前你需要建立一個名為 DrawImageOptions{} 的「選項建構體」,這個建構體主要是用來敘述我們的新畫布應該如何被渲染,但現在可以先留白,等一下才會提及。

先來個完整的範例,然後我們再逐一解釋。上面這些範例都是在 update 中撰寫的,讓我們看看你的 update 應該長成怎麼樣:

func update(screen *ebiten.Image) error {

    // 先幫畫布填上 #FF0000 顏色
    screen.Fill(color.NRGBA{0xff, 0x00, 0x00, 0xff})

    // 顯示一串除錯用的文字
    ebitenutil.DebugPrint(screen, "Our first game in Ebiten!")

    // 建立一個 16x16 的新畫布
    square, _ := ebiten.NewImage(16, 16, ebiten.FilterNearest)

    // 替 square 畫布填上白色
    square.Fill(color.White)

    // 建立一個空白選項建構體
    opts := &ebiten.DrawImageOptions{}

    // 渲染 square 畫布到 screen 主畫布上並套用空白選項
    screen.DrawImage(square, opts)

    return nil
}

DrawImage(image *Image, options *DrawImageOptions) 是渲染畫布的功能,看起來真長!不過放心,其實用起來的方法很簡單。第一個值你需要傳入我們的新畫布第二個則是渲染選項,沒了。簡單吧!

實際上也沒有新增很多的程式碼,老樣子,儲存完之後我們就用 go run ./main.go 來執行看看吧。

啊!我們的文字被遮蔽了!其實這是因為我們沒有設定新畫布座標的緣故,這會是這章最終的教學。

畫布變形

在 Ebiten 中,畫布並沒有座標配置,但是你可以透過變形達到同樣效果,變形的方法有很多種,日後會提到,這裡先提及基本的 x軸y軸座標移動變形

我們可以將「座標移動變形」的效果加入至「選項建構體」,其方法是 Translate(tx, ty float64)float64浮點數的意思,你也可以當作整數用沒關係,在這裡你有兩個值要顧慮:

  • tx 是距離螢幕左側的值,也就是 x軸 偏移。
  • ty 是距離螢幕頂部的值,也就是 y軸 偏移。

所以在你的選項建構體下方新增一行程式碼用來告訴渲染函式我們希望這個新的畫布有座標移動的變形效果

// 剛才的空白選項建構體
opts := &ebiten.DrawImageOptions{}

// 修改選項,新增 Translate 變形效果
opts.GeoM.Translate(64, 64)  

儲存檔案,執行你的程式,你就會看見新畫布會向螢幕左側和頂部偏移 64 個像素點。

順帶一提,如果你新增了很多個畫布,你可以逐一調整每個畫布的位置,然後就可以達到下面這樣的效果。

靜止不動看起來還真是無趣,但之後我們會讓它動起來的!讓我們在下個章節見面吧!