Build Awesome 2D Game Using Golang

Apparently, Golang game development is becoming a trend now. Although mainly used for back-end development and systems programming, developers are taking the help of amazing libraries, frameworks and gaming engines to develop 2D and 3D games. In this blog, we will focus on building a simple 2D game. 

 

The Prerequisites 

In order to develop your 2D, you would need two things. These are:

  • A Go compiler 
  • An IDE like VS Code 

 

Create the Directory and go.mod File 

  • In order to create a new directory, we have to open Command Prompt in Windows and terminal in Linux or MAC. In this tutorial, I’ll be using Windows and I will save my project in go-workspace. So, here is a snapshot of the code I typed:
  • As you can see, we have named the directory snkgm. As we type code ., the IDE opens up (in this case, it is VS Code). 
  • In the VS Code, we go to the terminal to create the go.mod file. We type:

go mod init github.com/golangcompany/go-snkgm 


Upon opening the go.mod file, we get to see the current version of Golang and the name of the module. 

 

Installing Dependency 

The next step involves the installation of the dependency required for Golang game development. In this case we will install termbox.go. 


For this, we have to type in the terminal,, 


go get github.com/nsf/termbox-go


This will result in the updation of the go.mod file and we get to see the creation of the go.sum file. 

 

Creating a main.go File 

Next, we will be creating a main.go file. And inside the ‘snkgm’ directory, we will create another folder. Let’s name this folder ‘snakefood’. And inside this folder, we will have another source file named ‘logic.go’ where we showcase the logic of the game. 

 

The Code in logic.go File

This time, we will type the code in the ‘game.go’ file.

package snakefood

type Game struct {

ground [][]int 

posX int

posY int

groundHeight int

groundWidth int 

}

  • As you can see, we have created a struct named Game, where we have five different data fields, all of type int. Only the ground is a 2-D array. If you wish to know more about Structs, then click What Are Structs in Golang? Moving on,

func GameInitiate() *Game {

m :=new(Game)

m.groundHeight =14

m.groundWidth =14

return m

}

  • We have to import the function GameInitiate in the main.go file. Thus, we used capital G to write the function name. The return type is *Game, which will return the pointer to the game object. 

func (m *Game) resetGame() {

m.ground = make ([][]int, m.groundHeight)

 

for i := 0; i<m.groundHeight; i++ {

    m.board [i]= make ([]int, m.groundWidth)

    for j :=0; j<m.groundWidth; j++ {

         m.board [i][j]= 0

}

}

mground [0][0]= 1

m.posX=0

m.posY=0

}

  • Now, we have to comprehend a lot of code. Here, we write another function resetGame(). Golang has predefined methods. In order to make the code readable, we will be making this a method. 
  • In this block of code, we will create the ground. Apart from having m.groundHeight(), it will have the type 2D array. 
  • Next, we insert two for loops, in order to create the Height and Breadth of the ground. As you can see, in the for loop, we create an array with the statement m.board [i]= make ([]int, m.groundWidth) and we initialize it to 0. 
  •  

mground [0][0]= 1

m.posX=0

m.posY=0

This states that all the positions in the X and Y axis will be 0. Only the mground [0][0]= 1 as it represents the player and the color needs to be changed. 

 

The Code in the main.go File 

In this part, 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() 

    • Let's now comprehend what we did. Here, the termbox will be set up. We also look for errors in this section. We incorporate panic in the event that the termbox initialization fails (err). The 'defer' keyword will cause termbox to run. When the main function is finished, call Close(). It will essentially shut the termbox.

  • In the following section, we will create channels. 

Hopefully, you were able to understand the concepts well. If you face any issues in understanding the process or methods, then go through the steps once more. You will surely get a hang of it. And most importantly, you should try this on your machine and check the output for yourself. 

eventStream := make (chan termbox.Event)

go func() {

for{

eventStream <- termbox.PollEvent() 

}

}()

m := snakefood.GameInitiate()


  • So, let us understand what we are doing here. With the creation of the variable eventStream, we create a channel make (chan termbox.Event).
  • Next, we move on to go func(), with the help of which we create goroutines.  
  • Following this, we will run an infinite loop. You won’t get to see this on the forefront as it will run in the background. When you press the keyboard, the input will be transferred to the eventStream. Thus, you are feeding live data into eventStream. Moving on to the next part, we describe m := snakefood.GameInitiate(). 

