MMO移动同步(1)

2024-09-04 18:28
文章标签 同步 移动 mmo

本文主要是介绍MMO移动同步(1),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

多个客户端同时连入游戏

这篇会从以下五个部分讲解:

同步的基本概念

完善角色进入及离开处理

CharacterManager(C/S)

EntityManager(C/S)

打包运行Win客户端

同步基本概念

同步:角色信息,位置,状态同步;客户端和服务端的数据是一致的;//只有网络游戏有同步,单机游戏没有

两种同步对比

详情请看:两种同步模式

状态同步帧同步
消息传输量
回放较难还原容易
安全性较多逻辑在服务器,安全主要逻辑在客户端,难以避免外挂
战斗校验较难精确校验服务器可进行完整战斗模拟
服务器压力重逻辑,大转发为主,小
网络卡顿表现瞬移,闪回,血量不一致,各种异常战斗卡顿
断线重连无负担游戏时长越长恢复压力越大
实现难点客户端需要做一些插值,或者行为预测等方式来优化卡顿体验。较多的逻辑要在服务器实现,调测压力较大需要规避辉昂宿问题,逻辑要与表现进行分离,对设计有一定要求

消息传输量:帧同步要定时的发送消息,需要高帧率(15b/s)

//做帧同步为回合制下的,一个回合发送一帧,也是合理的

战斗回放:MMO很难做到,RTS容易做到//RTS:是实时游戏而不采回合制

战斗校验:状态同步:每个状态的发送其他人收到是有延迟的

网络卡顿表现:帧同步:一个人掉线所有人都在等(必须保证每个人的战斗状态是一致的,不能模拟某个人)//有的游戏会在玩家掉线时委托AI承担游戏行为,并断线重连

以下的讲解的同步都是状态同步

同步什么?

 //扩展数据:只有打开某些界面,才会请求这些详细信息时,才把信息拉过来;正常情况下信息不会同步

逻辑图

打包发布准备

点击右下角Build ;创建一个Bin文件夹

等待把Window包打包出来

打包完这样:

双击启动,服务端也启动

发现卡住不动了,为了看到错误信息,要在运行目录下配置日志;

把日志复制到运行目录下

 我们可以在这里查看日志文件

显示找不到地图配置表

因为我们的Bin下面根本没有Data目录 ;从客户端拷进来

 //这个时候可以成功进入,并运行到我们恰好写到的小地图系统

角色离开逻辑

在UI界面有个离开按钮

给它添加逻辑

打开UIMainCity.cs//即前面这个框对应的逻辑所在的脚本

BackToCharSelect()

加载角色选择场景;

告诉服务器角色离开;

//离开服务器可以换个角色重新进入

public void BackToCharSelect()
{SceneManager.Instance.LoadScene("CharSelect");UserService.Instance.SendGameLeave();
}

把事件绑在按钮上

Client:UserService:SendGameLeave
public void SendGameLeave()
{Debug.Log("UserGameLeaveRequest");NetMessage message = new NetMessage();message.Request = new NetMessageRequest();message.Request.gameLeave = new UserGameLeaveRequest();NetClient.Instance.SendMessage(message);
}

Client:OnGameLeave

 void OnGameLeave(object sender, UserGameLeaveResponse response){MapService.Instance.CurrentMapId = 0;User.Instance.CurrentCharacter = null;Debug.LogFormat("OnGameLeave:{0} [{1}]", response.Result, response.Errormsg);}
//Add:鼠标右键调摄像机视角(未做

GameServer:UserService:OnGameLeave

订阅UserGameLeaveRequest协议下的OnGameLeave

在UserService()中加上

            MessageDistributer<NetConnection<NetSession>>.Instance.Subscribe<UserGameLeaveRequest>(this.OnGameLeave);

OnGameLeave:

//离开时:RemoveCharacter;MapManagerLeave

//进入地图时:AddCharacter;MapMangaer.CharacterEnter

