简介

SuperstellarGolang
health pointsenergy points

技术栈

central servera front end app
game simulationclient network communicationstatisticsmonitoringGo
JavaScriptwebpackPixieJS

在本文的其余部分中,我将讨论后端部分,而客户端应用程序将留待以后讨论。

游戏状态主控模拟 - 在一个地方,而且只有一个地方

Superstellar

下面是服务器工作方式的总体概述。它同时运行三种不同类型的动作:

simulation
20tt + 1
Gogoroutinechanneltickerssimulations stepsGoCPUgoroutinechannels

与客户端通信

websocketsGorilla web toolkitGolangwebsocketswebsocketGorilla
websockethandlerwebsocketclient
superstellar_websocket_handler.go
handler := func(w http.ResponseWriter, r *http.Request) {
  conn, err := s.upgrader.Upgrade(w, r, nil)
  
  if err != nil {
    log.Println(err)
    return
  }

  client := NewClient(conn, … //other attributes)
  client.Listen()
}
writingreadinggoroutinego
superstellar_websocket_listen.go
func (c *Client) Listen() {
  go c.listenWrite()
  c.listenRead()
}
readReadMessage()
superstellar_websocket_listen_loop.go
func (c *Client) listenRead() {
  for {
    messageType, data, err := c.conn.ReadMessage()

    if err != nil {
      log.Println(err)
    } else if messageType == websocket.BinaryMessage {
      // unmarshall and handle the data
    }
  }
}
goroutinegoroutines20050

当我们建立低级通信机制时,我们需要选择双方都将用来交换游戏消息的协议。 事实证明不是那么明显。

通信-协议必须小巧轻便

JSONGolangJavaScriptGostruct
superstellar_json_structs.go
type Spaceship struct {
  Position          *types.Vector `json:"position"`
  Velocity          *types.Vector `json:"velocity"`
  Facing            *types.Vector `json:"facing"`
  AngularVelocity   float64       `json:"thrust"`
}
JSONJSON
superstellar_json_marshall.go
bytes, err := json.Marshal(spaceship)
JSONJSON410“2147483647”
protobufProtobufDSLJavaScriptstruct
protobuf
superstellar_spaceship.proto
message Spaceship {
  uint32  id              = 1;
  Point   position        = 2;
  Vector  velocity        = 3;
  double  facing          = 4;
  double  angularVelocity = 5;
  ...
}
protobuf
superstellar_spaceship_to_proto.go
func (s *Spaceship) ToProto() *pb.Spaceship {
  return &pb.Spaceship {
    Id: s.Id(),
    Position: s.Position().ToProto(),
    Velocity: s.Velocity().ToProto(),
    Facing: s.Facing(),
    AngularVelocity: s.AngularVelocity(),
    ...
  }
}

最后序列化为原始字节:

superstellar_proto_marshal.go 
bytes, err := proto.Marshal(message)

现在,我们可以简单地通过网络以最小的开销将这些字节发送给客户端。

移动平滑和连接滞后补偿

一开始,我们试图在每个模拟帧上发送整个世界的状态。这样,客户端只会在接收到服务器消息时重新绘制屏幕。然而,这种方法导致了大量的网络流量—我们不得不将游戏中每个对象的细节每秒发送50次给所有的客户端,以使动画流畅。太多的数据了!

JavaScript
Internet

从一个程序包到事件调度程序

Go
$ go build
import cycle not allowed
Go
client joinsleavessends an input messagedispatcher

下面是一个示例,说明我们如何使用事件调度程序来传播模拟更新时间间隔。首先,我们需要创建一个能够监听事件的结构:

superstellar_eventdisp_create.go
type Updater struct {}

func (updater *Updater) HandleTimeTick(*events.TimeTick) {
  // do something with the event
}

然后我们需要实例化它,并将它注册到事件调度程序中:

superstellar_eventdisp_time_tick.go
updater := Updater{}
 
eventDispatcher := events.NewEventDispatcher()
eventDispatcher.RegisterTimeTickListener(updater)
ticker
superstellar_eventdisp_time_tick_loop.go
for range time.Tick(constants.PhysicsFrameDuration) {
  event := &events.TimeTick{}
  eventDispatcher.FireTimeTick(event)
}
goroutine
Go
Go

结论

Go

我们还了解了在创建实时多人游戏时必须面对的问题。 客户端和服务器之间的通信量可能非常大,必须付出很多努力来降低它。 您也不会忘记不可避免地会出现的滞后和网络问题。

最后值得一提的是,创建一个简单的在线游戏也需要大量的工作,无论是在内部实现方面还是在您想使其变得有趣且可玩时。 我们花了无休止的时间讨论要在游戏中放入哪种武器,资源或其他功能,只是意识到要实际实现需要多少工作。 但是,当您尝试做一些对您来说是全新的事情时,即使您设法制造出最小的东西也能给您带来很多满足感。

Refs