DEV Community

Cover image for Building a Snake Game With Fyne and Go.
FORCHA
FORCHA

Posted on

Building a Snake Game With Fyne and Go.

  1. How to install GO.
  2. How to install fyne.
  3. Drawing Snake and Fruit on Fyne Canvas.
  4. Adding a timer to move the snake.
  5. Using Keys to Control direction.

** 1 How to install Go**
The GO can be downloaded and installed from the official Golang website https://go.dev/doc/install
In this tutorial i will be using a windows environment. I f you are on Mac or Linux please follow the tutorial given on the official golang docs

2 How to install Fyne
2.1. For those on Windows environments download and install the GCC compiler. If you are running on a different OS you need to follow this guide Fyne Getting Started.

2.2 Using command Line create directory myapp

mkdir myapp && cd myapp
Enter fullscreen mode Exit fullscreen mode

2.3. In myapp directory Run the following command and replace MODULE_NAME with your preferred module name (this should be called in a new folder specific for your application).

go mod init MODULE_NAME
Enter fullscreen mode Exit fullscreen mode

2.4. You now need to download the Fyne module. This will be done using the following command:

go get fyne.io/fyne/v2
Enter fullscreen mode Exit fullscreen mode

2.5. To finish your module’s set up, you now need to tidy the module file to correctly reference Fyne as a dependency.

 go mod tidy
Enter fullscreen mode Exit fullscreen mode

3 Drawing Snake and Fruit on Fyne Canvas
3.1. Firstly, we will create a setup function that will build the game screen. We will call
this function setupGame and create list that we we populate.

3.2 By iterating through a loop of 10 elements (i from 0 to 9) and make a new Rectangle for each position. The elements are all created 10 x 20 in size and placed one above the other using the Move function. We
will add them all to the segment slice.

3.3 The return from this method is a container with no layout so that we can later use a manual layout for the visual elements:

func setupGame() *fyne.Container {
    for i := 0; i < lenght; i++ {
        seg := snakePart{9, float32(5 + i)}
        snakeParts = append(snakeParts, seg)

        r := canvas.NewRectangle(&color.RGBA{G: 0x66, A: 0xff})
        r.Resize(fyne.NewSize(10, 20))
        r.Move(fyne.NewPos(150, float32(500+i*20)))
        segments = append(segments, r)
    }

    head = canvas.NewRectangle(&color.RGBA{G: 0x66, A: 0xff})
    head.Resize(fyne.NewSize(10, 20))
    head.Move(fyne.NewPos(snakeParts[0].x*50, snakeParts[0].y*50))

    fruit = canvas.NewCircle(&color.RGBA{R: 0x66, A: 0xff})
    fruit.Resize(fyne.NewSize(10, 10))
    fruitPos= fyne.NewPos(snakeParts[0].x*20, snakeParts[0].y*20)

    fruit.Move(fruitPos)

    segments = append(segments, head)
    segments = append(segments, fruit)

    c:= fyne.NewContainerWithoutLayout(segments...)
    return c
}

Enter fullscreen mode Exit fullscreen mode

3.4. With the graphical setup code, we can wrap this in the usual application load code, this time passing the result of setupGame() to the SetContent function. As this game will not have dynamic sizing, we will call SetFixedSize(true) so that the window cannot be resized

func main() {
    a := app.New()

    w := a.NewWindow("Snake")
    w.Resize(fyne.NewSize(800, 800))
    w.SetFixedSize(false)
    game = setupGame()
    w.SetContent(game)

    w.Canvas().SetOnTypedKey(keyTyped)

    go runGame()
    w.ShowAndRun()
}

Enter fullscreen mode Exit fullscreen mode

4. Adding a timer to move the snake
The next step is to add some motion to the game. We will start with a simple timer that
repositions the snake on screen:

4.1. To help manage the game state, we will define a new type to store the x, y value of
each snake segment, named snakePart. We then make a slice that contains all of
the elements, and this is what we will update as the snake moves around the screen.

