如何实现各种游戏的思路杂想,心路历程

据Bmob官方人员的透露,他们一开始产生做Game SDK的念头,主要就是因为吃鸡的火爆。

(还有一大原因是,从后台数据、客服与开发者一对一聊天获取的信息来看,使用Bmob目前服务的开发者中,需要游戏实时数据云服务的开发者占了很大的比例。)

绝地求生大逃杀

所以一开始Bmob团队开发Game SDK和Demo的时候,都是复刻 蓝洞的绝地求生 这款游戏,努力在还原所有这个正版游戏的设定。后来历经沧桑,开发到游戏 能跑了微信小游戏 又出来了,而小游戏需要实时传输数据 仅支持Web socket ,且很多人都看好在这个小游戏平台开发的休闲类、益智类、棋牌类等等轻量的游戏。这样一来,以前面基于 FPS 开发的、 强交互 的、高频处理数据 的云服务就不适用了。

微信跳一跳

于是Bmob团队把这一套服务 几乎完全推倒重来,把核心算法保留下来后,再借鉴众多游戏的思路(其实就是主程沉迷过的游戏),解剖各类型游戏的各种场景,重新开发了一套BGS系统(心疼前面那一套花掉的血与泪一秒钟)。以下以 《梦幻西游》《绝地求生》 游戏为小节,分析一下如何用BGS实现。

(笔者只是Unity开发萌新一枚,非专业游戏行业人士,如有错误,还望谅解)


梦幻西游

梦幻西游

这个游戏真是让人又爱又恨啊,想当年…

好吧我们还是从技术层面来讨论一下。

  • 首先,这绝对是一个是 非常依赖数据库 的网游,小到物品栏的摆放、刷塔获得经验,大到人物升级、任务完成,都是建立在数据库操作完成的基础上才允许客户端呈现的;
也就是说,在你网络不好的情况下,连物品栏你都调整不了摆放的次序,选中的东西会没掉,且不会移动到你指定的地方
如果你要说,这不是废话吗?哪个游戏不是非常依赖数据库?那你就错了,很多实时对战的游戏还真不需要,例如FPS,只要在战斗开始时获取一下玩家姓名和皮肤,战斗结束时记录一下战绩就可以了
  • 其次,所有玩家动作都以 服务器的计算 为准,例如玩家走路,在一定的帧数内你能走几步,都是服务器计算的;此外在战斗中,伤害的计算、经验的结算,都是由服务器计算的;挖宝图的时候,爆率都是服务器调整的;
也就是说,在你网络不好的情况下,连走路你都走不了,就算在客户端来看你迈出了几步,但是网络一恢复,你原来在哪还是在哪
如果你要说,这不是废话吗?哪个游戏不是全通过服务器来计算?那你就错了,很多实时对战的游戏还真不需要,例如FPS,玩家的位置在哪里、是否击中目标对其它玩家造成伤害,都是客户端自己说了算的,因为很多服务器没有内置整套的物理引擎,即使有,造成的服务器消耗太大,成本也太高(试想一下,在PUBG巅峰300w玩家同时在线的时候,可能有6w个房间,每个房间都要有一套物理引擎,一局据说有10万+颗子弹,每一颗子弹飞出去都需要有弹道下坠,要逐帧计算初速度和重力加速度,以及和整个地图海量的3D刚体进行碰撞检测)。这也是为什么PUBG有飞毛腿、隔山打牛、瞬移、神罗天征、万象天引等等外挂了
  • 最后,除去IM系统来讲,实时数据方面,主要是每个城镇的其它玩家分布的位置,而梦幻西游一个城镇挂机的玩家是非常多的,所以开发的时候,肯定是将每个(有移动行为的)玩家的位置同步到界面可视范围内的其它玩家那里,而不是推送给整个城镇的玩家;即便如此,在有活动的时候,同屏的玩家仍然是非常多,这个时候就会感觉什么操作都特别卡
也就是说,在你网络不好的情况下,哪怕你身边人山人海,你也可能一个人都看不到,而你在荒无人烟的野外跑商的时候,连聊天都比较流畅
如果你要说,这不是废话吗?哪个游戏不是只传递可视范围内其它玩家的信息?那你就错了,很多实时对战的游戏还真不可以,例如FPS,你朝远方、朝天边随便开一枪,也可能射死个几千米以外、无需渲染的玩家,这也是为什么透视外挂再远能够看到所有玩家的信息

梦幻西游同屏玩家


那么,如何使用 Bmob Game SDK 来实现像 梦幻西游 这样的一款网游呢?

  • 首先,全部同区的玩家都在同一个Room,这个Room不需要同步任何玩家属性
  • 其次,一定要用 Bmob Game SDK云端代码,里面 自带Bmob数据库 的操作接口,可以快速结合数据库实现很多功能
  • 最后,一定要充分利用 Bmob Game SDK云端代码,用属性监控、事件监控等功能,可以完美实现上面所说的几个特性,例如伤害计算、同屏推送等

