Behind the Scenes: The Art of Multiplayer Game Server Development (Part - 1)
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:
Creating a snake game
Creating a network layer for communication between two clients
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
github.com/gdamore/tcell - To draw on screen - snake and food
github.com/libp2p/go-libp2p - For peer-to-peer connection
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 ๐
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.