阶段目标
- 从大厅进入战场
- 客户端修改为双人版本
- 战场内移动同步的实现
效果演示
单机版的结构
GameManager作为一个全局的游戏管理器存在,每个具体的坦克是由TankManager类来进行管理,其中,坦克具体的移动和射击逻辑是分别交由TankMovment和TankShooting去完成。
联机版的结构
联机版和单机版在类的设计实现上是一样的,主要区别也就下面几点:
- TankManager交由PlayerAvatar来持有,因为联机时,我们每个角色的数据创建时由server来驱动的,与server对应的玩家实体是PlayerAvatar,这个PlayerAvatar相当于是一个真实的服务器一致的逻辑数据实体。
- 区分出主玩家控制的坦克角色和其他非主玩家坦克
战场的跳转
这类开房间式的游戏,都会涉及到一个从大厅跳转战场的流程。坦克大战也是一样,我们的战场和大厅在这个版本里面都是SpaceRoom这一个类来完成,只是战场和大厅的uType值不同。
class SpaceType(object):
SPACE_TYPE_NONE = 0
SPACE_TYPE_HALL = 1
SPACE_TYPE_BATTLE = 2
在完成匹配后,我们会申请创建一个战场space,并且在space创建完毕后,让base上的playAvatar一个个enter进space,在enter到space上时,会先离开大厅这个space,然后调用playAvatar在cell部分的onTeleportSpaceCB方法,该方法主要做一件事情,就是让玩家teleport到战场所在的space。
跳转战场的时序图如下:
说明下,base_Hall就是大厅,base_SpaceRoom是战场,他们都是SpaceRoom这个类,只是图里面为了区分,用了不同的名称。
对应的基本代码(git版本号 459eb4a3980f183d5e8b9b191d91a1845bd7bf1c):
base/SpaceRoom.py
def enter(self, avatar_entity_call):
if self.uType == SpaceType.SPACE_TYPE_HALL:
avatar_entity_call.createCellEntity(self.cell)
else:
# 从大厅进入战场
# 先从大厅离开
avatar_entity_call.curHallSpace.leave(avatar_entity_call.id)
avatar_entity_call.cell.onTeleportSpaceCB(self, self.cell, avatar_entity_call.born_position, (0, 0, 0))
self._avatar_dict[avatar_entity_call.id] = avatar_entity_call
avatar_entity_call.onEnterSpace(self.id, self.uType)
cell/PlayerAvatar.py
def onTeleportSpaceCB(self, spaceBaseEntityCall, spaceCellEntityCall, position, direction):
"""
defined.
baseapp返回teleportSpace的回调
"""
DEBUG_MSG("PlayerAvatar::onTeleportSpaceCB. spaceBase:%s...spaceCell:%s" % (spaceBaseEntityCall, spaceCellEntityCall))
self.curSpaceBaseEntityCall = spaceBaseEntityCall
self.teleport(spaceCellEntityCall, position, direction)
移动同步
对于View范围内的Entity,其基础的属性,Kbengine引擎底层会自动帮我们做好了属性的同步,包括position和direction等。
但是,对于服务器自动同步下来的position,肯定都是一个个离散的点,同步数据到达客户端的时间也受到当前网络的影响,其次,服务端的tick频率也往往会比客户端低。如果客户端每次收到position的位置,就直接更新模型到对应的点上,看上去的移动一定会是一卡一卡的。
为了解决非主玩家的移动平滑同步,这里采用了简单的影子跟随算法。影子跟随简单理解就是,模型(Gameobject)一直去追随与之关联的逻辑对象(PlayAvatar)位置。这个PlayAvatar是个Entity,也就是和服务端保持一直的一个实体对象,这是个逻辑的数据,其上的position会经由服务器自动同步下发。每个PlayAvatar都有一个与之绑定的Gameobject对象,Gameobject是个渲染模型对象。
追随的核心算法在Scripts/Tank/TankMovement.cs里面。
// 影子追随
private float CalcNewValueByShadow(float curValue, float shadowValue, float deltaTime)
{
if (curValue == shadowValue)
return curValue;
float deltaValue = Mathf.Abs(curValue - shadowValue);
int ratio = 1;
if (curValue < shadowValue)
{
// 大于一定阈值,加速
if (deltaValue > m_Speed * 40 * deltaTime)
ratio = 2;
curValue += Mathf.Min(deltaValue, m_Speed * deltaTime * ratio);
}
else
{
if (deltaValue > m_Speed * 5 * deltaTime)
ratio = 2;
curValue -= Mathf.Min(deltaValue, m_Speed * deltaTime * ratio);
}
return curValue;
}
说明: 代码中有个40数值,这个仅仅是个保持相位差的最大阈值,可以根据需要自行调整,所谓相位差,就是每一帧下,实体和影子相对保持的距离。因为我们设定的移速是m_Speed,一帧的时间是deltaTime,40相当于是我们允许模型和影子的最大位移差是40倍的m_Speed*deltaTime。
存在的问题
这个版本其实是相对粗糙的,无论是代码结构还是游戏功能,都还属于功能验证和引擎学习阶段,对于战场也是只能每次只进入1轮便必须结束。这些问题会在下一篇文章里统一解决掉。这里只作为KBEngine和Unity的入门练习准备。
后续内容
- 战斗同步
- 代码结构调整,模块解耦(比如space拆分、ui和逻辑拆分)
- 断线重连和顶号
- 多房间创建(目前仅支持开一个战场,不然会出问题)
- KBEngine开发中遇到的坑
- KBEngine中部分功能的源码分析
源码地址
github地址:地址在这里
个人有点懒,再加上前端时间工作上比较忙,代码和文章会不定时更新哦。得逼自己一把,争取后续内容一个月内更完吧。