准备信息发给客户端

 void OnGameLeave(NetConnection<NetSession> sender, UserGameLeaveRequest request){
//由Sender传入一个characterCharacter character = sender.Session.Character;Log.InfoFormat("UserGameLeaveRequest:characterID:{0}:{1} Map:{2}",character.Id,character.Info.Name,character.Info.mapId);//收到了游戏的请求,从游戏管理器中把角色移掉CharacterManager.Instance.RemoveCharacter(character.Id);MapManager.Instance[character.Info.mapId].CharacterLeave(character.Info);NetMessage message=new NetMessage();message.Response = new NetMessageResponse();message.Response.gameLeave=new UserGameLeaveResponse();message.Response.gameLeave.Result = Result.Success;message.Response.gameLeave.Errormsg = "None";byte[] data=PackageHandler.PackMessage(message);sender.SendData(data,0,data.Length);}

转到定义MapManager:Map

Map:CharacterLeave

输出哪个角色离开了哪张地图

把角色从MapCharacters字典中移除

给所有还在地图中的角色广播此角色离开的信息SendCharacterLeaveMap

internal void CharacterLeave(NCharacterInfo cha)
{//离开时输出日志,哪个角色离开了那张地图Log.InfoFormat("CharacterLeave: Map:{0} characterId:{1}", this.Define.ID, cha.Id);this.MapCharacters.Remove(cha.Id);foreach (var kv in this.MapCharacters){this.SendCharacterLeaveMap(kv.Value.connection, cha);}
}
SendCharacterLeaveMap:

把角色Id传入,此角色离开地图

//此信息发到服务器窗口上

private void SendCharacterLeaveMap(NetConnection<NetSession> conn,NCharacterInfo character)
{NetMessage message = new NetMessage();message.Response = new NetMessageResponse();message.Response.mapCharacterLeave = new MapCharacterLeaveResponse();message.Response.mapCharacterLeave.characterId = character.Id;byte[] data=PackageHandler.PackMessage(message);conn.SendData(data,0,data.Length);}
演示

测试依赖windows端

每次发布之前删掉ExtremeWorld_Data,ExtrememWorld.exe,这两个

会发现刚刚删掉的那两个,又生成了新的

 //Windows运行时;可以用Unity运行查看报错

//两边可以登录不同的账号

启动服务器:

运行Windows端

 直接选择现有的角色

 

点击进入游戏

打开Unity登录另一个账号

 点击创建角色

点击开始冒险

 跳转到选择角色的面板:

选择新建的角色,点击进入游戏

这里不知道为啥被销毁了//

可以移动新疆炒米粉

回到Windows窗口

现在只能操控新疆炒米粉了

在unity的界面点击返回角色选择的按钮

选择懒羊羊大王

再进入游戏

这里是因为退出去有重新选择角色进入,但是原先的角色没有被销毁

Windows上操作,发现移动的依然是新疆炒米粉

出现多个角色,离开逻辑未完善

Client:MapService:OnMapCharacterLeave()

如果有玩家离开了地图,判断当前的玩家是否是自己

是:所有的角色都要销毁掉(因为我已经不再地图中了)

不是:把哪个角色 移除(保证角色管理器的角色都是再地图中的

private void OnMapCharacterLeave(object sender, MapCharacterLeaveResponse response)
{Debug.LogFormat("OnMapCharacterLeave: CharID:{0}", response.characterId);if (response.characterId != User.Instance.CurrentCharacter.Id)CharacterManager.Instance.RemoveCharacter(response.characterId);elseCharacterManager.Instance.Clear();}

在Character Manager中需要监听角色离开;

CharacterManager:RemoveCharacter

在角色离开的位置RemoveCharacter做通知;

 在现有的角色列表中找到离开的角色

角色离开告知OnCharacterLeave

public void RemoveCharacter(int characterId)
{Debug.LogFormat("RemoveCharacter:{0}", characterId);//this.Characters.Remove(characterId);if(this.Characters.ContainsKey(characterId)){//EntityManager.Instance.RemoveEntity(this.Characters[characterId].Info.Entity);if(OnCharacterLeave!=null)OnCharacterLeave(this.Characters[characterId]);this.Characters.Remove(characterId);}
}
 GameObjectManager:OnCharacterLeave
游戏对象管理器GameObjectManager负责游戏对象的进入;和离开

销毁掉角色;

//判断是否在角色字典中;因为游戏对有时候是在其他地方被删掉的;所以要判空

void OnCharacterLeave(Character character)
{if (!Characters.ContainsKey(character.entityId))return;if (Characters[character.entityId] != null){Destroy(Characters[character.entityId]);this.Characters.Remove(character.entityId);}
}
把GameObjectManager改成单例类
关于切换场景时游戏对象被销毁;

用单例,单例是全局的;

单例类里面有逻辑保证单例是不被销毁的

这个,改成这个

!!注意使用给单例类必须把Start函数给重载掉

//变成单例类,Start时:+

Destroy时:-

protected override void OnStart()
{//管理器在主城加载完之后启动StartCoroutine(InitGameObjects());//订阅了角色进入的事件CharacterManager.Instance.OnCharacterEnter += OnCharacterEnter;CharacterManager.Instance.OnCharacterLeave += OnCharacterLeave;
}private void OnDestroy()
{//CharacterManager.Instance.OnCharacterEnter = null;CharacterManager.Instance.OnCharacterEnter -= OnCharacterEnter; CharacterManager.Instance.OnCharacterLeave -= OnCharacterLeave; 
}
 所有单例类创建的角色要在它的子节点下

修改这个里的实例化的位置:

GameObject go = (GameObject)Instantiate(obj);

改成:

GameObject go = (GameObject)Instantiate(obj,this.transform);

//关于entityId

前面使用DBid是因为数据库里面只有角色没有怪物;到后面有怪物时就不能用DBId;因此再这里用EntityId

entityId是一种在内存中出现的每次进入游戏不同的Id;用来标识唯一性;

//GameObjectManager:

 改成这样:

 //对于某些会反复使用的代码;我们把CreateCharacterObject抽出一些代码做成InitGameObject

原来的CreateCharacterObject

private void CreateCharacterObject(Character character)
{if (!Characters.ContainsKey(character.entityId) || Characters[character.entityId] == null){Object obj = Resloader.Load<Object>(character.Define.Resource);if(obj == null){Debug.LogErrorFormat("Character[{0}] Resource[{1}] not existed.",character.Define.TID, character.Define.Resource);return;}GameObject go = (GameObject)Instantiate(obj,this.transform);go.name = "Character_" + character.entityId+ "_" + character.Info.Name;//角色实体坐标(服务器返回过来的坐标)要转换成世界坐标go.transform.position = GameObjectTool.LogicToWorld(character.position);go.transform.forward = GameObjectTool.LogicToWorld(character.direction);Characters[character.entityId] = go;//以下是绑定了两个对象的脚本,取出来并对它们赋值//当然也可以在Start里面getEntityController ec = go.GetComponent<EntityController>();if (ec != null){ec.entity = character;ec.isPlayer = character.IsPlayer;}PlayerInputController pc = go.GetComponent<PlayerInputController>();if (pc != null){if (character.entityId == Models.User.Instance.CurrentCharacter.Id){//如果是当前角色User.Instance.CurrentCharacterObject = go;MainPlayerCamera.Instance.player = go;pc.enabled = true;pc.character = character;pc.entityController = ec;}else{//不是当前玩家禁用角色控制器pc.enabled = false;}}UIWorldElementManager.Instance.AddCharacterNameBar(go.transform, character);}
}

把要创建角色,游戏体的处理部分做成init;Create在引用它

private void CreateCharacterObject(Character character)
{if (!Characters.ContainsKey(character.entityId) || Characters[character.entityId] == null){Object obj = Resloader.Load<Object>(character.Define.Resource);if(obj == null){Debug.LogErrorFormat("Character[{0}] Resource[{1}] not existed.",character.Define.TID, character.Define.Resource);return;}GameObject go = (GameObject)Instantiate(obj,this.transform);go.name = "Character_" + character.entityId+ "_" + character.Info.Name;Characters[character.entityId] = go;UIWorldElementManager.Instance.AddCharacterNameBar(go.transform, character);}this.InitGameObject(Characters[character.entityId],character);
}private void InitGameObject(GameObject go,Character character)
{//角色实体坐标(服务器返回过来的坐标)要转换成世界坐标go.transform.position = GameObjectTool.LogicToWorld(character.position);go.transform.forward = GameObjectTool.LogicToWorld(character.direction);//以下是绑定了两个对象的脚本,取出来并对它们赋值//当然也可以在Start里面getEntityController ec = go.GetComponent<EntityController>();if (ec != null){ec.entity = character;ec.isPlayer = character.IsPlayer;}PlayerInputController pc = go.GetComponent<PlayerInputController>();if (pc != null){if (character.entityId == Models.User.Instance.CurrentCharacter.Id){//如果是当前角色User.Instance.CurrentCharacterObject = go;MainPlayerCamera.Instance.player = go;pc.enabled = true;pc.character = character;pc.entityController = ec;}else{//不是当前玩家禁用角色控制器pc.enabled = false;}}
}

//原先是角色存在时做初始化逻辑;若角色进入又离开(会被删除掉)但是entityId有可能会在多次创建中删除中出现重复的;那么原先的if条件下的角色的属性就不能创建

//可能会导致角色切换回来的时候找不到原来的角色

EntityManager
GameServer:EntityManager
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Common;
using SkillBridge.Message;
using GameServer.Entities;
namespace GameServer.Managers
{class EntityManager:Singleton<EntityManager>{private int idx = 0;//用一个类似的类型维护所有entity 的列表public List<Entity> AllEntities=new List<Entity>();//某个地图的entity有哪些//一个地图id对应一张存实体的列表public Dictionary<int,List<Entity>> MapEntities=new Dictionary<int, List<Entity>>();public void AddEntity(int mapId,Entity entity){AllEntities.Add(entity);//每个entity都有唯一的idx//类似与数组模拟链表的索引entity.EntityData.Id=++this.idx;List<Entity> entities = null;if(!MapEntities.TryGetValue(mapId,out entities)){//判断mapid是那张地图//没有地图,加进去entities = new List<Entity>();MapEntities[mapId] = entities;}entities.Add(entity);}public void RemoveEntity(int mapId,Entity entity){//从总实体列表和地图实体列表移除this.AllEntities.Remove(entity);this.MapEntities[mapId].Remove(entity); }}
}
Client:EntityManager
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Entities;
using JetBrains.Annotations;
using SkillBridge.Message;
namespace Managers
{interface IEntityNofity{void OnEntityRemoved();}class EntityManager:Singleton<EntityManager>{Dictionary<int,Entity> entities = new Dictionary<int,Entity>();//用接口来实现一个事件:例如:要通知entity removed;//好处:一个接收者可以接收多种事件Dictionary<int,IEntityNofity>notifiers=new Dictionary<int, IEntityNofity>();   //在这里注册,它接收通知(删除entity的通知)public void RegisterEntityChangeNotify(int entityId,IEntityNofity nofity){this.notifiers[entityId] = nofity;}public void AddEntity(Entity entity){entities[entity.entityId] = entity;}public void RemoveEntity(NEntity entity){this.entities.Remove(entity.Id);    if(notifiers.ContainsKey(entity.Id)){notifiers[entity.Id].OnEntityRemoved();notifiers.Remove(entity.Id);}    }}
}

//这里面 的通知主要是用来通知Entity Controller ;在EntityController派生接口

并实现接口:

public void OnEntityRemoved()
{//把血条删掉;把自己删掉//服务器告知entity要删除;在这可以接收移动通知if (UIWorldElementManager.Instance != null)UIWorldElementManager.Instance.RemoveCharacterNameBar(this.transform);Destroy(this.gameObject);
}

在Start时注册:

若entity 不为空,则把方法放在这,并根据id传入RegisterEntityChangeNotify

总结:移动同步

派生接口,实现接口, 把通知注册到管理器中

那么管理器就能通过这个方式调用

//不要关心谁注册,只要知道有人注册,就可以通知

这就是通知机制;保证有人离开后对象可以删除掉

图示:

在Client:CharacterMangaer中的Add Remove Character

添加Entity Manager管理entity

public void AddCharacter(SkillBridge.Message.NCharacterInfo cha)
{Debug.LogFormat("AddCharacter:{0}:{1} Map:{2} Entity:{3}", cha.Id, cha.Name, cha.mapId, cha.Entity.String());Character character = new Character(cha);this.Characters[cha.Id] = character;//添加角色时,把角色放进管理器;因为Character是Entity的子类;Character就是EntityEntityManager.Instance.AddEntity(character);if (OnCharacterEnter != null){OnCharacterEnter(character);}
}public void RemoveCharacter(int characterId)
{Debug.LogFormat("RemoveCharacter:{0}", characterId);//this.Characters.Remove(characterId);if (this.Characters.ContainsKey(characterId)){EntityManager.Instance.RemoveEntity(this.Characters[characterId].Info.Entity);if (OnCharacterLeave != null)OnCharacterLeave(this.Characters[characterId]);//?this.Characters.Remove(characterId);}
}
Clear()//

删除时要通知给entity;注册事件的和管理器之类

在Clear时,查看当前角色列表Characters都有谁;把它们都remove

public void Clear()
{int[] keys=this.Characters.Keys.ToArray();foreach (int key in keys){//清除掉;通知事件接收者,角色离开this.RemoveCharacter(key);}this.Characters.Clear();
}
//关于minimap为空

当minmapBoundingBox或playerTransform先被删除了;当前的小地图还没被删除;小地图会为空

add:

if(minmapBoundingBox==null||playerTransform==null) return;

完整的UIMinmap:Update()

void Update()
{//if (this.playerTransform == null) playerTransform = MinimapManager.Instance.PlayerTransform;//组件与组件之间互相引用时,必须检查为空if(minmapBoundingBox==null||playerTransform==null) return;//Scale.xfloat realWidth = minmapBoundingBox.bounds.size.x;float realHeight=minmapBoundingBox.bounds.size.z;//这里用玩家相对与世界地图左下角的距离float realX = playerTransform.position.x - minmapBoundingBox.bounds.min.x;float realY = playerTransform.position.z - minmapBoundingBox.bounds.min.z;float pivotX=realX/realWidth;float pivotY=realY/realHeight;this.minimap.rectTransform.pivot=new Vector2(pivotX,pivotY);//minimap相对于父物体mask的位置this.minimap.rectTransform.localPosition = Vector2.zero;//顺着世界空间角色的y轴旋转;而箭头是xy平面轴的,因此是绕Z轴this.arrow.transform.eulerAngles = new Vector3(0, 0, -playerTransform.eulerAngles.y);
}
//关于PlayerInputController的LateUpdate

中的

//在MainPlayerCamera中

在这里;有的玩家初始化时机不同,找不到摄像机(摄像机没有在角色身上:可以修正

修正:把User.Instance.CurrentCharacterObject给player

这篇关于MMO移动同步(1)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1136689

相关文章

基于MySQL Binlog的Elasticsearch数据同步实践

一、为什么要做 随着马蜂窝的逐渐发展,我们的业务数据越来越多,单纯使用 MySQL 已经不能满足我们的数据查询需求,例如对于商品、订单等数据的多维度检索。 使用 Elasticsearch 存储业务数据可以很好的解决我们业务中的搜索需求。而数据进行异构存储后,随之而来的就是数据同步的问题。 二、现有方法及问题 对于数据同步,我们目前的解决方案是建立数据中间表。把需要检索的业务数据,统一放到一张M

服务器集群同步时间手记

1.时间服务器配置(必须root用户) (1)检查ntp是否安装 [root@node1 桌面]# rpm -qa|grep ntpntp-4.2.6p5-10.el6.centos.x86_64fontpackages-filesystem-1.41-1.1.el6.noarchntpdate-4.2.6p5-10.el6.centos.x86_64 (2)修改ntp配置文件 [r

我在移动打工的日志

客户:给我搞一下录音 我:不会。不在服务范围。 客户:是不想吧 我:笑嘻嘻(气笑) 客户:小姑娘明明会,却欺负老人 我:笑嘻嘻 客户:那我交话费 我:手机号 客户:给我搞录音 我:不会。不懂。没搞过。 客户:那我交话费 我:手机号。这是电信的啊!!我这是中国移动!! 客户:我不管,我要充话费,充话费是你们的 我:可是这是移动!!中国移动!! 客户:我这是手机号 我:那又如何,这是移动!你是电信!!

用Unity2D制作一个人物,实现移动、跳起、人物静止和动起来时的动画:中(人物移动、跳起、静止动作)

上回我们学到创建一个地形和一个人物,今天我们实现一下人物实现移动和跳起,依次点击,我们准备创建一个C#文件 创建好我们点击进去,就会跳转到我们的Vision Studio,然后输入这些代码 using UnityEngine;public class Move : MonoBehaviour // 定义一个名为Move的类,继承自MonoBehaviour{private Rigidbo

简单的角色响应鼠标而移动

actor类 //处理移动距离,核心是找到角色坐标在世界坐标的向量的投影(x,y,z),然后在世界坐标中合成,此CC是在地面行走,所以Y轴投影始终置为0; using UnityEngine; using System.Collections; public class actor : MonoBehaviour { public float speed=0.1f; CharacterCo

MySQL主从同步延迟原理及解决方案

概述 MySQL的主从同步是一个很成熟的架构,优点为: ①在从服务器可以执行查询工作(即我们常说的读功能),降低主服务器压力; ②在从主服务器进行备份,避免备份期间影响主服务器服务; ③当主服务器出现问题时,可以切换到从服务器。 相信大家对于这些好处已经非常了解了,在项目的部署中也采用这种方案。但是MySQL的主从同步一直有从库延迟的问题,那么为什么会有这种问题。这种问题如何解决呢? MyS

物联网之流水LED灯、正常流水灯、反复流水灯、移动流水灯

MENU 硬件电路设计软件程序设计正常流水LED灯反复流水LED灯移动流水LED灯 硬件电路设计 材料名称数量直插式LED1kΩ电阻杜邦线(跳线)若干面包板1 每一个LED的正极与开发板一个GPIO引脚相连,并串联一个电阻,负极接GND。 当然也可以选择只使用一个电阻。 软件程序设计 正常流水LED灯 因为要用到多个GPIO引脚,所以最好把所有的GPI

12C 新特性,MOVE DATAFILE 在线移动 包括system, 附带改名 NID ,cdb_data_files视图坏了

ALTER DATABASE MOVE DATAFILE  可以改名 可以move file,全部一个命令。 resue 可以重用,keep好像不生效!!! system照移动不误-------- SQL> select file_name, status, online_status from dba_data_files where tablespace_name='SYSTEM'

使用条件变量实现线程同步:C++实战指南

使用条件变量实现线程同步:C++实战指南 在多线程编程中,线程同步是确保程序正确性和稳定性的关键。条件变量(condition variable)是一种强大的同步原语,用于在线程之间进行协调,避免数据竞争和死锁。本文将详细介绍如何在C++中使用条件变量实现线程同步,并提供完整的代码示例和详细的解释。 什么是条件变量? 条件变量是一种同步机制,允许线程在某个条件满足之前进入等待状态,并在条件满

mysql创建新表,同步数据

import os import argparse import glob import cv2 import numpy as np import onnxruntime import tqdm import pymysql import time import json from datetime import datetime os.environ[“CUDA_VISIBLE_DEVICE