DEV Community

Cover image for Learn Golang By Building A 2D Snake Game: A Complete Guide
Kuldeep Singh
Kuldeep Singh

Posted on • Originally published at programmingeeksclub.com

Learn Golang By Building A 2D Snake Game: A Complete Guide

Learning a new programming language can be a daunting task, but working on a fun project like building a game can make the process enjoyable and effective. In this tutorial, we will learn Golang by creating a simple snake game. Golang, or Go, is an open-source programming language designed by Google that makes it easy to build simple, reliable, and efficient software.

By the end of this tutorial, you will have a basic understanding of Go and its core concepts, such as variables, functions, loops, and structs, as well as a working snake game to show off your new skills.


Setting Up Your Go Environment

Before you start building the game, You need to set up your Go environment. You can download Go from the official website at https://golang.org/dl/. Once Go installed, Make sure to set up your GOPATH and configure your environment variables correctly.


Introduction to Ebiten

For building a 2d game we are going to use Ebiten package. Ebiten is a 2D game library for Go that makes it easy to develop cross-platform games. Ebiten provides essential game features, Such as drawing images and handling user input. To get started with Ebiten, Install the library by running the following command:

go get -u github.com/hajimehoshi/ebiten/v2
Enter fullscreen mode Exit fullscreen mode

Setting Up the Game Environment

Begin by creating a new Go file and importing the required packages, such as fmt , log , math/rand , time , and github.com/hajimehoshi/ebiten/v2. Next, create constants for the screen width and height, and tile size.

const (
    screenWidth  = 320
    screenHeight = 240
    tileSize     = 5
)
Enter fullscreen mode Exit fullscreen mode

The main function should initialize the game state and call ebiten.RunGame() to start the game loop.

func main() {
    rand.Seed(time.Now().UnixNano())
    game := &Game{
        snake:    NewSnake(),
        food:     NewFood(),
        gameOver: false,
        ticks:    0,
        speed:    10,
    }
    ebiten.SetWindowSize(screenWidth*2, screenHeight*2)
    ebiten.SetWindowTitle("Snake Game")
    if err := ebiten.RunGame(game); err != nil {
        log.Fatal(err)
    }
}
Enter fullscreen mode Exit fullscreen mode

This code sets up a basic Ebiten game window with a size of 640×480 pixels.

Creating the Snake Structure

Create a Point struct to represent x and y coordinates, and a Snake struct to represent the snake's body, direction, and growth counter.

type Point struct {
    X int
    Y int
}

type Snake struct {
    Body        []Point
    Direction   Point
    GrowCounter int
}
Enter fullscreen mode Exit fullscreen mode

Now implement a NewSnake() function to create a new snake instance and a Move() method to update the snake's position based on its direction. The Move() method should also handle the snake's growth by keeping or removing the tail segment based on the GrowCounter.

func NewSnake() *Snake {
    return &Snake{
        Body: []Point{
            {X: screenWidth / tileSize / 2, Y: screenHeight / tileSize / 2},
        },
        Direction: Point{X: 1, Y: 0},
    }
}

func (s *Snake) Move() {
    newHead := Point{
        X: s.Body[0].X + s.Direction.X,
        Y: s.Body[0].Y + s.Direction.Y,
    }
    s.Body = append([]Point{newHead}, s.Body...)

    if s.GrowCounter > 0 {
        s.GrowCounter--
    } else {
        s.Body = s.Body[:len(s.Body)-1]
    }
}
Enter fullscreen mode Exit fullscreen mode

NewSnake function initializes the snake with a single body part located in the center of the screen and sets the initial direction to the right.

Move function calculates the new head position based on the current head position and direction. It then adds the new head to the body and removes the tail unless the snake is growing.

Implementing the Food

Now Create a Food struct to represent the food’s position and a NewFood() function to create a new food instance with a random position on the game screen.

type Food struct {
    Position Point
}

func NewFood() *Food {
    return &Food{
        Position: Point{
            X: rand.Intn(screenWidth / tileSize),
            Y: rand.Intn(screenHeight / tileSize),
        },
    }
}
Enter fullscreen mode Exit fullscreen mode

Handling User Input and Game Updates

Create a Game struct to represent the game state, including the snake, food, score, speed, and game over flag.

type Game struct {
    snake         *Snake
    food          *Food
    score         int
    gameOver      bool
    ticks         int
    updateCounter int
    speed         int
}
Enter fullscreen mode Exit fullscreen mode

Implement the Update() method for the Game struct to handle user input, update the snake's position, check for collisions, and handle the snake eating the food. If the snake eats the food, increment the score and update the food position.

