TQ
dev.com

Blog about software development

Subscribe

A 2D puzzle game in Go using Fyne

22 Apr 2024 - by 'Maurits van der Schee'

Last month I wrote Minesweeper written in Go using Fyne. It was a port of the Ebiten game engine implementation to desktop using the Fyne GUI library. During the implementation I ran into 3 problems that I eventually solved. In this post I'll explain what they were and how I solved them.

Source code: https://github.com/mevdschee/fyne-mines

1: MinSize of container without layout

Fyne can use a (JWT-like) borderlayout, but you can also freely place your controls (or "widgets" as Fyne calls them) using a container without a layout. To set the size of this container you can put this container in a a stack container together with an image of a certain (minimum) size.

parentContainer := container.NewStack()
i := canvas.NewImageFromImage(image.NewNRGBA(image.Rect(0, 0, 0, 0)))
i.SetMinSize(fyne.NewSize(float32(width), float32(height)))
parentContainer.Add(i)
childContainer := container.NewWithoutLayout()
parentContainer.Add(childContainer)
// add and move widgets in the childContainer

As you can see you can nest containers to achieve complex layouts.

2: Make Scalemode ImageScalePixels work

I wanted large pixels (size 2x2), but could only achieve smoothed results. When I was setting "ScaleMode = canvas.ImageScalePixels" on a image created using the "SubImage()" method it was not rendered. If I used "draw.Copy()" or the more powerful "draw.NearestNeighbor.Scale()" on an empty image created with "image.NewNRGBA()" it did work, see:

srcRect := image.Rect(srcX, srcY, srcX+srcWidth, srcY+srcHeight)
dstRect := image.Rect(0, 0, srcWidth, srcHeight)
dst := image.NewNRGBA(dstRect)
draw.Copy(dst, image.Point{}, src, srcRect, draw.Over, nil)
img := canvas.NewImageFromImage(dst)
img.ScaleMode = canvas.ImageScalePixels

NB: The simpler "dst := src.SubImage(srcRect)" worked, but not with this pixelated (and faster) scale mode.

3: The canvas.Image is not clickable

Fyne is awesome as it provides many controls (or "widgets" as they are called). You get "Button", "MainMenu" end even a nice (native) "TrayIcon". But for a game I mainly want to handle mouse events on a sprite (image). To handle mouse events on a "canvas.Image" I made an "interactive.Image" as defined below:

package interactive

import (
    "fyne.io/fyne/v2"
    "fyne.io/fyne/v2/canvas"
    "fyne.io/fyne/v2/driver/desktop"
    "fyne.io/fyne/v2/widget"
)

type Image struct {
    *canvas.Image
    onMouseDown  func(ev *desktop.MouseEvent)
    onMouseUp    func(ev *desktop.MouseEvent)
    onMouseIn    func(ev *desktop.MouseEvent)
    onMouseOut   func()
    onMouseMoved func(ev *desktop.MouseEvent)
}

// ensure Mousable and Hoverable
var _ desktop.Mouseable = (*Image)(nil)
var _ desktop.Hoverable = (*Image)(nil)

This is essential for allowing Mouse handlers on images. I used "fyne.NewMainMenu()" to create a main menu and listen to "desktop.MouseEvent" on the canvas images. Note that I've explicitly made a desktop application and therefor have chosen the desktop events and the main menu for optimal desktop application experience.

Conclusion

I have experience building (2d puzzle) games in various technologies and I have recently explored Typescript (see: TicTacToe in TypeScript) and Ebiten (see: Minesweeper written in Go using Ebiten). Typescript is great for DOM based web games and Ebiten is great for pixel based web games. With Fyne I have found a great way to build cross platform 2d puzzle games for the desktop.

Links


PS: Liked this article? Please share it on Facebook, Twitter or LinkedIn.