📜 ⬆️ ⬇️

NanoMMO on Go and Canvas [Server]


Each programmer must write his own cms , framework , mmorpg. That is what we are going to do.
Demo

Conventions

To understand the material, you must either know Go, or any other C-like language, and also imagine how to write in js.
Go introductory tour
Canvas tutorial
The main purpose of this material is to put my own thoughts in order. In no case should not be considered as described here as an example from which you can copy mindlessly.

Formulation of the problem

To begin, we define the task. It should start small, so we will write an extremely simplified client who will only be able to draw characters, receive and send data from the server. In turn, the server will be responsible for all the game logic required by the client.
')
The connection between the client and the server will be organized via web sockets, which means we can only transmit strings, and we will also have to put up with TCP slowness. For ease of implementation and debugging, we will exchange messages in json.

The first thought that occurred to me was that I would write a client first, with which I could later test the server. Actually, that's what I did. But we will act differently; further it becomes clear why.

Server

Our server will perform the following tasks:
By world we mean a list of connected characters and nothing more. No cards, no obstacles - only players. The only thing that characters will be able to do is to move at a certain speed to a given point.
Then the structure of our character will look like this:
/* point.go && character.go */ ... type Point struct { X, Y float64 } ... type Character struct { Pos, Dst Point //     Angle float64 //  Speed uint //  Name string } ... 

Let me remind you that in go, fields written with a capital letter are exported (public), and when you serialize an object in json, only exported fields are added. ( Several times stepped on this rake, not understanding why the correct code does not seem to work. It turns out the fields were written with a small letter ).

On the client, we will need to synchronize the data. In order not to write a bunch of code like character.x = data.X for all current and future fields, we will recursively walk through the data fields from the server and, if the names match, assign them to client objects. But the fields in go are capitalized. Therefore, we will accept the naming convention for js fields in the go style. It is for this reason that we started by looking at the server.

Application initialization and main loop
 /* main.go */ package main import ( "fmt" "time" ) const ( MAX_CLIENTS = 100 //      MAX_FPS = 60 //   go    // time.Second      FRAME_DURATION = time.Second / MAX_FPS ) //       var characters map[string]*Character func updateCharacters(k float64) { for _, c := range characters { c.update(k) } } func mainLoop() { //           //  . //    ,       var k float64 for { frameStart := time.Now() updateCharacters(k) duration := time.Now().Sub(frameStart) //    ,      if duration > 0 && duration < FRAME_DURATION { time.Sleep(FRAME_DURATION - duration) } ellapsed := time.Now().Sub(frameStart) //    ,        k = float64(ellapsed) / float64(time.Second) } } func main() { characters = make(map[string]*Character, MAX_CLIENTS) fmt.Println("Server started at ", time.Now()) //    go NanoHandler() mainLoop() } 

In the Character.update method, we move the character if there is where to go:
 /* point.go */ ... //        , //     func (p1 *Point) equals(p2 Point, epsilon float64) bool { if epsilon == 0 { epsilon = 1e-6 } return math.Abs(p1.X-p2.X) < epsilon && math.Abs(p1.Y-p2.Y) < epsilon } ... /* chacter.go */ ... func (c *Character) update(k float64) { //         //   ,        //       , //       if c.Pos.equals(c.Dst, float64(c.Speed)*k) { c.Pos = c.Dst return } // !        //         [],        //           lenX := c.Dst.X - c.Pos.X lenY := c.Dst.Y - c.Pos.Y c.Angle = math.Atan2(lenY, lenX) dx := math.Cos(c.Angle) * float64(c.Speed) * k dy := math.Sin(c.Angle) * float64(c.Speed) * k c.Pos.X += dx c.Pos.Y += dy } ... 

Now let's go directly to the web socket.
 /* nano.go */ package main import ( "code.google.com/p/go.net/websocket" "fmt" "io" "net/http" "strings" ) const ( MAX_CMD_SIZE = 1024 MAX_OP_LEN = 64 CMD_DELIMITER = "|" ) //  —    ip:port var connections map[string]*websocket.Conn //       json    type packet struct { Characters *map[string]*Character Error string } //      func NanoHandler() { connections = make(map[string]*websocket.Conn, MAX_CLIENTS) fmt.Println("Nano handler started") //  ws://hostname:48888/    NanoServer http.Handle("/", websocket.Handler(NanoServer)) //  48888      err := http.ListenAndServe(":48888", nil) if err != nil { panic("ListenAndServe: " + err.Error()) } } //   func NanoServer(ws *websocket.Conn) { //   MAX_CLIENTS,    ,      if len(connections) >= MAX_CLIENTS { fmt.Println("Cannot handle more requests") return } //  , , 127.0.0.1:52655 addr := ws.Request().RemoteAddr //    connections[addr] = ws //  ,      character := NewCharacter() fmt.Printf("Client %s connected [Total clients connected: %d]\n", addr, len(connections)) cmd := make([]byte, MAX_CMD_SIZE) for { //   n, err := ws.Read(cmd) //  if err == io.EOF { fmt.Printf("Client %s (%s) disconnected\n", character.Name, addr) //    delete(characters, character.Name) delete(connections, addr) //     ,    go notifyClients() //      break } //  ,     if err != nil { fmt.Println(err) continue } fmt.Printf("Received %d bytes from %s (%s): %s\n", n, character.Name, addr, cmd[:n]) //    : operation-name|{"param": "value", ...} //    opIndex := strings.Index(string(cmd[:MAX_OP_LEN]), CMD_DELIMITER) if opIndex < 0 { fmt.Println("Malformed command") continue } op := string(cmd[:opIndex]) //      json  //   ,       n  //   — ,     , //    json data := cmd[opIndex+len(CMD_DELIMITER) : n] //        switch op { case "login": var name string //     websocket.JSON.Unmarshal(data, ws.PayloadType, &name) //     if _, ok := characters[name]; !ok && len(name) > 0 { //  character.Name = name characters[name] = &character fmt.Println(name, " logged in") } else { //    fmt.Println("Login failure: ", character.Name) go sendError(ws, "Cannot login. Try another name") continue } case "set-dst": var p Point //  -      if err := websocket.JSON.Unmarshal(data, ws.PayloadType, &p); err != nil { fmt.Println("Unmarshal error: ", err) } //    //   ,  Character.update    character.Dst = p default: // fmt.Printf("Unknown op: %s\n", op) continue } //     //           go notifyClients() } } //    func sendError(ws *websocket.Conn, error string) { // ,       packet := packet{Error: error} //   json msg, _, err := websocket.JSON.Marshal(packet) if err != nil { fmt.Println(err) return } //   if _, err := ws.Write(msg); err != nil { fmt.Println(err) } } //    func notifyClients() { //       packet := packet{Characters: &characters} //   json msg, _, err := websocket.JSON.Marshal(packet) if err != nil { fmt.Println(err) return } //      for _, ws := range connections { if _, err := ws.Write(msg); err != nil { fmt.Println(err) return } } } 

Creating a character, we must set him some parameters. In go, it is customary to do this in the form function NewTypename
 /* character.go */ ... const ( CHAR_DEFAULT_SPEED = 100 ) ... func NewCharacter() Character { c := Character{Speed: CHAR_DEFAULT_SPEED} c.Pos = Point{100, 100} c.Dst = c.Pos return c } 

That's our whole server.
The article about the client part will be written after collecting feedback on this text.

Links

Demo
Card generator (picture on the background)
Sources

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


All Articles