πŸ“œ ⬆️ ⬇️

As I UDP game server on Golang wrote

Introduction


Recently, I began to learn the Go programming language and decided to try writing a client - server application on it for practice. Since I love computer games and in particular the good old "Snake" in which I played on an old black and white phone, I decided to make it personal. Yes, you heard right, it will be about the game "Snake" which is divided into server and client parts communicating with each other using the UPD protocol.

β†’ Code
β†’ Download and play - collected only for amd64 and for Windows, Mac, Linux


Project structure


I was asked to tell what the names of the folders in the project mean, so I decided to write about it here.
  1. al - Application Layer: Here I put the code that will be called by the user of our package. This is the package interface. That for which the package is generally made.
  2. bll - Bisnes Logic Layer: If al needs something to count, then he turns here. Here is the main logic of our package.
  3. dal - Data Asses Layer. If bll needs to get data from somewhere or put it somewhere, then it is sent here. These are our abstractions over input devices output, DB, etc.
  4. I also usually add the Infrastructure folder: A wrapper is stored here over the standard logger, Mapper, classes for initializing and configuring the DI container, etc. ancillary things not directly related to the logic of the package itself.

')

ToDo


  1. You can make pushing the game to the client and not constantly requesting it.
  2. You can send only the state of food and snakes to the client as the state of the walls remains unchanged.
  3. It would be possible to make a collision check with the walls by checking the extreme values ​​and not by bluntly searching all the values.
  4. Make a collision check with the tail in one cycle with the movement of the snake.
  5. Add mutexes to the code responsible for writing the game data to the matrix and the code verifying the clients currently playing.
  6. And in general, check the live clients for the moment into a separate thread and it would immediately eliminate this bug: If we have only one client and it suddenly disconnects, the server will not report this and will not kill the instance of this client.
  7. To fix a binding of constants for keys. Now their name does not match the actions they cause.
  8. Make a packet loss calculation for commands based on their ID


Game screenshots and pprof result


Caution! Many pictures

pprof cpu



Server screenshot





Work plan for client


1) Processing user input.
1.1) Read key pressed
1.2) Create a command corresponding to the key.
1.3) If the command was not recognized in the previous step, then skip the underlying steps.
1.4) Send a command to the server 4 times in a row with the same identifier with the hope that at least 1 packet from 4 will reach.
2) The display cycle.
2.1) Request the current state of the game from the server.
2.2) Get the current game status from the server.
2.3) Display the current state of the game.
2.4) To sustain a timeout.

Server plan


1) Handling the connection of a new client.
1.1) Create a new gaming session for a new client.
1.2) Type in the ip address of the new client.
1.3) Add a client to the server's client list.
2) Processing the command sent by the client
2.1) If the command with the given identifier has already been processed then do nothing.
2.2) If a new team has arrived, depending on it, change the state of the game.
2.2.1) If there was a move up command, then turn the snake up
2.2.2) If there was a command to move down then turn the snake down
2.2.3) If there was a move command to the right, then turn the snake to the right.
2.2.4) If there was a movement command to the left, then turn the snake to the left.
2.2.5) If the command was not processed in the previous steps, then do nothing.
3) Processing a game state request
3.1) Select the appropriate session of the game by ip address of the client.
3.2) Read the current state of the game session of the client.
3.3) Send the current game status to the client.
4) Game cycle.
4.1) Check the snake's collision with food
4.1.1) If the snake's head is faced with food.
4.1.1.1) Squel the length of a snake on one square
4.1.1.2) To generate food in a new place.
4.1.2) otherwise do nothing.
4.2) Check the snake's collision with the wall
4.2.1) If the snake collides with the wall to transfer the game to the initial state
4.2.2) If the snake does not collide with the wall then do nothing
4.3) Check the snake's head collision with its tail.
4.3.1) If the snake collides with the tail, then transfer the game to the initial state.
4.3.2) If the collision was not then do nothing.
4.3) Move the snake.
5) Handle client disconnection.
5.1) If the client did not send any requests for more than 5 seconds, then
5.1.1) Stop the gaming session of the client.
5.1.2) Print the address of the disconnected client to the console.
5.1.3) Remove the client's address from the list of current clients the server is working with.

Client code


It's all as simple as possible. We have a structure responsible for communication via the UPD protocol.