绝地求生大逃杀

绝地求生

欢迎四排组队,本人亚服排名3w求大腿…

好吧我们还是从技术层面来讨论一下。

我觉得比较有意思的讨论点在于:

  • HP的减少,谁说了算?
  • 毒圈,或者说安全区,怎么实现

HP的控制

FPS类,或者说所有有PVP模式的游戏,都有一个共同的烦恼,就是Damage谁来判定的问题;选项无外乎三个:攻击者被攻击者服务器

  • 首先,我们排除掉单纯采信 被攻击者 判定伤害这个选项,虽然这种方式在某些场合下显得较为公平(玩家被攻击得明明白白,不会莫名其妙GG);但是这样很容易造成 高Ping大神锁血挂 的出现;而且这样的话,掉线的玩家就永远不会受伤和死亡了。
所谓高Ping大神,就是将自己的网络搞差,例如利用南辕北辙的加速器,或者本身就是"卡B",他们可以在敌人的千军万马中几进几出,毫发无伤;说极端一点,拔掉网线,就可以进入无敌状态,还附送大招CD恢复
锁血挂的实现原理有很多种,有一种就是篡改本地逻辑或拦截网包,以破坏上报自己受伤的信息,服务器永远不知道客户端受到了伤害,在其它玩家看来就相当于锁血了。
  • 其次,我们也要排除掉单纯采信 攻击者 判定的方式,虽然目前大多数FPS游戏都是这样实现的,但是隔山打牛你见过吗?回放时看到神仙对着空气来一发喷子方圆千米寸草不生的恐惧你忘了吗?
所谓隔山打牛,其实就是利用了子弹与其它玩家的碰撞检测时发送信息给服务器这一点,如果hook了这个函数,或者破解了这个信息的网包协议,随便开一枪都可以将子弹精准得"碰撞"到对方头上,因为服务器傻啊
  • 然后,还要排除单纯使用 服务器 判定的方式,因为这是一个对现实世界还原度很高的FPS游戏,不是LOL、农药那种MOBA游戏,也不是梦幻西游那样的回合制游戏。要完成模拟子弹的射出的话要求服务器有一套和客户端完全同步的物理引擎(没有渲染功能),消耗太大;此外,在玩家需要爆头击杀、远程点杀的时候,稍微一点的角度偏差也会影响最终的结果,这意味着即使是即便有足够的资源支撑服务器模拟3D世界,高Ping玩家也可能因为那一点点角度,永远都能击中但杀不死那个敌人。而PUBG的弹道下坠更是让服务器模拟起来更加吃力,毕竟不像某go是激光枪啊。
虽然上面已经讲过了,但这里为了字数再啰嗦一遍:
PUBG的玩家同时在线巅峰值是300万
假如每个房间有50个人(不要说100人,有很多房间在决赛圈呢)
就会有有6万个房间正在进行游戏
每个房间都要有一套物理引擎
每一颗子弹飞出去都需要有弹道下坠,这意味着要逐帧计算初速度和重力加速度,来确定子弹位置
子弹飞出去后,要逐帧和整个地图海量的3D刚体进行碰撞检测
Uzi射速是0.048s/发
据分析源码的人说一局据说有10万+颗子弹
  • 所以,我认为最好的判定方式是 攻击者 结合 服务器 判定的方式,攻击者上报了击中事件后,服务器计算这个上报信息是否可信,然后再做出最终判定
服务器判断可信度的方式有很多,等级也分很多种:
1. 最低级的判断,通过一些常识来实现逻辑,例如一发喷子不可能打在位置相差甚远的不同的7名玩家身上
2. 中层次的判断,丢一个二向箔,把整个坐标系拍成2D(无视y轴也就是海拔高度),在攻击的瞬间,攻击者的朝向要和被攻击者一致(角度在一定误差范围内),这个可以通过简单的三角函数计算得到
3. 略高层次的判断,服务器要包含一套地图的3D低模,当某玩家声称击中另一个玩家时,可以模拟一下激光枪是否会接触到低模(障碍物),可能会出现低频的误判

以目前的BGS内测的情况来看,做到 攻击者 结合 服务器 判定,并且加上 中层次的判断 是完全没问题的:

要求开发者写云端代码,在云端代码添加 PositionRotationBmobGameHook ,在上报玩家击中事件时,用 java.lang.Math 里面的三角函数就可以简单判断一下这次击杀是否合理了

BGS云端代码

安全区机制

安全区肯定是服务器统一确定的,并且是逐帧计算的

