上一篇UnityMMO主世界的网络同步还没有说完,这一篇继续。
和作者大鹏聊了一下SynchFromNet为什么没有实现System,SynchFromNet是由服务器端驱动的,其功能就是进行网络同步。网络上的敌方释放了一个技能时,这个技能如果在主角视线范围内产生了影响,服务器才会把改变的信息发给客户端进行同步。理论上讲SynchFromNet就是System,因为它做了System该做的事情,只是没有继承和实现JobComponentSystem而已。
大鹏说要这样做(继承和实现JobComponentSystem)也是可以的:
实在想用ECS做的话就是收到后端协议时就加个组件(组件保存了服务器发送过来的数据),然后在System里处理(对实体组件进行刷选,如果实体上有相关改变的组件,则对其进行更改)。不过不需要为了用ECS而用ECS,它只是一个工具,有适合使用的情景。
因此暂时没有对其进行重构,我思考了好久,现在没有重构不保证以后不会这样去实现,毕竟ECS是面向数据的,那么在数据驱动的思想设计上,如果服务器发送了改变的数据,就应该驱动实体进行改变。这才是ECS的真正设计思想,大家觉得呢?
可能我的理解并没有大鹏的深刻,毕竟作者在实现SynchFromNet的时候是一年前,那个时候的ECS并没有现在这样成熟。至于是否应该重构,似乎并没有标准答案,只有数据达到一定的量级,我们才能够做出判断。
究竟什么时候该用ECS呢?
0下载Unity编辑器(2019.1.4f1 or 更新的版本),if(已经下载了)continue;
1大鹏将项目代码和资源拆分成两部分,所以我们需要分别下载,然后再整合。
命令行下载UnityMMO,打开Git Shell输入:
git clone https://github.com/liuhaopen/UnityMMO.git --recurse
下载完成后,继续输入:
git clone https://github.com/liuhaopen/UnityMMO-Resource.git --recurse
or 点击UnityMMO和UnityMMO-Resource分别下载Zip压缩包
if(已经下载了)continue;
2如果下载的是压缩包,需要先将两个压缩包分别进行解压。然后打开UnityMMO-Resource并把Assets/AssetBundleRes及其meta文件复制到UnityMMO项目的Assets目录里,接下来将UnityMMO添加到Unity Hub项目中;
3用Unity Hub打开大鹏的开源项目:UnityMMO,等待Unity进行编译工作;
4打开项目后,我们发现还需要下载Third Person Controller - Basic Locomotion FREE插件,这个简单,直接在资源商店找到下载导入即可,然后在Assets/XLuaFramework下找到main场景,打开该场景。
仔细研究了一个小时,我发现自己理解错了,SynchFromNet是实体,它把数据交给组件,然后对应的系统会自动处理。只是这个实体是混合单例实体,而不是ECS传统的实体,我是陷入思维定式了Orz。
///
/// 应用位置信息改变
///
/// 实体
/// 改变的信息
private void ApplyChangeInfoPos(Entity entity, SprotoType.info_item change_info)
{
//把服务器的信息转化成本地数据,然后交给C组件
string[] pos_strs = change_info.value.Split(',');
// Debug.Log("SynchFromNet recieve pos value : "+change_info.value);
if (pos_strs.Length < 3)
{
Debug.Log("SynchFromNet recieve a wrong pos value : "+change_info.value);
return;
}
long new_x = Int64.Parse(pos_strs[0]);
long new_y = Int64.Parse(pos_strs[1]);
long new_z = Int64.Parse(pos_strs[2]);
Transform trans = SceneMgr.Instance.EntityManager.GetComponentObject<Transform>(entity);
trans.localPosition = SceneMgr.Instance.GetCorrectPos(new Vector3(new_x/GameConst.RealToLogic, new_y/GameConst.RealToLogic, new_z/GameConst.RealToLogic));
SceneMgr.Instance.EntityManager.SetComponentData(entity, new TargetPosition {Value = trans.localPosition});
}
如上代码是SynchFromNet中,在收到服务器位置信息改变时触发的,对应到ECS中应该是下表这样:
E | C | S |
---|---|---|
entity | TargetPosition | MovementUpdateSystem |
所以,ApplyChangeInfoPos实际上干了Convert的事情,在传统的E中,Convert就是把数据交给C的,然后由S去更新。
思维定式太可怕了,不一定非要叫什么名字才是什么,非要实现什么接口才是什么,而大鹏做的就是数据驱动系统去更新,没有理由重构。我对ECS的理解也应该是正确的,只是想要凭借官方案例的经验去对号入座了。
同样的原理,目标位置信息也传递给C,然后由S最终处理:
///
/// 把目标位置改变信息转化成本地信息,然后传递给组件
///
/// 实体
/// 服务器发送的改变信息
private void ApplyChangeInfoTargetPos(Entity entity, SprotoType.info_item change_info)
{
string[] pos_strs = change_info.value.Split(',');
// Debug.Log("SynchFromNet recieve pos value : "+change_info.value);
if (pos_strs.Length != 2)
{
Debug.Log("SynchFromNet recieve a wrong pos value : "+change_info.value);
return;
}
long new_x = Int64.Parse(pos_strs[0]);
// long new_y = Int64.Parse(pos_strs[1]);
long new_z = Int64.Parse(pos_strs[1]);
var newTargetPos = new float3(new_x/GameConst.RealToLogic, 0, new_z/GameConst.RealToLogic);
SceneMgr.Instance.EntityManager.SetComponentData(entity, new TargetPosition {Value = newTargetPos});
}
///
/// 同上:跳跃状态
///
/// 实体
/// 服务器信息
private void ApplyChangeInfoJumpState(Entity entity, SprotoType.info_item change_info)
{
var actionData = SceneMgr.Instance.EntityManager.GetComponentData<ActionData>(entity);
actionData.Jump = 1;
SceneMgr.Instance.EntityManager.SetComponentData(entity, actionData);
}
///
/// 场景改变
///
/// 实体
/// 改变信息
private void ApplyChangeInfoSceneChange(Entity entity, SprotoType.info_item change_info)
{
Debug.Log("ApplyChangeInfoSceneChange : "+change_info.value);
string[] strs = change_info.value.Split(',');
int sceneID = int.Parse(strs[0]);
//激活加载视图,加载对应场景
LoadingView.Instance.SetActive(true);
LoadingView.Instance.ResetData();
SceneMgr.Instance.LoadScene(sceneID);
//把对应的实体的信息转化成本地信息,传递给C
if (entity != Entity.Null)
{
long new_x = Int64.Parse(strs[1]);
long new_y = Int64.Parse(strs[2]);
long new_z = Int64.Parse(strs[3]);
Transform trans = SceneMgr.Instance.EntityManager.GetComponentObject<Transform>(entity);
trans.localPosition = SceneMgr.Instance.GetCorrectPos(new Vector3(new_x/GameConst.RealToLogic, new_y/GameConst.RealToLogic, new_z/GameConst.RealToLogic));
SceneMgr.Instance.EntityManager.SetComponentData(entity, new TargetPosition {Value = trans.localPosition});
}
}
///
/// 血量改变
///
/// 实体
/// 改变信息
private void ApplyChangeInfoHPChange(Entity entity, SprotoType.info_item change_info)
{
// Debug.Log("hp change : "+change_info.value);
string[] strs = change_info.value.Split(',');
float curHp = (float)Int64.Parse(strs[0])/GameConst.RealToLogic;
var healthData = SceneMgr.Instance.EntityManager.GetComponentData<HealthStateData>(entity);
healthData.CurHp = curHp;
SceneMgr.Instance.EntityManager.SetComponentData(entity, healthData);
bool hasNameboardData = SceneMgr.Instance.EntityManager.HasComponent<NameboardData>(entity);
if (hasNameboardData)
{
var nameboardData = SceneMgr.Instance.EntityManager.GetComponentData<NameboardData>(entity);
if (nameboardData.UIResState==NameboardData.ResState.Loaded)
{
var nameboardNode = SceneMgr.Instance.EntityManager.GetComponentObject<Nameboard>(nameboardData.UIEntity);
if (nameboardNode != null)
{
nameboardNode.CurHp = curHp;
//remove nameboard when dead
var isDead = strs.Length == 2 && strs[1]=="dead";
if (isDead)
{
SceneMgr.Instance.World.RequestDespawn(nameboardNode.gameObject);
nameboardData.UIResState = NameboardData.ResState.DontLoad;
nameboardData.UIEntity = Entity.Null;
SceneMgr.Instance.EntityManager.SetComponentData(entity, nameboardData);
}
}
}
else if (nameboardData.UIResState==NameboardData.ResState.DontLoad)
{
var isRelive = strs.Length == 2 && strs[1]=="relive";
Debug.Log("isRelive : "+isRelive);
if (isRelive)
{
nameboardData.UIResState = NameboardData.ResState.WaitLoad;
SceneMgr.Instance.EntityManager.SetComponentData(entity, nameboardData);
}
}
}
if (strs.Length == 2)
{
var isRelive = strs[1]=="relive";
var locoState = SceneMgr.Instance.EntityManager.GetComponentData<LocomotionState>(entity);
locoState.LocoState = isRelive?LocomotionState.State.Idle:LocomotionState.State.Dead;
// Debug.Log("Time : "+TimeEx.ServerTime.ToString()+" time:"+change_info.time+" isRelive:"+isRelive+" state:"+locoState.LocoState.ToString());
locoState.StartTime = Time.time - (TimeEx.ServerTime-change_info.time)/1000.0f;
SceneMgr.Instance.EntityManager.SetComponentData(entity, locoState);
}
}
下一步S,S中会每帧把数据同步到实体上:
[DisableAutoCreation]//禁用自动创建
public class MovementUpdateSystem : BaseComponentSystem
{
public MovementUpdateSystem(GameWorld world) : base(world) {}
EntityQuery group;
///
/// 通过C来刷选E,并缓存到组中
///
protected override void OnCreateManager()
{
base.OnCreateManager();
// group = GetComponentGroup(typeof(TargetPosition), typeof(Transform), typeof(MoveSpeed), typeof(MoveQuery), typeof(LocomotionState), typeof(PosOffset), typeof(PosSynchInfo));
group = GetEntityQuery(typeof(TargetPosition), typeof(Transform), typeof(MoveSpeed), typeof(MoveQuery), typeof(LocomotionState), typeof(PosOffset));
}
///
/// 每帧调用,把数据同步到实体上
///
protected override void OnUpdate()
{
if (SceneMgr.Instance.IsLoadingScene)//正在加载场景则不用同步
return;
float dt = Time.deltaTime;
//各种缓存
var entities = group.ToEntityArray(Allocator.TempJob);//所有实体
var targetPositions = group.ToComponentDataArray<TargetPosition>(Allocator.TempJob);//目标位置数据
var speeds = group.ToComponentDataArray<MoveSpeed>(Allocator.TempJob);//移动速度数据
var transforms = group.ToComponentArray<Transform>();//Transform组件
var moveQuerys = group.ToComponentArray<MoveQuery>();
var locoStates = group.ToComponentDataArray<LocomotionState>(Allocator.TempJob);//移动状态数据
var posOffsets = group.ToComponentDataArray<PosOffset>(Allocator.TempJob);//位置偏移数据
for (int i=0; i<targetPositions.Length; i++)
{
var targetPos = targetPositions[i].Value;
var speed = speeds[i].Value;
var posOffset = posOffsets[i].Value;
var curLocoStateObj = locoStates[i];
var query = moveQuerys[i];
// if (speed <= 0 || curLocoStateObj.LocoState==LocomotionState.State.BeHit|| curLocoStateObj.LocoState==LocomotionState.State.Dead)
if (speed <= 0)
continue;
var curTrans = transforms[i];
float3 startPos = curTrans.localPosition;
var moveDir = targetPos-startPos;
var groundDir = moveDir;
groundDir.y = 0;
float moveDistance = Vector3.Magnitude(groundDir);
groundDir = Vector3.Normalize(groundDir);
var isAutoFinding = query.IsAutoFinding;
bool isMoveWanted = moveDistance>0.01f || isAutoFinding;
var newLocoState = LocomotionState.State.StateNum;
var phaseDuration = Time.time - curLocoStateObj.StartTime;
var curLocoState = curLocoStateObj.LocoState;
bool isOnGround = curLocoStateObj.IsOnGround();
if (isOnGround)
{
if (isMoveWanted)
newLocoState = LocomotionState.State.Run;
else
newLocoState = LocomotionState.State.Idle;
}
float ySpeed = 0;
bool isClickJump = false;
if (EntityManager.HasComponent<ActionData>(entities[i]))
{
var actionData = EntityManager.GetComponentData<ActionData>(entities[i]);
isClickJump = actionData.Jump == 1;
}
// Jump 跳
if (isOnGround)
curLocoStateObj.JumpCount = 0;
if (isClickJump && isOnGround)
{
curLocoStateObj.JumpCount = 1;
newLocoState = LocomotionState.State.Jump;
}
if (isClickJump && curLocoStateObj.IsInJump() && curLocoStateObj.JumpCount < 3)
{
curLocoStateObj.JumpCount = curLocoStateObj.JumpCount + 1;
newLocoState = curLocoStateObj.JumpCount==2?LocomotionState.State.DoubleJump:LocomotionState.State.TrebleJump;
}
if (curLocoStateObj.LocoState == LocomotionState.State.Jump || curLocoStateObj.LocoState == LocomotionState.State.DoubleJump || curLocoStateObj.LocoState == LocomotionState.State.TrebleJump)
{
if (phaseDuration >= GameConst.JumpAscentDuration[curLocoStateObj.LocoState-LocomotionState.State.Jump])
newLocoState = LocomotionState.State.InAir;
}
if (newLocoState != LocomotionState.State.StateNum && newLocoState != curLocoState)
{
curLocoStateObj.LocoState = newLocoState;
curLocoStateObj.StartTime = Time.time;
}
if (curLocoStateObj.LocoState == LocomotionState.State.Jump || curLocoStateObj.LocoState == LocomotionState.State.DoubleJump || curLocoStateObj.LocoState == LocomotionState.State.TrebleJump)
{
ySpeed = GameConst.JumpAscentHeight[curLocoStateObj.LocoState-LocomotionState.State.Jump] / GameConst.JumpAscentDuration[curLocoStateObj.LocoState-LocomotionState.State.Jump] - GameConst.Gravity;
}
EntityManager.SetComponentData<LocomotionState>(entities[i], curLocoStateObj);
if (isAutoFinding)
{
curTrans.rotation = query.navAgent.transform.rotation;
}
else
{
float3 newPos;
if (moveDistance < speed/GameConst.SpeedFactor*dt)
{
//目标已经离得很近了
newPos = targetPos;
}
else
{
newPos = startPos+groundDir*speed/GameConst.SpeedFactor*dt;
}
newPos.y = startPos.y;
//模仿重力,人物需要贴着地面走,有碰撞检测的所以不怕
newPos.y += (GameConst.Gravity+ySpeed) * dt;
newPos += posOffset;
query.moveQueryStart = startPos;
// Debug.Log("newPos : "+newPos.x+" z:"+newPos.z+" target:"+targetPos.x+" z:"+targetPos.z);
//不能直接设置新坐标,因为需要和地形做碰撞处理什么的,所以利用CharacterController走路,在HandleMovementQueries才设置新坐标
query.moveQueryEnd = newPos;
//change role rotation
if (isMoveWanted)
{
Vector3 targetDirection = new Vector3(groundDir.x, groundDir.y, groundDir.z);
Vector3 lookDirection = targetDirection.normalized;
Quaternion freeRotation = Quaternion.LookRotation(lookDirection, curTrans.up);
var diferenceRotation = freeRotation.eulerAngles.y - curTrans.eulerAngles.y;
var eulerY = curTrans.eulerAngles.y;
if (diferenceRotation < 0 || diferenceRotation > 0) eulerY = freeRotation.eulerAngles.y;
var euler = new Vector3(0, eulerY, 0);
curTrans.rotation = Quaternion.Slerp(curTrans.rotation, Quaternion.Euler(euler), Time.deltaTime*50);
}
}
}
//释放内存
entities.Dispose();
targetPositions.Dispose();
speeds.Dispose();
locoStates.Dispose();
posOffsets.Dispose();
}
}
主世界加载之后,就需要处理玩家输入了,玩家输入系统在MainWorld.cs脚本的InitializeSystems方法中第一个被主世界创建出来,如下图所示:
这些系统组成了主世界的大部分规则,这里先看看玩家输入系统PlayerInputSystem:
///
/// S:玩家输入系统
///
[DisableAutoCreation]//禁用自动创建 在StartGame中触发创建
public class PlayerInputSystem : ComponentSystem
{
public PlayerInputSystem()
{//构造函数
}
EntityQuery group;//实体组缓存
///
/// 在创建管理器时获取实体组(通过实体上的组件来进行刷选)
///
protected override void OnCreateManager()
{
base.OnCreateManager();//该实体上包含C:用户命令,目标位置,移动速度
group = GetEntityQuery(typeof(UserCommand), typeof(TargetPosition), typeof(MoveSpeed));
}
///
/// 每帧更新
///
protected override void OnUpdate()
{
// Debug.Log("on OnUpdate player input system");
float dt = Time.deltaTime;
//缓存数据
var userCommandArray = group.ToComponentDataArray<UserCommand>(Allocator.TempJob);
var targetPosArray = group.ToComponentDataArray<TargetPosition>(Allocator.TempJob);
var moveSpeedArray = group.ToComponentDataArray<MoveSpeed>(Allocator.TempJob);
if (userCommandArray.Length > 0)
{
var userCommand = userCommandArray[0];
SampleInput(ref userCommand, dt);
}
//释放缓存
userCommandArray.Dispose();
targetPosArray.Dispose();
moveSpeedArray.Dispose();
}
///
/// 简单输入处理
///
/// 用户命令
/// 时间
public void SampleInput(ref UserCommand command, float deltaTime)
{
//获取键盘方向输入(上下左右)
Vector2 moveInput = new Vector2(Input.GetAxisRaw("Horizontal"), Input.GetAxisRaw("Vertical"));
// GameInput.GetInstance().JoystickDir = new Vector2(Input.GetAxisRaw("Horizontal"), Input.GetAxisRaw("Vertical"));
float angle = Vector2.Angle(Vector2.up, moveInput);
if (moveInput.x < 0)
angle = 360 - angle;
float magnitude = Mathf.Clamp(moveInput.magnitude, 0, 1);
command.moveYaw = angle;
command.moveMagnitude = magnitude;
//获取主角混合体
var roleGameOE = RoleMgr.GetInstance().GetMainRole();
EntityManager.SetComponentData<ActionData>(roleGameOE.Entity, ActionData.Empty);
float invertY = 1.0f;
Vector2 deltaMousePos = new Vector2(0, 0);
if(deltaTime > 0.0f)
deltaMousePos += new Vector2(Input.GetAxisRaw("Mouse X"), Input.GetAxisRaw("Mouse Y") * invertY);
const float configMouseSensitivity = 1.5f;
command.lookYaw += deltaMousePos.x * configMouseSensitivity;
command.lookYaw = command.lookYaw % 360;
while (command.lookYaw < 0.0f) command.lookYaw += 360.0f;
command.lookPitch += deltaMousePos.y * configMouseSensitivity;
command.lookPitch = Mathf.Clamp(command.lookPitch, 0, 180);
command.jump = (command.jump!=0 || Input.GetKeyDown(KeyCode.Space))?1:0;
command.sprint = (command.sprint!=0 || Input.GetKey(KeyCode.LeftShift))?1:0;
//技能释放
if (GameInput.GetInstance().GetKeyUp(KeyCode.J))
CastSkill(-1);
else if (GameInput.GetInstance().GetKeyUp(KeyCode.I))
CastSkill(0);
else if (GameInput.GetInstance().GetKeyUp(KeyCode.O))
CastSkill(1);
else if (GameInput.GetInstance().GetKeyUp(KeyCode.K))
CastSkill(2);
else if (GameInput.GetInstance().GetKeyUp(KeyCode.L))
CastSkill(3);
else if (GameInput.GetInstance().GetKeyUp(KeyCode.Space))
DoJump();
}
///
/// 跳跃
///
void DoJump()
{
var roleGameOE = RoleMgr.GetInstance().GetMainRole();
var jumpState = EntityManager.GetComponentData<LocomotionState>(roleGameOE.Entity);
var isMaxJump = jumpState.JumpCount >= GameConst.MaxJumpCount;
if (isMaxJump)
{
//已经最高跳段了,就不能再跳
return;
}
var actionData = EntityManager.GetComponentData<ActionData>(roleGameOE.Entity);
actionData.Jump = 1;
EntityManager.SetComponentData<ActionData>(roleGameOE.Entity, actionData);
//这里的timeline只作跳跃中的表现,如加粒子加女主尾巴等,状态和高度控制还是放在MovementUpdateSystem里,因为跳跃这个动作什么时候结束是未知的,你可能跳崖了,这时跳跃状态会一直维持至到碰到地面,所以不方便放在timeline里。
var newJumpCount = math.clamp(jumpState.JumpCount+1, 0, GameConst.MaxJumpCount);
var roleInfo = roleGameOE.GetComponent<RoleInfo>();
var assetPath = ResPath.GetRoleJumpResPath(roleInfo.Career, newJumpCount);
var timelineInfo = new TimelineInfo{ResPath=assetPath, Owner=roleGameOE.Entity, StateChange=null};
var uid = EntityManager.GetComponentData<UID>(roleGameOE.Entity);
TimelineManager.GetInstance().AddTimeline(uid.Value, timelineInfo, EntityManager);
}
///
/// 投射技能
///
/// 技能索引
void CastSkill(int skillIndex=-1)
{
var roleGameOE = RoleMgr.GetInstance().GetMainRole();
var roleInfo = roleGameOE.GetComponent<RoleInfo>();
var skillID = SkillManager.GetInstance().GetSkillIDByIndex(skillIndex);
string assetPath = ResPath.GetRoleSkillResPath(skillID);
bool isNormalAttack = skillIndex == -1;//普通攻击
if (!isNormalAttack)
SkillManager.GetInstance().ResetCombo();//使用非普攻技能时就重置连击索引
var uid = EntityManager.GetComponentData<UID>(roleGameOE.Entity);
Action<TimelineInfo.Event> afterAdd = null;
if (isNormalAttack)
{
//普攻的话增加连击索引
afterAdd = (TimelineInfo.Event e)=>
{
if (e == TimelineInfo.Event.AfterAdd)
SkillManager.GetInstance().IncreaseCombo();
};
}
var timelineInfo = new TimelineInfo{ResPath=assetPath, Owner=roleGameOE.Entity, StateChange=afterAdd};
TimelineManager.GetInstance().AddTimeline(uid.Value, timelineInfo, EntityManager);
}
}
这一篇给主世界的多系统开个头,下篇主要将System系统。
这一篇梳理下网络同步的流程:
如果喜欢我的文章可以点赞支持一下,谢谢鼓励!如果有什么疑问可以给我留言,有错漏的地方请批评指证!
如果有技术难题需要讨论,可以加入开发者联盟:566189328(付费群)为您提供有限的技术支持,以及,心灵鸡汤!
当然,不需要技术支持也欢迎加入进来,随时可以请我喝咖啡、茶和果汁!( ̄┰ ̄*)