关于移动同步
首先需要知道,不同的游戏类型、玩法,会有不同的移动同步方法。
本项目采用的移动同步方法为:
- 所有移动过程均在服务器端计算
- 服务器每隔100ms发送一次移动状态包
- 客户端根据最新的移动状态包,做追随移动
项目移动代码分析 - ScenePlayer.go / BallPlayer.go
移动请求
func (this *ScenePlayer) Move(power, angle float64, face uint32) { if power != 0 { power = 1 // power恒为1,减少移动同步影响因素 } this.Power = power this.Face = face if power != 0 { this.Angle = angle } if power == 0 { this.isRunning = false } }服务器收到移动请求后,保存下次移动的角度等信息。怎么会调到该函数,略。
每帧计算移动
func (this *ScenePlayer) UpdateMove(perTime float64, frameRate float64) { if !this.IsLive { return } // 玩家球移动 ball := this.SelfAnimal ball.UpdateForce(perTime) if ball.Move(perTime, frameRate) { ball.FixMapEdge() //修边 this.isMoved = true ball.ResetRect() if this.isRunning { cost := frameRate * float64(consts.FrameTimeMS) * consts.DefaultRunCostMP diff := ball.GetMP() - cost if diff <= 0 { this.isRunning = false } else { ball.SetMP(diff) } } } }每帧都会计算移动
移动计算细节
func (this *BallPlayer) Move(perTime float64, frameRate float64) bool { // 有推力情况下, 忽略原来速度方向 if this.HasForce() == true { force := this.GetForce() pos := this.PhysicObj.GetPostion() this.Pos = util.Vector2{float64(pos.X), float64(pos.Y)} this.PhysicObj.SetVelocity(&bmath.Vector2{float32(force.X), float32(force.Y)}) return true } pos := this.PhysicObj.GetPostion() this.Pos = util.Vector2{float64(pos.X), float64(pos.Y)} speed := consts.DefaultBallSpeed powerMul := util.Clamp(this.player.GetPower(), 0, 1) if this.player.IsRunning() { speed *= consts.DefaultRunRatio powerMul = 1 } speed *= powerMul this.speed = *this.angleVel.MultiMethod(speed) vel := this.speed vel.ScaleBy(frameRate) //几帧执行一次物理tick if 0 == this.player.GetPower() { this.PhysicObj.SetVelocity(&bmath.Vector2{0, 0}) } else { this.PhysicObj.SetVelocity(&bmath.Vector2{float32(vel.X) / 30, float32(vel.Y) / 30}) } return true }这里看似很多代码,实际上就做了3个事情:
一. 从base/ape中,获取本球的实际位置( pos := this.PhysicObj.GetPostion() )
二. 确定本帧的期望位移大小
三. 把算好的期望位移传递给base/ape。( this.PhysicObj.SetVelocity( ... ) )
在下一帧,base/ape内部会根据传递给它的期望位移,算出实际位移。然后又重复1、2、3这3个步骤
项目移动状态同步代码分析 - ScenePlayer.go
func (this *ScenePlayer) SendSceneMsg() {
var (
// ...(无关代码,略)
Moves []*usercmd.BallMove
// ...(无关代码,略)
)
// ...(无关代码,略)
ball := this.SelfAnimal
if this.isMoved {
ballmove := this.ScenePlayerPool.MsgBallMove
ballmove.Id = ball.GetID()
ballmove.X = int32(ball.Pos.X * bll.MsgPosScaleRate)
ballmove.Y = int32(ball.Pos.Y * bll.MsgPosScaleRate)
// angle && face
if (this.SelfAnimal.HasForce() == false || this.Power == 0) && this.Face != 0 {
ballmove.Face = uint32(this.Face)
ballmove.Angle = 0
} else {
ballmove.Face = 0
ballmove.Angle = int32(this.Angle)
}
ballmove.State = 0
if this.isRunning {
ballmove.State = 2
}
if skillid := this.Skill.GetCurSkillId(); skillid != 0 {
ballmove.State = skillid
}
Moves = append(Moves, &ballmove)
}
//玩家广播
for _, other := range this.Others {
Eats = append(Eats, other.ScenePlayerPool.MsgEats...)
Hits = append(Hits, other.ScenePlayerPool.MsgHits...)
if other.isMoved {
ball = other.SelfAnimal
ballmove := other.ScenePlayerPool.MsgBallMove
ballmove.Id = ball.GetID()
ballmove.X = int32(ball.Pos.X * bll.MsgPosScaleRate)
ballmove.Y = int32(ball.Pos.Y * bll.MsgPosScaleRate)
// angle && face
if (other.SelfAnimal.HasForce() == false || other.Power == 0) && other.Face != 0 {
ballmove.Face = uint32(other.Face)
ballmove.Angle = 0
} else {
ballmove.Face = 0
ballmove.Angle = int32(other.Angle)
}
ballmove.State = 0
if other.isRunning {
ballmove.State = 2
}
if skillid := other.Skill.GetCurSkillId(); skillid != 0 {
ballmove.State = skillid
}
if other != this {
Moves = append(Moves, &ballmove)
}
}
}
// 玩家视野中的所有消息,发送给自己
for _, cell := range this.LookCells {
Moves = append(Moves, cell.MsgMoves...)
}
// ...(无关代码,略)
if len(Moves) != 0 {
msg := &this.msgPool.MsgSceneUDP
msg.Moves = Moves
msg.Frame = this.GetScene().Frame()
if this.Sess != nil {
// 优先采用可丢包的原生UDP信道发送
this.Sess.SendUDPCmd(usercmd.MsgTypeCmd_SceneUDP, msg)
}
}
}
每100ms,会把视野内所有球的移动状态信息通过 MsgSceneUDP协议 发给客户端。