多人即时战斗游戏服务端系列[2]--90坦克Online游戏对象介绍以及渲染机制
先上类图,略大,点击此处放大:
1.先说下方接口
1.1 场景物品接口 ISceneObject : OpLog.IOpItem, IStackPoolObject
全部场景对象的基本接口,包含类型定义,通用渲染接口,所在场景,子对象树,尺寸,坐标等..
1.2 游戏场景接口 IScene : ISceneObject
继承于基本场景接口,拥有加入对象,对象列表,获取相邻对象,等其它逻辑.
1.3 Buff基类 IBuff
buff表现,拥有持续时间,加入/删除/移动/开火/渲染/被击中时触发事件
1.4 爆裂物接口 IHitSceneObject
假设须要造成伤害/或者破坏物体,都须要先拥有一个爆裂物,爆裂物会检測爆裂范围内的接口,然后逐个进行碰撞.
1.5 可移动物品接口 IMoveSceneObject
拥有方向,速度,以及射程,继承此接口对象,当你设置一个速度,在每帧渲染时,都会移动,直到收到停止或最大可移动距离
1.6 可加入Buff对象 ICanBuffSceneObject
继承此接口物品可加入/移除buff,并受ibuff事件触发.
1.7 可使用装置接口(主动/被动技能) IDevice
分为2类一般,发射一个子弹,或者加入一个buff,或者两者都有
1.8 地图上可获取道具接口 IItem
仅參与碰撞,被碰撞后会对碰撞对象加入一个触发buff.
2.然后是上方buff,全部的对象改动功能大部分都是buff触发 无敌/武器升级/无法移动/减速/回血等..
3武器 装置和buff的关系
3.1 全部的buff有个持续时间,有一到两个可选參数.
3.2 全部武器都会发射子弹,全部子弹都会创建一个爆裂物,爆裂物能够对碰撞对象产生伤害和加入buff
3.3 全部子弹/爆裂物都有碰撞列表,仅会对有效的目标进行伤害和加入buff
4 游戏世界 GameWorld
4.1 每场游戏都是一个游戏世界
4.2 每一个游戏时间包括一个场景
4.3 每帧这个世界会进行活动.通过fpstimer
4.4 不同的游戏世界有不同的游戏模式,通过继承GameWorld
5.渲染
5.1 基类实现,检查是否有子对象,然后进行子对象渲染(调用一次 ,对象有渲染等级,也就是渲染优先级,有些每帧渲染,有些每5帧渲染以此类推)
/// <summary> /// 渲染 /// </summary> /// <param name="ms"></param> public virtual void Render(int ms, int lv) { if (SubObjects != null && SubObjects.Count > 0) { Assert.IsNull(SubObjects, "SubObjects"); foreach (var sceneObject in SubObjects) { sceneObject.Render(ms, lv); } } }5.2 场景渲染方法 (管理加入物体和移除物体)
/// <summary> /// 渲染全部物体 /// </summary> /// <param name="ms"></param> public override void Render(int ms, int lv) { base.Render(ms, lv); Assert.IsNull(SceneObjects, "SceneObjects"); #if TEST var sw = new System.Diagnostics.Stopwatch(); sw.Start(); #endif //加入延迟对象 if (DelaySceneObjects.Count > 0) { lock (DelaySceneObjects) { var removeList = new List<DelaySceneObject>(); foreach (var sceneObject in DelaySceneObjects) { if (sceneObject != null && sceneObject.ActiveTime < CurrentTimeGetter.Now) { removeList.Add(sceneObject); if (OnAddSceneObject == null || OnAddSceneObject(sceneObject.SceneObject)) AddObject(sceneObject.SceneObject); } } foreach (var delaySceneObject in removeList) { DelaySceneObjects.Remove(delaySceneObject); } } #if TEST if (sw.ElapsedMilliseconds > 20) logger.Debug("DelaySceneObjects Add:" + sw.ElapsedMilliseconds); #endif } var list = RenderList; if (OthersLv.Count > 0) { list = RenderList.Where(r => !(r is OtherSceneObject) || (r as OtherSceneObject).Lv <= 0 || lv % (r as OtherSceneObject).Lv == 0).ToList(); } foreach (var sceneObject in list) { //Assert.IsNull(sceneObject, "sceneObject"); if (sceneObject != null && sceneObject.Hp > 0) { sceneObject.Render(ms, lv); } else { SceneObjects.Remove(sceneObject); } } }
5.3 游戏场景渲染
/// <summary> /// 渲染事件 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void timer_Elapsed(object sender, FpsTimer.FpsElapsedEventArgs e) { if (ThreadNum == 0) { ThreadNum = System.Threading.Thread.CurrentThread.ManagedThreadId; } else { if (System.Threading.Thread.CurrentThread.ManagedThreadId != ThreadNum) { #if DEBUG System.Diagnostics.Debugger.Break(); #endif } } #if !TEST try { #endif if (!IsStart) { if ((CurrentTimeGetter.Now - StartTime).TotalMilliseconds > TankConfigs.AutoStartGameSleep) { IsStart = true; Statues = GameStatues.Start; CurrentScene.AddMessage(new Message() { Type = MessageType.RoundStart, Content = CurrentTimeGetter.Now + ":回合開始:" + OpId }); } } if (!IsStart) { return; } #if DEBUG long lastms = sw.ElapsedMilliseconds; #endif Ms += (int) e.ElapsedMilliseconds; if (CurrentTimeGetter.Now.Second != lastReaderTime.Second) { if (Math.Abs((Avg - TankConfigs.RenderFps)) > TankConfigs.RenderFps/5 || Math.Abs((Ms - 1000)) > 200) logger.Warn("[" + OpId + "]Fps:" + Avg + " MS:" + Ms); Ms = Avg = 0; } lastReaderTime = CurrentTimeGetter.Now; LeftMs += (int)e.ElapsedMilliseconds; this.BeforeRender(); for (int i = 0; i < (LeftMs / (TankConfigs.RenderTimer)); i++) { Avg++; if (CurrentScene != null) { CurrentScene.Render((int)(TankConfigs.RenderTimer), LoopSeed++ % 100); } LeftMs -= (int) (TankConfigs.RenderTimer); } if (LeftMs >= TankConfigs.RenderTimer) { Avg++; if (CurrentScene != null) { CurrentScene.Render((int)(LeftMs), LoopSeed++ % 100); } LeftMs = 0; } this.AfterRender(); if (Time <= 0) { Close(); } #if DEBUG if (sw.ElapsedMilliseconds - lastms > TankConfigs.RenderTimer*5) { logger.Warn("渲染时间过长:" + (sw.ElapsedMilliseconds - lastms)); } #endif #if !TEST } catch (Exception ex) { logger.ErrorException(ex, "渲染出错!"); ErrorCount++; if (ErrorCount >= TankConfigs.MaxReaderErrorCount) { RoundEnd(0); } } #endif }
保证每秒能渲染到设定的帧数60fps,过快或者过慢,过慢进行渲染补偿(连续渲染,过快进行跳过,等待下秒)
这里并不採用每一个世界一个线程的渲染方式,而是使用线程池,相应一个线程渲染几个特定的游戏世界,用于避开一些多线程锁操作.
6.玩家操作流程
6.1 玩家命令会进入相应操作的坦克队列
/// <summary> /// 开火 /// </summary> /// <param name="index">武器序号</param> /// <param name="arg">投掷參数1-100</param> public void Fire(int index = 0, int arg = 0) { if (CheckStatues()) { if (Tank.Devices.Count > index) { Tank.EnqueueCmd(new Tank.TankCmd(Tank.TankCmd.OpCmd.Fire, index, arg)); } } }
6.2 坦克会在渲染时运行队列中的命令
public override void Render(int ms, int lv) { while (true) { TankCmd cmd = null; Cmds.TryDequeue(out cmd); if (cmd != null) { switch (cmd.Cmd) { case TankCmd.OpCmd.Fire: FireOnWeapon(Devices[(int) cmd.Args[0]], (int) cmd.Args[1]); break; case TankCmd.OpCmd.Move: MoveOn((Direction) cmd.Args[0]); break; case TankCmd.OpCmd.StopMove: Block(); break; } } else { break; } } base.Render(ms,lv); //检查装置CD foreach (var device in Devices) { device.Render(ms); } //Buff渲染触发 foreach (var buff in Buffs) { buff.OnReader(this, ms); } //移除时间已经到了的buff foreach (var buff in Buffs) { if (CurrentTimeGetter.Now > buff.EndTime) { buff.OnBuffRemove(this); } } }