Are you excited about learning programming to develop a simple 2D game?
Great! This blog will provide you with enough information so that you can get started. Each and every statement has been explained properly so that you face no issues understanding the logic of the code.
So, let us begin our journey of Go game development.
➔ Create a New Directory
Let’s start from scratch. We will create a new directory inside go-workspace called ‘gaming’. As we type code ., the VS Code opens up.

➔ Create the go.mod file
Now, in the VS Code, we will create the go.mod file. For this, we write in the Terminal:
go mod init github.com/sohammukherjee/go-gaming
As we open the go.mod file, we get to see the name of the module and the version of Golang that we are using.
➔ Install the Dependency
Next, we will install the dependency which is required forgame development with Golang. We will be installing termbox.go.
(Side Note: The library Termbox offers a simple API that enables programmers to create user interfaces which are text-based. The cross-platform library contains console-based Winapi implementations for Windows operating systems in addition to terminal-based versions for *nix operating systems. The termbox is an exceptional library in its field because of its small API, which makes it simple to develop, test, maintain, and learn.)
Now, we type in the Terminal,
go get github.com/nsf/termbox-go
With this, the go.mod file will be updated. And you will get to see the formation of the go.sum file which is kind of like a package log file.
➔ Create a main.go File
Next, we create a file named main.go. Following this, we will create another folder ‘videogame’. And it will have a file named ‘game.go’. In this ‘game.go’ file, we will write down all the game logic.
➔ Start Coding in game.go File
Now, we will type the code in the ‘game.go’ file.
package videogame
type Game struct {
board [][]int
locX int
locY int
boardLength int
boardBreadth int
}
- We have created a struct with five different fields. If you wish to know more about structs, visit What Are Structs in Golang? The board is a 2D array, and all the fields are of type int.
- Now, you will need to write the methods in order to run the game properly. We start with GameStart()
func GameStart() *Game {
m :=new(Game)
m.boardLength =14
m.boardBreadth =14
return m
}
- We will import the function GameStart() in the main.go file; so we write it with capital G. We also provide the return type *Game, to return the pointer to the game object. To keep things, we assign both the m.boardLength and m.boardBreadth to 14. Finally, it will return the game m.
func (m *Game) reinitializeGame() {
m.board = make ([][]int, m.boardLength)
for i := 0; i<m.boardLength; i++ {
m.board [i]= make ([]int, m.boardBreadth)
for j :=0; j<m.boardBreadth; j++ {
m.board [i][j]= 0
}
}
m.board [0][0]= 1
m.locX=0
m.locY=0
}
- So, a lot to understand in this chunk of code. We won’t be importing reinitializeGame(), so we have written it in small. Golang is not an OOPs language, but it has predefined methods. So, we will make this a method to help with the readability.
- We will also create the board here. It will have the type 2D array, and it will have m.boardLength.
- Following this, we insert two for loops, to create Length and Breadth. In the for loop, we create an array with the line m.board[i]= make([]int, m.boardBreadth), and we initialize the whole value to 0.
- As per logic, all the positions will be 0 except for the 0th position, i.e, m.board[0][0] which will have value 1. This is due to the fact that on the board if the value is 1, we need to represent as a player and change the color. And we also store the location of the player or head, as m.locX=0 and m.locY=0.
➔ Start Coding in the main.go File
Now, we will type the code in the main.go file.
package main
import “github.com/nsf/termbox-go”
func main() {
err := termbox.Init()
if err !=nil {
panic(err)
}
defer termbox.Close()
- Now, let us understand what we did here. We will initialize the termbox. Here, we also check for errors. If there is any error in initializing the termbox, we include panic(err). Now the ‘defer’ keyword will execute termbox.Close() when the main function gets over. In simple words, it will close the termbox.
- Now, we will create channels, which is an important aspect of Golang.
eventStream := make (chan termbox.Event)
go func() {
for {
eventStream <- termbox.PollEvent()
}
} ()
m := videogame.GameStart()
- So, what do we have here? We have created a variable eventStream and we have made a channel with ‘make (chan termbox.Event). The termbox.Event is extremely helpful if you wish to detect any input from the keyboard. Thus, whenever the keyboard is pressed, it will store the value in eventStream.
- With go func(), we create goroutines, which will create a thread and run the function func() inside another thread. Simply put, a thread is a block of code that runs in the background.
- Next, we will run a for loop which will be infinite. This will be run continuously in the background and whenever the keyboard is pressed, it will send the input directly to eventStream. Thus, eventStream will always get the live data. Following this, we will define the game m := videogame.GameStart()
for {
select {
case st := <-eventStream:
if st.Type == termbox.EventKey {
switch {
case st.Key == termbox.KeyArrowDown:
case st.Key == termbox.KeyArrowLeft:
case st.Key == termbox.KeyArrowRight:
case st.Key == termbox.KeyArrowUp:
case st.Ch == ‘q’ || st.Key == termbox.KeyEsc || st.Key == termbox.KeyCtrlC:
return
}
default:
}
}
}
- Here, we have to write a case. Whenever there is an event in the eventStream, it will send it to case st. So, we need to check whether the event that took place is equal to the termbox.EventKey. Next, we need to write a switch case statement in case any key is really pressed on the keyboard. So, we write four different cases KeyArrowDown, KeyArrowLeft, KeyArrowRight, KeyArrowUp. The last condition case st.Ch == ‘q’ || st.Key == termbox.KeyEsc || st.Key == termbox.KeyCtrlC: will stop the game. Finally, we write a default case.
Are you still with us in this Golang game development? Great! Read on to accomplish the project.
➔ Start Coding in render.go File
Now, we start coding in the render.go file.
package videogame
import github.com/nsf/termbox.go
const boardColor= termbox.ColorRed
const backgroundColor= termbox.ColorBlue
const instructionsColor= termbox.ColorBlack
const defaultMarginBreadth= 2
const defaultMarginLength= 1
const titleBeginX= defaultMarginBreadth
const titleBeginY= defaultMarginLength
const titleLength= 1
const titleFinishY= titleBeginY+ titleLength
const boardStartX= defaultMarginBreadth
const boardStartY= titleFinishY+ defaultMarginLength
const cellWidth= 2
const title= “GAME OF SNAKE BY SOHAM”
- Here, in this section, we are creating basic styling. As you can see, we have set the board color, background color, instructions color. Furthermore, we have mentioned the Margin breadth and length. We have also set the location and positions for starting and finishing the game, the starting location of the board, etc.
- The most important aspect here is the cellWidth, which is the smallest piece of the game. This is because the whole game will be created using cells. Following this, we will move on to the function render().
func (m *Game) Render() {
termbox.Clear(backgroundColor, backgroundColor)
termboxPrint(titleBeginX, titleBeginY, instructionsColor, backgroundColor, title)
for i := 0; i< m.boardLength; i++ {
for j := 0; j< m.boardBreadth; j++ {
var cellColor termbox.Attribute
switch m.board [i][j] {
case 0:
cellColor = termbox.ColorCyan
case 1:
cellColor = termbox.ColorBlack
}
for l :=0; l<cellWidth; l++ {
termbox.SetCell(boardStartX+cellWidth*j+l, boardStartY+i, ‘ ‘, cellColor, cellColor)
}
}
termbox.Flush()
}
func termboxPrint (x, y int, fg, bg termbox.Attribute, msg string) {
for _, c :=range msg {
termbox.SetCell(x, y, c, fg, bg)
x++
}
}
- Ok, so we have a lot to cover in this section. Here, we create a function Render() that we have to import. Following this, we need to clear the terminal. So, we write termbox.Clear() and the default color will be backgroundColor
- Then we write the function termboxPrint(). This function is responsible for printing the required message. This is not as easy as printing fmt.Println, as we have to consider various parameters like length, breadth, location, etc.
- In termboxPrint, we have to print the title, and describe where we wish to print the title. Along with this, we mention the instructionsColor and backgroundColor. Next, we write a for loop to run to the boardLength. And we write another for loop so that it runs till boardBreadth.
- Next, we need to create the game object and cell color object. This is extremely crucial as everything will be represented in terms of color. So, we will define var cellColor as termbox.Attribute.
- Following this, we need to write a switch case, and its condition will be m.board and its current location [i][j]. We need to check if the case is 0. And we will set its color to termbox.ColorCyan. If it’s a player, then we need to set the color to termbox.ColorBlack.
- In this section,
for l :=0; l<cellWidth; l++ {
termbox.SetCell(boardStartX+cellWidth*j+l, boardStartY+i, ‘ ‘, cellColor, cellColor)
While we were busy with the styling, we assigned cellWidth to 2
So, this loop will run for two times and will set two cells.
Also, the termbox.Flush() is very important.
With this, we complete the Render() function.
➔ Go Back to main.go File
Remember this section?
m := videogame.GameStart()
m.Render()
for {
select {
case st := <-eventStream:
We insert the line m.Render().
Similarly, in this section
case st.Key == termbox.KeyArrowUp:
case st.Ch == ‘q’ || st.Key == termbox.KeyEsc || st.Key == termbox.KeyCtrlC:
return
}
default:
m.Render()
}
}
}
We insert m.Render in default.
➔ Go Back to game.go File
- Remember this section? Well, we have to insert one more line in this code.
func GameStart() *Game {
m :=new(Game)
m.boardLength =14
m.boardBreadth =14
m.reinitializeGame()
return m
}
We inserted the m.reinitializeGame() function.
- Next, we move on to another section
for i := 0; i<m.boardLength; i++ {
m.board [i]= make ([]int, m.boardBreadth)
for j :=0; j<m.boardBreadth; j++ {
m.board [i][j]= 0
}
}
m.board [0][0]= 1
m.locX=0
m.locY=0
}
Here, we have to code further:
func (m *Game) ShiftUp() {
m.board[m.locX][m.locY] = 0
g.locX–
m.board [m.locX][m.locY]= 1
}
func (m *Game) ShiftRight () {
m.board [m.locX][m.locY] =0
m.locY++
m.board[m.locX][m.locY]=1
}
func (m *Game) ShiftDown () {
m.board [m.locX][m.locY] =0
m.locX++
m.board [m.locX][m.locY] =1
}
func (m *Game) ShiftLeft() {
m.board [m.locX][m.locY] =0
m.locY–
m.board [m.locX][m.locY] =1
}
- So, what is happening here? As we press ShiftUp(), then it will just set the current location as 0. Following this, there will be a decrease in the X position. And then it will update the current location as a player.
- Similarly, all the other functions have been written accordingly. Next, we need to check whether the user is crossing the boundary of the game. In that case, it will showcase an error like ‘outside the range’. For this, we have to insert another function:
func (m *Game) CheckShift() bool {
switch m.Direction {
case “top”:
if m.locX == 0 || m.board [m.locX-1][m.locY] == 1 {
return false
}
return true
Case “right”:
if m.locY == m.boardLength-1 || m.board[m.locX][m.locY+1] ==1 {
return false
}
return true
case “below”:
if m.locX == m.boardBreadth-1 || m.board[m.locX+1][m.locY] == 1{
return false
}
return true
case “left”
if m.locY == 0 || m.board [m.locX][m.locY-1] ==1 {
return false
}
return true
default:
panic (“TRY AGAIN!! WRONG DIRECTION”)
}
}
- So, we have added the CheckShift(). Since, we have also included the m.Direction, we have to insert the Direction as a field in the struct we created right at the start.
package videogame
type Game struct {
board [][]int
locX int
locY int
boardLength int
boardBreadth int
Direction string
}
- Now, in the CheckShift(), there are several switch cases. If the direction is top, and the location is 0, you cannot press the up button. Otherwise, it will crash into the boundary. Similarly, we have created conditions for all the other directions as well.
➔ Go Back to the main.go File
Recollect this section?
for {
select {
case st := <-eventStream:
if st.Type == termbox.EventKey {
switch {
case st.Key == termbox.KeyArrowDown:
case st.Key == termbox.KeyArrowLeft:
case st.Key == termbox.KeyArrowRight:
case st.Key == termbox.KeyArrowUp:
case st.Ch == ‘q’ || st.Key == termbox.KeyEsc || st.Key == termbox.KeyCtrlC:
return
}
default:
}
}
}
Now, we have to insert certain parameters.
for {
select {
case st := <-eventStream:
if st.Type == termbox.EventKey {
switch {
case st.Key == termbox.KeyArrowDown:
m.Direction= “down”
if m.CheckShift() == 1 {
m.ShiftDown()
}
case st.Key == termbox.KeyArrowLeft:
m.Direction= “left”
if m.CheckShift() == 1 {
m.ShiftLeft()
}
case st.Key == termbox.KeyArrowRight:
m.Direction= “right”
if m.CheckShift() == 1{
m.ShiftRight()
}
case st.Key == termbox.KeyArrowUp:
m.Direction= “up”
if m.CheckShift() ==1 {
m.ShiftUp()
}
case st.Ch == ‘q’ || st.Key == termbox.KeyEsc || st.Key == termbox.KeyCtrlC:
return
}
default:
}
}
}
- So, you can see that we have updated m.Direction= “down”. Now, only if m.CheckShift() == 1, then only we can call the function m.ShiftDown(). Similarly, we have updated other directions as well.
Hopefully, you were able to gain an insight into the programming aspect of the 2D game development. If you follow the steps carefully, you will face no issues. Happy coding!