PUBG play zone

  • 随机圈的时候:

    1. 大圈半径减去小圈半径得到一个数字,以这个数字为半径弄一个圆
    2. 随机一个圆内的点,再加上大圈的圆心坐标点,作为下个毒圈的圆心
  • 缩圈的时候:

    1. 半径按每帧m米的速度减小
其中m = (大圈半径 - 小圈半径) / 帧数
  1. 圆心按每帧n米的速度从大圈圆心移动到小圈圆心
其中n = [(大圈圆心x - 小圈圆心x) / 帧数, (大圈圆心y - 小圈圆心y) / 帧数]

这一套完全可以用Bmob Game SDK的云端代码的 Room.onTick 实现,然后再逐帧向客户端下发当前的安全区的圆心和半径,并且结合 Player.getPosition ,计算玩家是否在圈内,如果在圈外的,调用 Player.setHp 进行减血

请注意,上面提到的 getPosition、getRotation、onAction_Damage、setHp (还有类似 onUpdate_Posision)等方法,都不是Bmob Game SDK云端代码自带的!而是开发者在设定了他游戏有hp这个属性后,在上传的 Player.java 类内,直接按这种命名规则写这个方法就可以读取、监听、修改这些属性啦,超方便的!

有什么好的建议,给我留言,或者直接去Bmob游戏开发官方群中呼唤我:
Q群:726133616

其他教程

落地成盒?Bmob帮你开发自己的联网”吃鸡”游戏

