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
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 )
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) } }
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 }
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] } }
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), }, } }
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 }
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 }
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) }
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 }
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
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!