package dal import ( "fmt" "net" ) type IUdpClient interface { Read(p []byte) (int, error) Write(p []byte) (int, error) Close() error } type udpClient struct { connection *net.UDPConn localAddress *net.UDPAddr remouteAddress *net.UDPAddr timeOut uint } func NewUdpClient(clientIp, serverIp string, timeOut uint) (IUdpClient, error) { client, e := net.ResolveUDPAddr("udp4", clientIp) if e != nil { return nil, e } server, e := net.ResolveUDPAddr("udp4", serverIp) if e != nil { return nil, e } conn, err := net.ListenUDP("udp", client) if err != nil { return nil, err } return udpClient{conn, client, server, timeOut}, nil } func (c udpClient) Read(p []byte) (int, error) { //c.connection.SetReadDeadline(time.Now().Add(time.Second * time.Duration(c.timeOut))) i, a, e := c.connection.ReadFromUDP(p) if e != nil { return 0, e } if a.String() != c.remouteAddress.String() { return 0, fmt.Errorf("Unnown addres %v", a) } return i, nil } func (c udpClient) Write(p []byte) (int, error) { return c.connection.WriteToUDP(p, c.remouteAddress) } func (c udpClient) Close() error { return c.connection.Close() } 

In one routine, we read the keys pressed by the player and send commands to the server
 go func() { var id uint64 = 1 for { key := screen.ReadKey() code := parseKeyCode(key) if code > 0 { id++ for i := 0; i < 4; i++ { e := sendCommandToServer(Command{id, code}) if e != nil { logger.Println(e.Error()) } } } } }() 

In another routine, we read the status that came from the server and display it to the client.
 go func() { for { s, e := requestStateFromServer() if e != nil { l.Println(e.Error()) continue } showState(s) } }() 


Server code


Here, too, everything is extremely simple.

We read the incoming data and send the result.

 func (this server) listen() { in := make(chan inValue, 100) out := make(chan outValue, 100) defer func() { this.listener.Close() this.dispatcher.Close() close(in) close(out) }() for i := 0; i < COUNT_THREADS_IN_POOL; i++ { go func() { for input := range in { bytes, err := this.dispatcher .Dispatch(input.data[:input.count], fmt.Sprintf("%v", input.remoteaddr)) this.pool.Put(input.data) if err != nil { fmt.Printf("Error on dispathing %v\n", err) continue } if len(bytes) < 1 { fmt.Print("Empty result\n") continue } out <- outValue{bytes,input.remoteaddr} } }() } go func() { for result := range out { _, err := this.listener.Write(result.data, result.remoteaddr) if err != nil { fmt.Printf("Couldn't send response - %v \n", err) } } }() for { data := this.pool.Get().([]byte) count, remoteaddr, err := this.listener.Read(data) if err != nil { fmt.Printf("Error on reading from listener %v\n", err) continue } in <- inValue{count:count,remoteaddr:remoteaddr, data:data} } } 

Our dispatcher simply selects the right customers for his or her pi.

 func (this *dispatcher) Dispatch(data []byte, clientId string) ([]byte, error) { this.checkAliveClients() c, ok := this.clients[clientId] if !ok { fmt.Printf("Connected new client %s\n", clientId) this.clients[clientId] = this.factory.CreateClient() c = this.clients[clientId] } c.UpdateLastActiveTime() return c.Accept(data) } 

If the team came, then we change the direction of the snake.

 func (game *game) Logic(timeDeltaInNanoSeconds int64) { game.timeBuffer += timeDeltaInNanoSeconds select { case command := <-game.commandChannel: switch command { case Up: game.snake.Go(bll.UpDirection) case Down: game.snake.Go(bll.DownDirection) case Left: game.snake.Go(bll.LeftDirection) case Right: game.snake.Go(bll.RightDirection) } default: } if game.timeBuffer >= timeDeltaInNanoSecondsAfterThatSnakeMoves { game.snake.Move() game.snake.TryEat(game.food) if game.snake.IsHit(game.frame) || game.snake.IsHitTail() { game.snake.Reset() } game.timeBuffer -= timeDeltaInNanoSecondsAfterThatSnakeMoves } } 

If the game state request has arrived, we are sending a new state.

 func (game *game) Draw() [][]rune { game.screen.Clear() game.frame.Draw() game.food.Draw() game.snake.Draw() return game.screen.Data() } 

findings


When add generics and imputed enum to Go, then somewhere in a year after that I will look at
this yap again.

Source: https://habr.com/ru/post/417815/


All Articles