Unity联网对战游戏小Demo

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
回合制自己的 战斗DEMO 加动画状态 Q键移动并攻击。 代码很简单。不要抱有太大希望 。作为新手学习使用。 public enum HeroStatus { idle = 0, //空闲 attack, //攻击 other, //其他 hit, //受击 die, //死亡 defense, //防御 cast, //施法 miss, //闪避 seriousInjury, //重伤 move, //移动 exit, //获取下一个状态 MAX, } public class hero : MonoBehaviour { public UISpriteAnimation m_spriteAnimation = null; public HeroStatus m_staus = HeroStatus.exit; //身体碰撞盒 public BoxCollider m_bodyBC = null; public string m_name = ""; public AttackCrash m_attackCrash = null; public byte m_posIndex = 0; void Awake() { m_spriteAnimation = transform.GetComponent(); m_bodyBC = transform.GetComponent(); } // Use this for initialization void Start () { m_spriteAnimation.AddFrameCallBack("attack", 1, AttackCallBack); m_spriteAnimation.AddFrameCallBack("attack", 4, AttackCallBack); m_spriteAnimation.AddFrameCallBack("attack", 7, AttackCallBack); } public float m_speed = 1f; public float m_offset = 0.5f; public Vector3 m_destination = Vector3.zero; public List m_actionList = new List(); // Update is called once per frame void Update () { switch (m_staus) { case HeroStatus.idle: //空闲状态 if (m_actionList.Count > 0) m_staus = HeroStatus.exit; break; case HeroStatus.attack: if (!m_spriteAnimation.isPlaying) { //攻击动画播放完毕 m_staus = HeroStatus.exit; } break; case HeroStatus.defense: transform.position = Vector3.MoveTowards(transform.position, m_destination, m_speed * Time.deltaTime); if (transform.position == m_destination) m_staus = HeroStatus.exit; break; case HeroStatus.hit: transform.position = Vector3.MoveTowards(transform.position, m_destination, m_speed * Time.deltaTime); if (transform.position == m_destination) m_staus = HeroStatus.exit; break; case HeroStatus.die: break; case HeroStatus.cast: break; case HeroStatus.miss: break; case HeroStatus.seriousInjury: break; case HeroStatus.move: transform.position = Vector3.MoveTowards(transform.position, m_destination, m_speed * Time.deltaTime); if (transform.position == m_destination) m_staus = HeroStatus.exit; break; case HeroStatus.exit: //获取下一个状态 if (m_actionList.Count > 0) { string str = "idle"; switch(m_actionList[0].status) { case HeroStatus.move: str = "idle"; break; default: str = Enum.GetName(typeof(HeroStatus), m_actionList[0].status); break; } m_spriteAnimation.namePrefix = str; m_spriteAnimation.loop = m_actionList[0].loop; m_destination = m_actionList[0].destinationMove; m_speed = m_actionList[0].speed; m_spriteAnimation.ResetToBeginning(); m_staus = m_actionList[0].status; m_actionList.RemoveAt(0); } else { m_spriteAnimation.namePrefix = Enum.GetName(typeof(HeroStatus), HeroStatus.idle); m_spriteAnimation.loop = true; m_spriteAnimation.ResetToBeginning(); m_staus = HeroStatus.idle; } break; // case HeroStatus.moveback: // //transform.position = Vector3.SmoothDamp(transform.position, destinationMove, ref cameraVelocity, smoothTime); // transform.position = Vector3.MoveTowards(transform.position, destinationMove, m_speed * Time.deltaTime); // if (transform.position == destinationMove) // m_staus = HeroStatus.idle; // break; } } public void SetPosition(byte pos, float x, float y) { m_posIndex = pos; transform.localPosition = new Vector3(x, y); } public void AttackCallBack() { //创建攻击特效 A攻击B B掉血222 B反击A闪避 UnityEngine.Object sourceObj = Resources.Load("AttackCrash"); GameObject go = UnityEngine.Object.Instantiate(sourceObj) as GameObject; go.transform.parent = transform; go.transform.localScale = Vector3.one; go.transform.localPosition = new Vector3(-70, 0, 0); } private void OnCollisionEnter(Collision co) { //进入碰撞 Debug.Log("进入碰撞!"); UnityEngine.Object sourceObj = Resources.Load("Effect"); GameObject go = UnityEngine.Object.Instantiate(sourceObj) as GameObject; go.transform.parent = transform; go.transform.localScale = Vector3.one; go.transform.localPosition = new Vector3(0, 0, 0); //Defense(); Hit(); } public void Attack(GameObject aims) { ActionData tmpAD = new ActionData(); tmpAD.status = HeroStatus.move; tmpAD.loop = true; UISprite tmpS = transform.GetComponent(); tmpAD.destinationMove = GameObject.Find("UI Root/Camera").transform.TransformPoint(new Vector3(aims.transform.localPosition.x + (tmpS.width/2), aims.transform.localPosition.y)); tmpAD.speed = Vector3.Distance(transform.position, tmpAD.destinationMove) * 4; //4/1秒到达目的地 m_actionList.Add(tmpAD); ActionData tmpAD1 = new ActionData(); tmpAD1.status = HeroStatus.attack; tmpAD1.loop = false; tmpAD1.destinationMove = Vector3.zero; tmpAD1.speed = 0; m_actionList.Add(tmpAD1); ActionData tmpAD2 = new ActionData(); tmpAD2.status = HeroStatus.move; tmpAD2.loop = true; tmpAD2.destinationMove = transform.position; tmpAD2.speed = Vector3.Distance(tmpAD.destinationMove, tmpAD2.destinationMove) * 4; //4/1秒到达目的地 m_actionList.Add(tmpAD2); } public void Defense() { ActionData tmpAD = new ActionData(); tmpAD.status = HeroStatus.defense; tmpAD.loop = false; tmpAD.destinationMove = GameObject.Find("UI Root/Camera").transform.TransformPoint(new Vector3(transform.localPosition.x - 25, transform.localPosition.y)); tmpAD.speed = 0.25f; //4/1秒到达目的地 m_actionList.Add(tmpAD); ActionData tmpAD1 = new ActionData(); tmpAD1.status = HeroStatus.move; tmpAD1.loop = false; tmpAD1.destinationMove = transform.position; tmpAD1.speed = 0.8f; //4/1秒到达目的地 m_actionList.Add(tmpAD1); } public void Hit() { ActionData tmpAD = new ActionData(); tmpAD.status = HeroStatus.hit; tmpAD.loop = false; tmpAD.destinationMove = GameObject.Find("UI Root/Camera").transform.TransformPoint(new Vector3(transform.localPosition.x - 25, transform.localPosition.y)); tmpAD.speed = 0.3f; //4/1秒到达目的地 m_actionList.Add(tmpAD); ActionData tmpAD1 = new ActionData(); tmpAD1.status = HeroStatus.move; tmpAD1.loop = false; tmpAD1.destinationMove = transform.position; tmpAD1.speed = 0.8f; //4/1秒到达目的地 m_actionList.Add(tmpAD1); } public void Idle() { m_spriteAnimation.namePrefix = Enum.GetName(typeof(HeroStatus), HeroStatus.idle); m_spriteAnimation.loop = true; m_spriteAnimation.ResetToBeginning(); m_staus = HeroStatus.idle; } public void Move() { // //transform.GetComponent().depth = 99; // GameObject go = GameObject.Find("enemy").gameObject; // destinationMove = GameObject.Find("UI Root/Camera").transform.TransformPoint(new Vector3(go.transform.localPosition.x + 98,go.transform.localPosition.y,go.transform.localPosition.z)); // m_speed = Vector3.Distance(transform.position, destinationMove) * 4; //4/1秒到达目的地 // m_staus = HeroStatus.moveto; // m_spriteAnimation.namePrefix = Enum.GetName(typeof(HeroStatus), HeroStatus.idle); // m_spriteAnimation.loop = true; // m_spriteAnimation.ResetToBeginning(); }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值