各プログラマは、独自の
デモ
コンベンション
素材を理解するには、Goまたは他のC言語に似た言語を知っている必要があり、jsでの記述方法も想像する必要があります。イントロツアーに行く
キャンバスチュートリアル
この資料の主な目的は、自分の考えを整理することです。 ここで前述のことを考えずにコピーできる例として考えてはいけません。
問題の声明
まず、タスクを決めましょう。 小さく始める必要がありますので、文字の描画、サーバーからのデータの送受信のみが可能な非常に単純化されたクライアントを作成します。 また、サーバーは、クライアントが必要とするすべてのゲームロジックを担当します。クライアントとサーバー間の接続は、Webソケットを介して編成されます。つまり、文字列のみを送信できます。また、ゆるやかなTCPにも我慢する必要があります。 実装とデバッグを簡単にするために、jsonでメッセージを交換します。
私の頭に浮かんだ最初の考えは、最初にクライアントを記述し、その後その助けを借りてサーバーをテストすることでした。 実際に、そうしました。 しかし、私たちは異なる行動をとります。 その理由が明らかになります。
サーバー
サーバーは次のタスクを実行します。- 顧客からのコマンドを受け取る
- 接続されたクライアントにゲームの世界の変化について通知する
- ゲームのサイクルを実行して、世界の状態を変えます
世界では、接続されたキャラクターなどのリストを意味します。 カードなし、障害物なし-プレイヤーのみ。 キャラクターができることは、特定の速度で特定のポイントに移動することだけです。
すると、キャラクターの構造は次のようになります。
/* point.go && character.go */ ... type Point struct { X, Y float64 } ... type Character struct { Pos, Dst Point // Angle float64 // Speed uint // Name string } ...
goでは、大文字で書かれたフィールドがエクスポート(パブリック)され、オブジェクトをシリアル化すると、エクスポートされたフィールドのみがjsonに追加されます。 ( 私はこのレーキを数回踏んだが、どうやら正しいように見えるコードが機能しない理由を理解していなかった。フィールドは小さな文字で書かれていたことがわかる )
クライアントでは、データを同期する必要があります。 すべての現在および将来のフィールドに対して
character.x = data.X
ようなコードを書かないために、サーバーからデータフィールドを再帰的に調べ、名前が一致する場合、それらをクライアントオブジェクトに割り当てます。 ただし、goのフィールドは大文字です。 したがって、goスタイルのjson命名規則を受け入れます。 このため、サーバーを調べることから始めました。
アプリケーションの初期化とメインループ
/* 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() }
Character.updateメソッドでは、移動先がある場合はキャラクターを移動します。
/* 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 } ...
次に、Webソケットに直接渡します。
/* 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 } } }
キャラクターを作成するには、いくつかのパラメーターを彼に尋ねなければなりません。 Goでは、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 }
これがサーバー全体です。
クライアント部分に関する記事は、このテキストに関するフィードバックを収集した後に書かれます。
参照資料
デモカードジェネレーター(背景の画像)
ソースコード