type snakePart struct {
    x, y float32
}
var (
    snakeParts []snakePart
    game       *fyne.Container
    head       *canvas.Rectangle
    fruit      *canvas.Circle
    move       = moveDown
    segments []fyne.CanvasObject
    lenght        int = 20
    fruitPos     fyne.Position 
   )
Enter fullscreen mode Exit fullscreen mode

4.2. We create a new function that will update the rectangles that we created earlier based on updated snake section information. To make sure that the game refreshes each time we move the snake, we need to move the rectangles and call ** Refresh()**.

func refreshGame() {

    for i, seg := range snakeParts {
        rect := game.Objects[i]
        rect.Move(fyne.NewPos(seg.x*10, seg.y*20))
    }

    game.Refresh()
}

Enter fullscreen mode Exit fullscreen mode

4.3. To run the main game loop, we need one more function that will use a timer of 250 milliseconds to move the snake. We call this function runGame.

4.4. To move the snake forward , we copy the position of each element from that of the element that is one segment further forward, working from the tail to the head.

5. Using keys to control direction
5.1. To start with, we define a new type (moveType) that will be used to describe the
next direction in which to move. The move variable is then defined to hold the next move direction:

type moveType int
const (
 moveUp moveType = iota
 moveDown
 moveLeft
 moveRight
)
var move = moveUp
Enter fullscreen mode Exit fullscreen mode

5.2. Create a new keyTyped function as follows that will perform the keyboard
mapping:

func keyTyped(e *fyne.KeyEvent) {
 switch e.Name {
 case fyne.KeyUp:
 move = moveUp
 case fyne.KeyDown:
 move = moveDown
 case fyne.KeyLeft:
 move = moveLeft
 case fyne.KeyRight:
 move = moveRight
 }
}
Enter fullscreen mode Exit fullscreen mode

5.3. For the key events to be triggered we use the SetOnKeyTyped() function on the window's canvas.

w.Canvas().SetOnTypedKey(keyTyped)
Enter fullscreen mode Exit fullscreen mode

5.4. Lastly, the code moves the head to a new position, in this case further up the screen (by using snakeParts[0].y-1). Refer to the following function:

5.5. We set up our animations inside the for loop in the runGame function, before the
timer pause

func runGame() {
    nextPart := snakePart{snakeParts[0].x, snakeParts[0].y - 1}
    for {
        oldPos := fyne.NewPos(snakeParts[0].x*10, snakeParts[0].y*10)
        newPos := fyne.NewPos(nextPart.x*10, nextPart.y*10)
        canvas.NewPositionAnimation(oldPos, newPos, time.Millisecond*250, func(p fyne.Position) {
            head.Move(p)
            canvas.Refresh(head)
        }).Start()

        end := len(snakeParts) - 1
        canvas.NewPositionAnimation(fyne.NewPos(snakeParts[end].x*10, snakeParts[end].y*10),
            fyne.NewPos(snakeParts[end-1].x*10, snakeParts[end-1].y*10), time.Millisecond*250,
            func(p fyne.Position) {
                tail := game.Objects[end]
                tail.Move(p)
                canvas.Refresh(tail)
            }).Start()

        time.Sleep(time.Millisecond * 250)
        for i := len(snakeParts) - 1; i >= 1; i-- {
            snakeParts[i] = snakeParts[i-1]
        }
        snakeParts[0] = nextPart //SetUp the new head position
        refreshGame()

    }
}

Enter fullscreen mode Exit fullscreen mode

5.6. After the refresh line, we need to update the movement calculations to set up nextPart ready for the next movement:

 switch move {
 case moveUp:
 nextPart = snakePart{nextPart.x,
 nextPart.y - 1}
 case moveDown:
 nextPart = snakePart{nextPart.x,
 nextPart.y + 1}
 case moveLeft:
 nextPart = snakePart{nextPart.x - 1,
 nextPart.y}
 case moveRight:
 nextPart = snakePart{nextPart.x + 1,
 nextPart.y}
 }
Enter fullscreen mode Exit fullscreen mode

Discussion (1)

Collapse
karanpratapsingh profile image
Karan Pratap Singh

Interesting, thanks for sharing!