for {

   select {

    case tx := <-eventStream:

           if tx.Type == termbox.EventKey {

              switch {

               case tx.Key == termbox.KeyArrowDown:

               case tx.Key == termbox.KeyArrowLeft:

               case tx.Key == termbox.KeyArrowRight:

               case tx.Key == termbox.KeyArrowUp:

               case tx.Ch == ‘q’ || tx.Key == termbox.KeyEsc ||txt.Key == termbox.KeyCtrlC:

               return 

            }

            default:

        }

    }

  • In this situation, we must write a case. It will transmit any events that are present in the eventStream to case tx. Therefore, we must determine if the event that occurred matches the termbox.EventKey. The switch case statement must then be written in case any key on the keyboard is really pushed. Thus, we provide four distinct situations. KeyArrowUp, KeyArrowDown, KeyArrowLeft, and KeyArrowRight. The last scenario depicts tx.Ch == ‘q’|| tx.Key == termbox.KeyctrlC: will go on to stop the game. And we end with a default case. 

So far Golang game development looks good, right? Let us proceed further. 



Code in the execution.go File 

Here, we will code in the execution.go file. 

package snakefood

import github.com/nsf/termbox.go

const groundColor= termbox.ColorRed

const backgroundColor= termbox.ColorBlue

const setinsColor= termbox.ColorBlack 

 

const defaultMarginWidth= 2

const defaultMarginHeight= 1

const titleCommX= defaultMarginWidth 

const titleCommY= defaultMarginHeight

const titleHeight= 1

const titleCompleteY= titleCommY+ titleHeight 

const groundStartX= defaultMarginWidth

const groundStartY= titleCompleteY+ defaultMarginHeight 

const cellBreadth= 2

const title= “GOLANG SNAKE GAME” 

  • We are focussed on the basic styling in this section. We have described the color for the ground, instruction set and background. Next, we have highlighted the MarginWidth, MarginHeight. In addition to this, we have mentioned the starting and finishing locations of the game, etc. 
  • The cellBreadth, the smallest component in the game, is the most crucial factor here. This is because the cells will be used to form the whole game. Next, we proceed to the function Execute().

 

func (m *Game) Execute() {

termbox.Clear(groundColor, groundColor)

termboxPrint(titleCommX, titleCommY, setinsColor, backgroundColor, title)

for i := 0; i< m.groundHeight; i++ {

   for j := 0; j< m.groundWidth; j++ {

                 var cellColor termbox.Attribute 

                switch m.ground [i][j] {

case 0: 

cellColor = termbox.ColorCyan 

case 1:

cellColor = termbox.ColorBlack 

}

 

for l :=0; l<cellBreadth; l++ {

 

    termbox.SetCell(groundStartX+cellBreadth*j+l, groundStartY+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++

}

}

  • This is a huge chunk of code that we have to understand. In this section, we create a function Execute(), which we have to import. After this, we clear the terminal with termbox.Clear and the default color will be groundColor. 
  • Following this, we write the function termboxPrint. The necessary message must be printed using this function. This is more difficult than printing fmt.Println since we need to take into account a number of factors, such as height, breadth, location, etc.
  • The title must be printed, together with information about where it should be printed, in termboxPrint. We also mention the backgroundColor and setinsColor. Next, a for loop is created to reach the groundHeight. Additionally, we create a second for loop that continues until groundWidth.
  • Next, we have to create a cell color object and a game object. In this game, everything is represented in the form of a color. That is why we define var cellColor as termbox.Attribute. 
  • Following this, we write a switch case. In the switch instance that follows, the condition will be m.ground and its current location [i][j]. We must determine if the case is 0. And we'll change its hue to termbox.ColorCyan. We must set the color to termbox.ColorBlack if it is a player. 
  • In the next section, we write 

l=0; l<cellBreadth; l++{

termbox.SetCell(groundStartX+cellBreadth*j+l, groundStartY+i, ‘ ‘, cellColor, cellColor)

} In this case, we have assigned cellBreadth to 2. 

Thus, the loop will continue for two times and will create two cells. 

We also have to keep in mind the termbox.Flush(). 

 

Time to Go Back to main.go File 

Do you recollect this section? 

m := snakefood.GameInitiate()

m.Execute ()

for {

   select {

    case tx := <-eventStream:

           if tx.Type == termbox.EventKey {

              switch {

               case tx.Key == termbox.KeyArrowDown:

               case tx.Key == termbox.KeyArrowLeft:

               case tx.Key == termbox.KeyArrowRight:

               case tx.Key == termbox.KeyArrowUp:

               case tx.Ch == ‘q’ || tx.Key == termbox.KeyEsc ||txt.Key == termbox.KeyCtrlC:

               return 

            }

            default:

            m.Execute()

        }

    }

 

As you can see, we have inserted the m.Execute() function. 

 

Time to Go Back to logic.go File 

  • Can you remember the following section?

func GameInitiate() *Game {

m :=new(Game)

m.groundHeight =14

m.groundWidth =14

m.resetGame()

return m

}

We just inserted the m.resetGame() function. 

 

  • We move onto another section, 

 

for i := 0; i<m.groundHeight; i++ {

    m.board [i]= make ([]int, m.groundWidth)

    for j :=0; j<m.groundWidth; j++ {

         m.board [i][j]= 0

}

}

mground [0][0]= 1

m.posX=0

m.posY=0

}

 

We had deliberately left some space for codes to enter. 

func (m *Game) ShiftUp() {

m.ground[m.posX][m.posY] = 0

g.posX--

m.ground [m.posX][m.posY]= 1

}

 

func (m *Game) ShiftRight () {

m.ground [m.posX][m.posY] =0 

m.posY++

m.ground[m.posX][m.posY]=1

}

 

func (m *Game) ShiftDown () {

m.ground [m.posX][m.posY] =0

m.posX++

m.board [m.posX][m.posY] =1

}

 

func (m *Game) ShiftLeft() {

m.board [m.posX][m.posY] =0 

m.posY–

m.board [m.posX][m.posY] =1

}

  • So what's going on here? ShiftUp() will just set the current position to 0 as soon as we press it. The X position will then go downward after that. The player's current location will then be updated.
  • The other functions have been coded accordingly. Now, we need to verify whether the player is crossing the boundary. In such a case, the player will receive an error ‘outside the range’. Thus, we have to write another function. 

 

func (m *Game) CheckPlace() bool {

switch m.Direction {

 

case “top”:

if m.posX == 0 || m.ground [m.posX-1][m.posY] == 1 {

return false 

}

return true

 

Case “right”:

if m.posY == m.groundHeight-1 || m.ground[m.posX][m.posY+1] ==1 {

return false

}

return true

 

case “below”:

if m.posX == m.groundWidth-1 || m.ground[m.posX+1][m.posY] == 1{

return false 

}

return true

 

case “left”

if m.posY == 0 || m.ground [m.posX][m.posY-1] ==1 {

return false 

}

return true 

 

default: 

panic (“TRY AGAIN!! DIRECTION IS WRONG”)

}

  • As you can see, we have added the CheckPlace(). And inside it, we have included several switch cases with different conditions. As we have also incorporated m.Direction, we have to insert it as a data field in the Game struct in logic.go. So far how are you enjoying Golang game development

type Game struct {

ground [][]int 

posX int

posY int

groundHeight int

groundWidth int 

Direction string 

}



Time to Go Back to main.go File 

Do you remember this section?  

 

for {

   select {

    case tx := <-eventStream:

           if tx.Type == termbox.EventKey {

              switch {

               case tx.Key == termbox.KeyArrowDown:

               case tx.Key == termbox.KeyArrowLeft:

               case tx.Key == termbox.KeyArrowRight:

               case tx.Key == termbox.KeyArrowUp:

               case tx.Ch == ‘q’ || tx.Key == termbox.KeyEsc ||txt.Key == termbox.KeyCtrlC:

               return 

            }

            default:

            m.Execute()

        }

    }

 

Now, we will be inserting certain parameters inside this block. 

 

for {

   select {

    case tx := <-eventStream:

           if tx.Type == termbox.EventKey {

              switch {

               case tx.Key == termbox.KeyArrowDown:

 m.Direction= “down”

if m.CheckPlace() == 1 {

                m.ShiftDown()

}

               case tx.Key == termbox.KeyArrowLeft:

m.Direction= “left”

if m.CheckPlace() == 1 {

                m.ShiftLeft()

}

               case tx.Key == termbox.KeyArrowRight:

                m.Direction= “right”

if m.CheckPlace() == 1{

                m.ShiftRight()

}

               case tx.Key == termbox.KeyArrowUp:

m.Direction= “up”

if m.CheckPlace() ==1 {

                m.ShiftUp()

}

               case tx.Ch == ‘q’ || tx.Key == termbox.KeyEsc ||txt.Key == termbox.KeyCtrlC:

               return 

            }

            default:

            m.Execute()

        }

    }

  • So you can see that we have set the m.Direction. Now, only if CheckPlace()==1, we can call the function m.ShiftLeft(), m.ShiftRight(), m.ShiftUp(), and m.ShiftDown(). 

Hopefully, you were able to understand the logic behind the creation of the 2D game. If you carefully follow the steps, you will be able to work on intricate projects sooner than you think.

Tags

Post a Comment

0 Comments
* Please Don't Spam Here. All the Comments are Reviewed by Admin.