Behind the Scenes: The Art of Multiplayer Game Server Development (Part - 1)

Dhairya VermaDhairya Verma
3 min read

In this blog post, I will guide you through the process of developing a multiplayer terminal-based snake game. This game allows two players to engage in a head-to-head battle over the same network, testing their snake-charming skills against each other.

This project involves three parts:

  1. Creating a snake game

  2. Creating a network layer for communication between two clients

  3. Using the network layer to enable both players to play the snake game

Gaming Terms

Some gaming terms that one should be familiar with.

  • Game loop - It is an infinite loop that runs every x seconds and renders objects on the screen. If a loop runs every 16.6ms then it is 60 fps;

  • Game Object - All the objects in the game like in our case snake and food(that snake eats and gets bigger).

Library

We are going to use two modules here

Code

Let's, define the structure for our game.

type Game struct {
    Screen            tcell.Screen // drawing on terminal
    snakeBody         SnakeBody
    FoodPos           Part
    Score             int
    GameOver          bool
}

type SnakeBody struct {
    Parts  []Part
    Xspeed int
    Yspeed int
}

type Part struct {
    X int
    Y int
}

Initializing the screen

    screen, err := tcell.NewScreen()

    if err != nil {
        log.Fatalf("%+v", err)
    }
    if err := screen.Init(); err != nil {
        log.Fatalf("%+v", err)
    }

    defStyle := tcell.StyleDefault.Background(tcell.ColorBlack).Foreground(tcell.ColorWhite)
    screen.SetStyle(defStyle)

    game := Game{
        Screen: screen,
    }

Game loop

game.Run() will start the game loop in a different goroutine.

go game.Run()

Let's see what our game loop looks like.

func (g *Game) Run() {
    defStyle := tcell.StyleDefault.Background(tcell.ColorBlack).Foreground(tcell.ColorWhite)
    g.Screen.SetStyle(defStyle)
    // getting terminal width and height
    width, height := g.Screen.Size()
    // drawing snake position on default
    g.snakeBody.ResetPos(width, height)
    g.UpdateFoodPos(width, height)
    g.GameOver = false
    g.Score = 0
    snakeStyle := tcell.StyleDefault.Background(tcell.ColorWhite).Foreground(tcell.ColorWhite)
    for {

        longerSnake := false

        g.Screen.Clear()
        // this function if there is a collision between snake head and food
        // if there is a collision then snake would be longer
        // It just checks if the coordinates of food and snake's head are same
        if checkCollision(g.snakeBody.Parts[len(g.snakeBody.Parts)-1:], g.FoodPos)) {
            g.UpdateFoodPos(width, height)
            longerSnake = true
            g.Score++
        }
        // this function just updates the snake location 
        // accoring to the snake direction
        // eg. if snake is moving in x direction then update its x position
        g.snakeBody.Update(width, height, longerSnake)

        // This is the new game state
        newState := GameStateUpdade{
            FoodPos: g.FoodPos,
            Parts:   g.snakeBody.Parts,
            Xspeed:  g.snakeBody.Xspeed,
            Yspeed:  g.snakeBody.Yspeed,
            Width:   width,
            Height:  height,
        }

        // it draws snake and food 
        drawParts(g.Screen, g.snakeBody.Parts, g.FoodPos, snakeStyle, defStyle)
        drawText(g.Screen, 1, 1, 8+len(strconv.Itoa(g.Score)), 1, "Score: "+strconv.Itoa(g.Score))

        // sleep before loop runs again
        // this sleep time decides fps of a game
        time.Sleep(40 * time.Millisecond)
        g.Screen.Show()
    }
    g.GameOver = true
    drawText(g.Screen, width/2-20, height/2, width/2+20, height/2, "Game Over, Score: "+strconv.Itoa(g.Score)+", Play Again? y/n")
    g.Screen.Show()
}

Snake direction

This is also an infinite loop waiting for user input and changing snake direction accordingly.

    for {
        // this call listens for any key press by user
        switch event := game.Screen.PollEvent().(type) {
        case *tcell.EventResize:
            game.Screen.Sync()
        case *tcell.EventKey:
            if event.Key() == tcell.KeyEscape || event.Key() == tcell.KeyCtrlC {
                game.Screen.Fini()
                os.Exit(0)
            } else if event.Key() == tcell.KeyUp && game.snakeBody.Yspeed == 0 {
                game.snakeBody.ChangeDir(-1, 0)
            } else if event.Key() == tcell.KeyDown && game.snakeBody.Yspeed == 0 {
                game.snakeBody.ChangeDir(1, 0)
            } else if event.Key() == tcell.KeyLeft && game.snakeBody.Xspeed == 0 {
                game.snakeBody.ChangeDir(0, -1)
            } else if event.Key() == tcell.KeyRight && game.snakeBody.Xspeed == 0 {
                game.snakeBody.ChangeDir(0, 1)
            } else if event.Rune() == 'y' && game.GameOver {
                go game.Run()
            } else if event.Rune() == 'n' && game.GameOver {
                game.Screen.Fini()
                os.Exit(0)
            }
        }
    }

These are the major parts of the snake game. You can find the full source code here - GIT

In the next part, we will see how to create the network layer.

Happy Coding ๐Ÿ

0
Subscribe to my newsletter

Read articles from Dhairya Verma directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Dhairya Verma
Dhairya Verma

Hey there ๐Ÿ‘‹ I'm Dhairya, a backend developer. With a solid background spanning 5 years, my achievement includes scaling an app to 3M users.