func (g *Game) Update() error {
    if g.gameOver {
        if inpututil.IsKeyJustPressed(ebiten.KeyR) {
            g.restart()
        }
        return nil
    }

    g.updateCounter++
    if g.updateCounter < g.speed {
        return nil
    }
    g.updateCounter = 0

    // Update the snake's position
    g.snake.Move()

    if ebiten.IsKeyPressed(ebiten.KeyLeft) && g.snake.Direction.X == 0 {
        g.snake.Direction = Point{X: -1, Y: 0}
    } else if ebiten.IsKeyPressed(ebiten.KeyRight) && g.snake.Direction.X == 0 {
        g.snake.Direction = Point{X: 1, Y: 0}
    } else if ebiten.IsKeyPressed(ebiten.KeyUp) && g.snake.Direction.Y == 0 {
        g.snake.Direction = Point{X: 0, Y: -1}
    } else if ebiten.IsKeyPressed(ebiten.KeyDown) && g.snake.Direction.Y == 0 {
        g.snake.Direction = Point{X: 0, Y: 1}
    }

    head := g.snake.Body[0]
    if head.X < 0 || head.Y < 0 || head.X >= screenWidth/tileSize || head.Y >= screenHeight/tileSize {
        g.gameOver = true
        g.speed = 10
    }

    for _, part := range g.snake.Body[1:] {
        if head.X == part.X && head.Y == part.Y {
            g.gameOver = true
            g.speed = 10
        }
    }

    if head.X == g.food.Position.X && head.Y == g.food.Position.Y {
        g.score++
        g.snake.GrowCounter += 1
        g.food = NewFood()
        g.score++
        g.food = NewFood()
        g.snake.GrowCounter = 1

        // Decrease speed (with a lower limit)
        if g.speed > 2 {
            g.speed--
        }
    }

    return nil
}
Enter fullscreen mode Exit fullscreen mode

In the update function we're handling, game over logic, speed, user input, and restart game functionalities.

Drawing the Game Objects

Implement the Draw() method for the Game struct to render the game objects, such as the snake, food, and game over screen. Use Ebiten's drawing functions, like DrawRect(), to draw the snake and food as rectangles and the text.Draw() function to display the score and game over message.

func (g *Game) Draw(screen *ebiten.Image) {
    // Draw background
    screen.Fill(color.RGBA{0, 0, 0, 255})

    // Draw snake
    for _, p := range g.snake.Body {
        ebitenutil.DrawRect(screen, float64(p.X*tileSize), float64(p.Y*tileSize), tileSize, tileSize, color.RGBA{0, 255, 0, 255})
    }

    // Draw food
    ebitenutil.DrawRect(screen, float64(g.food.Position.X*tileSize), float64(g.food.Position.Y*tileSize), tileSize, tileSize, color.RGBA{255, 0, 0, 255})

    // Create a font.Face
    face := basicfont.Face7x13

    // Draw game over text
    if g.gameOver {
        text.Draw(screen, "Game Over", face, screenWidth/2-40, screenHeight/2, color.White)
        text.Draw(screen, "Press 'R' to restart", face, screenWidth/2-60, screenHeight/2+16, color.White)
    }

    // Draw score
    scoreText := fmt.Sprintf("Score: %d", g.score)
    text.Draw(screen, scoreText, face, 5, screenHeight-5, color.White)
}
Enter fullscreen mode Exit fullscreen mode

Restarting the Game

At last implement a restart() method for the Game struct to reset the game state when the player presses 'R' after a game over.

func (g *Game) restart() {
    g.snake = NewSnake()
    g.score = 0
    g.gameOver = false
    g.food = NewFood()
    g.speed = 10
}
Enter fullscreen mode Exit fullscreen mode

This code checks if the R key is pressed when the game is over, and if so, it restarts the game by calling the restart method. The restart method resets the game state, including the snake, score, and gameOver flag.

Running Game

Golang Snake Game

Follow me on twitter and message me, I’ll send you the full working code at once.

Conclusion

Congratulations! You’ve now built a simple snake game using Golang and the Ebiten library. In the process, you’ve learned the basics of Golang, including variables, functions, loops, and structs, as well as how to handle user input and collisions in a game.

You can further enhance this game by adding more features such as different levels, obstacles, or power-ups. The possibilities are endless, and you now have a solid foundation to build upon as you continue learning Golang through game development. Happy coding!

If you’re interested in learning more about programming and related topics, we invite you to check out our website programmingeeksclub.com. We offer valuable resources and insights.

You can find us on Twitter and Facebook.

Download my first ebook about mastering markdown, from here: Download , reviews and recommendations are appreciated.

Top comments (0)