基于websocket的帧同步联机Demo体验

项目地址:https://github.com/beijian128/framesync

在线体验:点击此处传送- 帧同步联机Demo

服务端核心代码

// FrameSyncInstance 帧同步系统的全局实例
var FrameSyncInstance = NewFrameSync()

// FrameSync 管理多人游戏的帧同步逻辑
type FrameSync struct {
	// curFrame 存储当前帧所有玩家的操作指令
	// key: 玩家连接会话, value: 包含该玩家所有操作指令的结构体
	// 注意:暂不考虑历史帧存储(不支持断线重连和回放功能)
	curFrame map[appframe.GateSession]*cmsg.PlayerInput

	// ticker 控制帧同步的定时器
	ticker *time.Ticker

	// userCnt 玩家ID计数器,用于分配唯一ID
	userCnt uint64
}

// NewFrameSync 创建并初始化帧同步实例
func NewFrameSync() *FrameSync {
	return &FrameSync{
		curFrame: make(map[appframe.GateSession]*cmsg.PlayerInput),
	}
}

// OnInputCommand 处理玩家输入指令
// session: 发送指令的玩家会话
// msg: 包含移动方向的输入指令消息
func (fs *FrameSync) OnInputCommand(session appframe.GateSession, msg *cmsg.InputCommand) {
	// 从会话管理器获取完整的会话对象
	s, _ := SessionMgrInstance.getSession(session.ID())

	// 如果该玩家在当前帧还没有输入记录,则初始化
	if fs.curFrame[session] == nil {
		fs.curFrame[session] = &cmsg.PlayerInput{
			PlayerId: s.userid,               // 玩家ID
			Commands: []*cmsg.InputCommand{}, // 指令列表
		}
	}

	// 将新指令添加到该玩家的当前帧输入中
	fs.curFrame[session].Commands = append(fs.curFrame[session].Commands, msg)

	// 移动速度常量
	speed := int32(3)

	// 根据输入指令更新玩家位置
	if msg.Down {
		s.y += speed // 向下移动
	}
	if msg.Up {
		s.y -= speed // 向上移动
	}
	if msg.Left {
		s.x -= speed // 向左移动
	}
	if msg.Right {
		s.x += speed // 向右移动
	}
}

// OnAddSession 处理新玩家连接
// ss: 新加入的玩家会话
func (fs *FrameSync) OnAddSession(ss *session) {
	// 分配唯一玩家ID
	fs.userCnt++
	ss.userid = fs.userCnt

	// 设置随机初始位置(1-100范围内)
	ss.y = rand.Int31n(100) + 1
	ss.x = rand.Int31n(100) + 1

	// 为玩家分配随机颜色
	ss.color = generateRandomColor()

	// 通知所有现有玩家有新玩家加入
	SessionMgrInstance.execByEverySession(func(s *session) {
		// 向每个现有会话发送所有玩家信息(包括新玩家)
		SessionMgrInstance.execByEverySession(func(s2 *session) {
			s.SendMsg(&cmsg.PlayerEnter{
				PlayerId: s2.userid, // 玩家ID
				X:        s2.x,      // X坐标
				Y:        s2.y,      // Y坐标
				Color:    s2.color,  // 玩家颜色
			})
		})
	})
}

// StartSync 启动帧同步循环
// ctx: 上下文对象,用于控制协程退出
func (fs *FrameSync) StartSync(ctx context.Context) {
	// 创建16毫秒间隔的定时器(约60FPS)
	fs.ticker = time.NewTicker(16 * time.Millisecond)
	go func() {
		for {
			select {
			case <-ctx.Done(): // 收到退出信号
				fs.ticker.Stop()               // 停止定时器
				logrus.Debug("FrameSync exit") // 记录调试日志
				return                         // 退出协程
			case <-fs.ticker.C: // 定时器触发
				AppInstance.Post(func() {
					// 向所有会话广播当前帧的所有玩家输入
					SessionMgrInstance.execByEverySession(func(s *session) {
						for _, input := range fs.curFrame {
							s.SendMsg(input) // 发送玩家输入数据
						}
					})
					clear(fs.curFrame) // 清空当前帧数据
				})
			}
		}
	}()
}

// RemoveSession 处理玩家退出
// sess: 要移除的玩家会话
func (fs *FrameSync) RemoveSession(sess *session) {
	delete(fs.curFrame, sess.GateSession) // 从当前帧移除该玩家

	// 通知其他玩家该玩家已退出
	SessionMgrInstance.execByEverySession(func(s *session) {
		if s.ID() == sess.ID() { // 不通知自己
			return
		}
		s.SendMsg(&cmsg.PlayerLeave{PlayerId: sess.userid}) // 发送玩家离开消息
	})
}

C/S 交互 消息定义:

// ================== 输入指令 ==================
message InputCommand {
  bool up = 1;            // W键
  bool left = 2;          // A键
  bool down = 3;          // S键
  bool right = 4;         // D键
}

// ================== 玩家指令集 ==================
message PlayerInput {
  uint64 playerId = 1;
  repeated InputCommand commands = 2; // 该玩家的所有指令
}

// 新玩家刚进入,同步一次状态
message PlayerEnter{
  uint64 playerId = 1;
  int32 x = 2;
  int32 y = 3;
  string color = 4;
}

// 某个玩家离开
message PlayerLeave{
  uint64 playerId = 1;
}

客户端代码和更多完整代码详见 https://github.com/beijian128/framesync

滚动至顶部