Ebiten
网友文章学习一下,不过看起来有点老了,可能针对1.0版吧。
简单跨平台,据说支持Windows(No Cgo!)、macOS、Linux、FreeBSD、Android、iOS、WebAssembly。或许得益于golang本身的跨平台性。不过wasm方式我也没有成功。
还是老规矩,HelloWorld。看起来没有默认支持中文(可以想见)
不过看起来Update和Draw是重复的呢?
package main
import (
"log"
"github.com/hajimehoshi/ebiten/v2"
"github.com/hajimehoshi/ebiten/v2/ebitenutil"
)
type Game struct{}
// 默认情况下每秒将Update调用60次
// Update接受一个参数screen,它是一个指向ebiten.Image的指针。这个例子没有使用这个
// Update返回一个错误值。
// 当更新函数返回一个非nil错误时,Ebiten游戏暂停。
func (g *Game) Update() error {
return nil
}
// 如果监视器的刷新率是60 [Hz],绘制被调用60次每秒。
// Draw还接受一个参数screen
func (g *Game) Draw(screen *ebiten.Image) {
ebitenutil.DebugPrint(screen, "Hello, 你个World!") //DebugPrint是一个在图像上呈现调试消息的实用函数
}
// 布局接受外部大小,即桌面上的窗口大小,并返回游戏的逻辑屏幕大小。
func (g *Game) Layout(outsideWidth, outsideHeight int) (screenWidth, screenHeight int) {
return 640, 480
}
func main() {
ebiten.SetWindowSize(1024, 768) // 设置窗口的大小
ebiten.SetWindowTitle("Hello, 你个World!")
if err := ebiten.RunGame(&Game{}); err != nil { // 运行Ebiten游戏主循环的函数
log.Fatal(err)
}
}
看看我们关注的中文
package main
import (
"fmt"
"image/color"
"log"
"math/rand"
"strings"
"time"
"github.com/golang/freetype/truetype"
"golang.org/x/image/font"
"github.com/hajimehoshi/ebiten"
"github.com/hajimehoshi/ebiten/examples/resources/fonts"
"github.com/hajimehoshi/ebiten/text"
)
const (
screenWidth = 1024
screenHeight = 768
)
var (
sampleText = `为什么中文出错呢?`
mplusNormalFont font.Face
mplusBigFont font.Face
counter = 0
kanjiText = []rune{}
kanjiTextColor color.RGBA
)
var jaKanjis = []rune{}
func init() {
const table = `
我人有的和主产不为这工要在地一上是中国经发以了民同
`
for _, c := range table {
if c == '\n' {
continue
}
jaKanjis = append(jaKanjis, c)
}
}
func init() {
tt, err := truetype.Parse(fonts.YaHei_ttf)
// 注意这里我添加并修改了雅黑字体
// 这里需要将字体复制到资源目录,并运行:
// file2byteslice -package=fonts -input=./fonts/yahei.ttf -output=./fonts/yahei.go -var=YaHei_ttf
// 这是 github.com/hajimehoshi/file2byteslice 作者的一个打包工具。
// 当你编译此程序时,可以生成不依赖字库的文件(已包含在运行程序中)
if err != nil {
log.Fatal(err)
}
const dpi = 72
mplusNormalFont = truetype.NewFace(tt, &truetype.Options{
Size: 24,
DPI: dpi,
Hinting: font.HintingFull,
})
mplusBigFont = truetype.NewFace(tt, &truetype.Options{
Size: 48,
DPI: dpi,
Hinting: font.HintingFull,
})
}
func init() {
rand.Seed(time.Now().UnixNano())
}
func update(screen *ebiten.Image) error {
// Change the text color for each second.
if counter%ebiten.MaxTPS() == 0 {
kanjiText = []rune{}
for j := 0; j < 4; j++ {
for i := 0; i < 8; i++ {
kanjiText = append(kanjiText, jaKanjis[rand.Intn(len(jaKanjis))])
}
kanjiText = append(kanjiText, '\n')
}
kanjiTextColor.R = 0x80 + uint8(rand.Intn(0x7f))
kanjiTextColor.G = 0x80 + uint8(rand.Intn(0x7f))
kanjiTextColor.B = 0x80 + uint8(rand.Intn(0x7f))
kanjiTextColor.A = 0xff
}
counter++
if ebiten.IsDrawingSkipped() {
return nil
}
const x = 20
// Draw info
msg := fmt.Sprintf("TPS: %0.2f", ebiten.CurrentTPS())
text.Draw(screen, msg, mplusNormalFont, x, 40, color.White)
// Draw the sample text
text.Draw(screen, sampleText, mplusNormalFont, x, 80, color.White)
// Draw Kanji text lines
for i, line := range strings.Split(string(kanjiText), "\n") {
text.Draw(screen, line, mplusBigFont, x, 160+54*i, kanjiTextColor)
}
return nil
}
func main() {
if err := ebiten.Run(update, screenWidth, screenHeight, 1, "汉字演示"); err != nil {
log.Fatal(err)
}
}
考虑是否可以用它来做go程序的界面,不过这个示例有点长。
新加字体时,需要在golang.org/x/image/font里gen.go生成。
字体设置为微软雅黑,失败,估计是字体太大。改为文泉字体,成功。
看起来它通过画图的方法,将各个控件画在界面上(实际其它实现的低层也是如此)。如果要实现更多的控件,得自己去画,包括一些事件。
package main
import (
"bytes"
"image"
"image/color"
_ "image/png"
"log"
"strings"
"github.com/golang/freetype/truetype"
"golang.org/x/image/font"
"golang.org/x/image/font/gofont/wenquan"
"github.com/hajimehoshi/ebiten"
"github.com/hajimehoshi/ebiten/examples/resources/images"
"github.com/hajimehoshi/ebiten/inpututil"
"github.com/hajimehoshi/ebiten/text"
)
const (
lineHeight = 16
)
var (
uiImage *ebiten.Image
uiFont font.Face
uiFontMHeight int
)
func init() {
// 从字节片(而不是文件)解码图像,以便此示例可以在任何工作目录中工作。(就是说的包含图片到可执行文件中)
img, _, err := image.Decode(bytes.NewReader(images.UI_png)) //这里加载了一个资源文件,包括了基本的UI元素
if err != nil {
log.Fatal(err)
}
uiImage, _ = ebiten.NewImageFromImage(img, ebiten.FilterDefault)
tt, err := truetype.Parse(wenquan.TTF)
if err != nil {
log.Fatal(err)
}
uiFont = truetype.NewFace(tt, &truetype.Options{
Size: 12,
DPI: 72,
Hinting: font.HintingFull,
})
b, _, _ := uiFont.GlyphBounds('M')
uiFontMHeight = (b.Max.Y - b.Min.Y).Ceil()
}
type imageType int
const (
imageTypeButton imageType = iota
imageTypeButtonPressed
imageTypeTextBox
imageTypeVScollBarBack
imageTypeVScollBarFront
imageTypeCheckBox
imageTypeCheckBoxPressed
imageTypeCheckBoxMark
)
var imageSrcRects = map[imageType]image.Rectangle{
imageTypeButton: image.Rect(0, 0, 16, 16),
imageTypeButtonPressed: image.Rect(16, 0, 32, 16),
imageTypeTextBox: image.Rect(0, 16, 16, 32),
imageTypeVScollBarBack: image.Rect(16, 16, 24, 32),
imageTypeVScollBarFront: image.Rect(24, 16, 32, 32),
imageTypeCheckBox: image.Rect(0, 32, 16, 48),
imageTypeCheckBoxPressed: image.Rect(16, 32, 32, 48),
imageTypeCheckBoxMark: image.Rect(32, 32, 48, 48),
}
const (
screenWidth = 640
screenHeight = 480
)
type Input struct {
mouseButtonState int
}
func drawNinePatches(dst *ebiten.Image, dstRect image.Rectangle, srcRect image.Rectangle) {
srcX := srcRect.Min.X
srcY := srcRect.Min.Y
srcW := srcRect.Dx()
srcH := srcRect.Dy()
dstX := dstRect.Min.X
dstY := dstRect.Min.Y
dstW := dstRect.Dx()
dstH := dstRect.Dy()
op := &ebiten.DrawImageOptions{}
for j := 0; j < 3; j++ {
for i := 0; i < 3; i++ {
op.GeoM.Reset()
sx := srcX
sy := srcY
sw := srcW / 4
sh := srcH / 4
dx := 0
dy := 0
dw := sw
dh := sh
switch i {
case 1:
sx = srcX + srcW/4
sw = srcW / 2
dx = srcW / 4
dw = dstW - 2*srcW/4
case 2:
sx = srcX + 3*srcW/4
dx = dstW - srcW/4
}
switch j {
case 1:
sy = srcY + srcH/4
sh = srcH / 2
dy = srcH / 4
dh = dstH - 2*srcH/4
case 2:
sy = srcY + 3*srcH/4
dy = dstH - srcH/4
}
op.GeoM.Scale(float64(dw)/float64(sw), float64(dh)/float64(sh))
op.GeoM.Translate(float64(dx), float64(dy))
op.GeoM.Translate(float64(dstX), float64(dstY))
dst.DrawImage(uiImage.SubImage(image.Rect(sx, sy, sx+sw, sy+sh)).(*ebiten.Image), op)
}
}
}
// Button ---------------------------------------------------------------------
type Button struct {
Rect image.Rectangle
Text string
mouseDown bool
onPressed func(b *Button)
}
func (b *Button) Update() {
if ebiten.IsMouseButtonPressed(ebiten.MouseButtonLeft) {
x, y := ebiten.CursorPosition()
if b.Rect.Min.X <= x && x < b.Rect.Max.X && b.Rect.Min.Y <= y && y < b.Rect.Max.Y {
b.mouseDown = true
} else {
b.mouseDown = false
}
} else {
if b.mouseDown {
if b.onPressed != nil {
b.onPressed(b)
}
}
b.mouseDown = false
}
}
func (b *Button) Draw(dst *ebiten.Image) {
t := imageTypeButton
if b.mouseDown {
t = imageTypeButtonPressed
}
drawNinePatches(dst, b.Rect, imageSrcRects[t])
bounds, _ := font.BoundString(uiFont, b.Text)
w := (bounds.Max.X - bounds.Min.X).Ceil()
x := b.Rect.Min.X + (b.Rect.Dx()-w)/2
y := b.Rect.Max.Y - (b.Rect.Dy()-uiFontMHeight)/2
text.Draw(dst, b.Text, uiFont, x, y, color.Black)
}
func (b *Button) SetOnPressed(f func(b *Button)) {
b.onPressed = f
}
// VScrollBar --------------------------------------------------
const VScrollBarWidth = 16
type VScrollBar struct {
X int
Y int
Height int
thumbRate float64
thumbOffset int
dragging bool
draggingStartOffset int
draggingStartY int
contentOffset int
}
func (v *VScrollBar) thumbSize() int {
const minThumbSize = VScrollBarWidth
r := v.thumbRate
if r > 1 {
r = 1
}
s := int(float64(v.Height) * r)
if s < minThumbSize {
return minThumbSize
}
return s
}
func (v *VScrollBar) thumbRect() image.Rectangle {
if v.thumbRate >= 1 {
return image.Rectangle{}
}
s := v.thumbSize()
return image.Rect(v.X, v.Y+v.thumbOffset, v.X+VScrollBarWidth, v.Y+v.thumbOffset+s)
}
func (v *VScrollBar) maxThumbOffset() int {
return v.Height - v.thumbSize()
}
func (v *VScrollBar) ContentOffset() int {
return v.contentOffset
}
func (v *VScrollBar) Update(contentHeight int) {
v.thumbRate = float64(v.Height) / float64(contentHeight)
if !v.dragging && inpututil.IsMouseButtonJustPressed(ebiten.MouseButtonLeft) {
x, y := ebiten.CursorPosition()
tr := v.thumbRect()
if tr.Min.X <= x && x < tr.Max.X && tr.Min.Y <= y && y < tr.Max.Y {
v.dragging = true
v.draggingStartOffset = v.thumbOffset
v.draggingStartY = y
}
}
if v.dragging {
if ebiten.IsMouseButtonPressed(ebiten.MouseButtonLeft) {
_, y := ebiten.CursorPosition()
v.thumbOffset = v.draggingStartOffset + (y - v.draggingStartY)
if v.thumbOffset < 0 {
v.thumbOffset = 0
}
if v.thumbOffset > v.maxThumbOffset() {
v.thumbOffset = v.maxThumbOffset()
}
} else {
v.dragging = false
}
}
v.contentOffset = 0
if v.thumbRate < 1 {
v.contentOffset = int(float64(contentHeight) * float64(v.thumbOffset) / float64(v.Height))
}
}
func (v *VScrollBar) Draw(dst *ebiten.Image) {
sd := image.Rect(v.X, v.Y, v.X+VScrollBarWidth, v.Y+v.Height)
drawNinePatches(dst, sd, imageSrcRects[imageTypeVScollBarBack])
if v.thumbRate < 1 {
drawNinePatches(dst, v.thumbRect(), imageSrcRects[imageTypeVScollBarFront])
}
}
// TextBox ---------------------------------------------------------
const (
textBoxPaddingLeft = 8
)
type TextBox struct {
Rect image.Rectangle
Text string
contentBuf *ebiten.Image
vScrollBar *VScrollBar
offsetX int
offsetY int
}
func (t *TextBox) AppendLine(line string) {
if t.Text == "" {
t.Text = line
} else {
t.Text += "\n" + line
}
}
func (t *TextBox) Update() {
if t.vScrollBar == nil {
t.vScrollBar = &VScrollBar{}
}
t.vScrollBar.X = t.Rect.Max.X - VScrollBarWidth
t.vScrollBar.Y = t.Rect.Min.Y
t.vScrollBar.Height = t.Rect.Dy()
_, h := t.contentSize()
t.vScrollBar.Update(h)
t.offsetX = 0
t.offsetY = t.vScrollBar.ContentOffset()
}
func (t *TextBox) contentSize() (int, int) {
h := len(strings.Split(t.Text, "\n")) * lineHeight
return t.Rect.Dx(), h
}
func (t *TextBox) viewSize() (int, int) {
return t.Rect.Dx() - VScrollBarWidth - textBoxPaddingLeft, t.Rect.Dy()
}
func (t *TextBox) contentOffset() (int, int) {
return t.offsetX, t.offsetY
}
func (t *TextBox) Draw(dst *ebiten.Image) {
drawNinePatches(dst, t.Rect, imageSrcRects[imageTypeTextBox])
if t.contentBuf != nil {
vw, vh := t.viewSize()
w, h := t.contentBuf.Size()
if vw > w || vh > h {
t.contentBuf.Dispose()
t.contentBuf = nil
}
}
if t.contentBuf == nil {
w, h := t.viewSize()
t.contentBuf, _ = ebiten.NewImage(w, h, ebiten.FilterDefault)
}
t.contentBuf.Clear()
for i, line := range strings.Split(t.Text, "\n") {
x := -t.offsetX + textBoxPaddingLeft
y := -t.offsetY + i*lineHeight + lineHeight - (lineHeight-uiFontMHeight)/2
if y < -lineHeight {
continue
}
if _, h := t.viewSize(); y >= h+lineHeight {
continue
}
text.Draw(t.contentBuf, line, uiFont, x, y, color.Black)
}
op := &ebiten.DrawImageOptions{}
op.GeoM.Translate(float64(t.Rect.Min.X), float64(t.Rect.Min.Y))
dst.DrawImage(t.contentBuf, op)
t.vScrollBar.Draw(dst)
}
// CheckBox -----------------------------------------------
const (
checkboxWidth = 16
checkboxHeight = 16
checkboxPaddingLeft = 8
)
type CheckBox struct {
X int
Y int
Text string
checked bool
mouseDown bool
onCheckChanged func(c *CheckBox)
}
func (c *CheckBox) width() int {
b, _ := font.BoundString(uiFont, c.Text)
w := (b.Max.X - b.Min.X).Ceil()
return checkboxWidth + checkboxPaddingLeft + w
}
func (c *CheckBox) Update() {
if ebiten.IsMouseButtonPressed(ebiten.MouseButtonLeft) {
x, y := ebiten.CursorPosition()
if c.X <= x && x < c.X+c.width() && c.Y <= y && y < c.Y+checkboxHeight {
c.mouseDown = true
} else {
c.mouseDown = false
}
} else {
if c.mouseDown {
c.checked = !c.checked
if c.onCheckChanged != nil {
c.onCheckChanged(c)
}
}
c.mouseDown = false
}
}
func (c *CheckBox) Draw(dst *ebiten.Image) {
t := imageTypeCheckBox
if c.mouseDown {
t = imageTypeCheckBoxPressed
}
r := image.Rect(c.X, c.Y, c.X+checkboxWidth, c.Y+checkboxHeight)
drawNinePatches(dst, r, imageSrcRects[t])
if c.checked {
drawNinePatches(dst, r, imageSrcRects[imageTypeCheckBoxMark])
}
x := c.X + checkboxWidth + checkboxPaddingLeft
y := (c.Y + 16) - (16-uiFontMHeight)/2
text.Draw(dst, c.Text, uiFont, x, y, color.Black)
}
func (c *CheckBox) Checked() bool {
return c.checked
}
func (c *CheckBox) SetOnCheckChanged(f func(c *CheckBox)) {
c.onCheckChanged = f
}
// ------------------------------------------------------
var (
button1 = &Button{
Rect: image.Rect(16, 16, 144, 48),
Text: "按钮1",
}
button2 = &Button{
Rect: image.Rect(160, 16, 288, 48),
Text: "按钮2",
}
checkBox = &CheckBox{
X: 16,
Y: 64,
Text: "选择框",
}
//一个文本框
textBoxLog = &TextBox{
Rect: image.Rect(16, 96, 624, 464),
}
)
func init() {
//事件
button1.SetOnPressed(func(b *Button) {
textBoxLog.AppendLine("Button 1 Pressed")
})
button2.SetOnPressed(func(b *Button) {
textBoxLog.AppendLine("Button 2 Pressed")
})
checkBox.SetOnCheckChanged(func(c *CheckBox) {
msg := "Check box check changed"
if c.Checked() {
msg += " (Checked)"
} else {
msg += " (Unchecked)"
}
textBoxLog.AppendLine(msg)
})
}
func update(screen *ebiten.Image) error {
button1.Update()
button2.Update()
checkBox.Update()
textBoxLog.Update()
if ebiten.IsDrawingSkipped() {
return nil
}
screen.Fill(color.RGBA{0xeb, 0xeb, 0xeb, 0xff})
button1.Draw(screen)
button2.Draw(screen)
checkBox.Draw(screen)
textBoxLog.Draw(screen)
return nil
}
func main() {
if err := ebiten.Run(update, screenWidth, screenHeight, 1, "UI示例"); err != nil {
log.Fatal(err)
}
}
画图
package main
import (
_ "image/png"
"log"
"github.com/hajimehoshi/ebiten/v2"
"github.com/hajimehoshi/ebiten/v2/ebitenutil"
)
var img *ebiten.Image
func init() {
var err error
img, _, err = ebitenutil.NewImageFromFile("gopher.png")
if err != nil {
log.Fatal(err)
}
}
type Game struct{}
func (g *Game) Update() error {
return nil
}
func (g *Game) Draw(screen *ebiten.Image) {
//图片参数
//op := &ebiten.DrawImageOptions{}
//op.GeoM.Translate(50, 50)
//op.GeoM.Scale(1.5, 1)
//screen.DrawImage(img, op)
screen.DrawImage(img, nil) // 关键语句
}
func (g *Game) Layout(outsideWidth, outsideHeight int) (screenWidth, screenHeight int) {
return 640, 480
}
func main() {
ebiten.SetWindowSize(640, 480)
ebiten.SetWindowTitle("Render an image")
if err := ebiten.RunGame(&Game{}); err != nil {
log.Fatal(err)
}
}