Unity上移动设备的大城市。 开发和优化经验



哈勃! 在本出版物中,我想分享开发具有大城市和大流量的大型手机游戏的经验。 该出版物中描述的示例和技术并不声称被称为参考和理想。 我不是认证专家,也不希望重复自己的经验。 游戏的目标是获得有趣的体验,并获得具有开放世界的优化游戏。 在开发过程中,我试图尽可能简化代码。 不幸的是,我没有使用ECS,但是犯了单身罪。

游戏


以黑手党为主题的游戏。 在游戏中,我尝试重新创建30-40。 本质上,游戏是第一人称经济策略。 玩家抓住了业务,并试图使其保持生计。
实施:汽车交通(交通信号灯,避撞灯),人流,酒吧,赌场,俱乐部,球员的公寓,购买套装,更换套装,购买/涂漆/加油,警察,安全/黑帮,经济学,出售/购买资源。

建筑学


图片

很遗憾我没有使用ECS,而是尝试骑自行车。 最后,一切都变得繁琐且过于依赖。 该应用程序具有一个入口点-应用程序(go)游戏对象,同名的Application类挂在该对象上。 他负责预加载数据库,填充池和初始设置。 另外,其他几个单例管理器组件类也落在应用程序的肩膀上(转到)。

  • 音频经理
  • UIManager
  • 输入经理

我狂热地尝试创建这样一种体系结构,在该体系结构中我可以从管理器管理各种组件。 例如,AudioManager管理所有声音,UIManager包含所有UI元素和管理方法。 所有输入都使用事件和委托通过InputManager处理。

简化的AudioManager。 它允许您向游戏对象添加尽可能多的音频组件,并在必要时播放声音:

public class AudioManager : MonoBehaviour { public static AudioManager instance = null; //  public AudioClip metalHitAC; //   private AudioSource metalHitAS; //    public bool isMetalHit = false; private void Awake() { if (instance == null) instance = this; else if (instance == this) Destroy(gameObject); } void Start() { metalHitAS = AddAudio(metalHitAC, false, false, 0.3f, 1); } void LateUpdate() { if (isMetalHit) { metalHitAS.Play(); isMetalHit = false; } } AudioSource AddAudio(AudioClip clip, bool loop, bool playAwake, float vol, float pitch) { var newAudio = gameObject.AddComponent<AudioSource>(); newAudio.clip = clip; newAudio.loop = loop; newAudio.playOnAwake = playAwake; newAudio.volume = vol; newAudio.pitch = pitch; newAudio.minDistance = 10; return newAudio; } public AudioSource AddAudioToGameObject(AudioClip clip, bool loop, bool playAwake, float vol, float pitch, float minDistance, float maxDistance, GameObject go) { var newAudio = go.AddComponent<AudioSource>(); newAudio.spatialBlend = 1; newAudio.clip = clip; newAudio.loop = loop; newAudio.playOnAwake = playAwake; newAudio.volume = vol; newAudio.pitch = pitch; newAudio.minDistance = minDistance; newAudio.maxDistance = maxDistance; return newAudio; } } 

在启动时,AddAudio方法会添加一个组件,然后我们可以从任何地方播放所需的声音:

 AudioManager.instance.isMetalHit = true; 

在此示例中,将oneshot回放放到该方法中会更明智。

简化的InputManager如下所示:

 public class InputManager : MonoBehaviour { public static InputManager instance = null; public float horizontal, vertical; public delegate void ClickAction(); public static event ClickAction OnAimKeyClicked; //public delegate void ClickActionFloatArg(float arg); //public static event ClickActionFloatArg OnRSliderValueChange, OnGSliderValueChange, OnBSliderValueChange; public void AimKeyDown() { OnAimKeyClicked(); } } 

将AimKeyDown方法放在按钮上 ,并在OnAimKeyClicked上签名武器控制脚本:

 InputManager.instance.OnAimKeyClicked += GunShot; 

我的整个输入系统以类似的方式实现。 我没有发现速度方面的任何问题。 这使我们可以将所有单击处理程序收集在一个位置-InputManager。

最佳化


让我们继续进行最有趣的事情。 对于初学者而言,Unity中的优化主题是痛苦的,并充满许多陷阱。 我将分享我正在处理的事情。

1.组件缓存(从简单的基础开始)

通常,在Toster上,何时在Update中使用GetComponent时,您会遇到一些带有示例的问题。 您无法执行此操作,GetComponent正在对象上寻找组件。 此操作很慢,并且在Update中导致它,您可能会失去宝贵的FPS。 这是组件缓存的很好解释。

2.使用SendMessage

使用SendMessage()比GetComponent()慢。 SendMessage使用字符串比较来遍历每个脚本以查找具有所需名称的方法。 GetComponent通过类型比较查找脚本,然后直接调用该方法。

3.对象标签比较

使用CompareTag方法而不是obj.tag ==“ string”。 在Unity中,从游戏对象中提取字符串会创建一个重复的字符串,这会将工作添加到垃圾收集器中。 最好避免获取游戏对象的名称。 您不能在Update中调用CompareTag以及读取繁重的操作。

4.材料

材料越少越好。 尽可能减少材料量。 为了达到这个目的,请帮助缎纹。 例如,在我的游戏中,几乎整个城市都由2-3个地图集组成。 应该注意的是,并不是所有的移动设备都能够使用大型地图集。 因此,如果您要支持11-13岁的设备,则值得考虑。 我决定拒绝支持5.1以下的android系统,因为这些设备大多数都是旧设备。 此外,由于线性渲染,该游戏可在OpenGL 3.x上运行。

5.物理

将FPS降低到10很容易。事实证明,即使静态对象也会相互作用并参与计算。 我错误地认为静态物理对象(具有RigidBody组件的对象)在需要时完全是被动的。 我被旧教程误导了,旧教程说,只要有对撞机,就应该有RigidBody。 现在我所有的静态对象都是Static + BoxCollider。 在需要物理的地方,例如可以拆下来的灯柱,我认为如有必要,可以减少RigidBody组件。

图层是优化的生命线。 使用图层禁用不必要的交互。 重铸时,请使用图层蒙版。 为什么我们需要额外的错误计算? 请记住,如果您的对象具有复杂的对撞机网格,并且用射线对其进行射击,最好创建一个简单的父对撞机来“捕捉”射线。 对撞机越复杂,计算错误就越多。

6.遮挡剔除+ Lod

对于较大的场景,遮挡剔除是必不可少的。 为了远距离禁用对象(树木,电线杆等),我使用Lod。

图片

图片

7.对象池

我发现的对象池的所有现成实现都使用实例化。 他们还删除和创建对象。 我害怕实例化所有形式。 缓慢的操作会使游戏停滞不前,或多或少都有大物体。 我决定走一条简单而快捷的道路-我的整个筹码池都以物理游戏对象的形式存在,我可以在需要时将其关闭然后再打开。 它达到了RAM,但是更好。 适用于现代设备的RAM从1GB起,游戏消耗300-500 MB。

用于管理战斗机器人的简单池:

  public List<Enemy> enemyPool = new List<Enemy>(); private void Start() { //    Enemy Transform enemyGameObjectContainer = Application.instance.objectPool.Find("Enemy"); //  enemyPool  for (int i = 0; i < enemyGameObjectContainer.childCount; i++) { enemyPool.Add(new Enemy() { Id = i, ParentRoomId = 0, GameObj = enemyGameObjectContainer.GetChild(i).gameObject }); } } public void SpawnEnemyForRoom(int roomId, int amount, Transform spawnPosition, bool combatMode) { //Stopwatch sw = new Stopwatch(); //sw.Start(); foreach (Enemy enemy in enemyPool) { if (amount > 0) { if (enemy.ParentRoomId == 0 && enemy.GameObj.activeSelf == false) { // id   enemy.ParentRoomId = roomId; enemy.GameObj.transform.position = spawnPosition.position; enemy.GameObj.transform.rotation = spawnPosition.rotation; enemy.AICombat = enemy.GameObj.GetComponent<AICombat>(); enemy.AICombat.parentRoomId = roomId; // id  enemy.AICombat.id = enemy.Id; //   enemy.GameObj.SetActive(true); //      if (combatMode) enemy.AICombat.ActivateCombatMode(); amount--; } } if (amount == 0) break; } } 

资料库


我将sqlite用作数据库-方便快捷。 数据以表格的形式呈现,您可以进行复杂的查询。 在用于数据库的类中,当800行时。 我无法想象XML / JSON中的外观。

未来的问题和计划


为了从城市转移到“房间”,我选择了“远程办公”。 播放器靠近门,场景室已装满,播放器被传送。 这使您不必在城市中保留房间。 如果您在城市中安装了15个房间,并且有填充空间,那么内存消耗将至少增加1GB。 我不喜欢这种实现方式,它不现实,并且施加了很多限制。 Unity最近展示了其Megacity的演示,令人印象深刻。 我想逐步将游戏转移到esc,并使用Megacity的技术来加载建筑物和房屋。 这是一次有趣而有趣的经历,我认为它将变成一个真正充满活力的城市。 为什么不使用异步加载场景 ? 很简单,它不起作用,在2018.3版本中没有开箱即用的异步加载场景。 最初,我希望在规划城市时使用异步加载场景,但事实证明,在大型场景中,它会像常规加载场景一样冻结游戏。 这已在Unity论坛上得到确认,您可以绕开,但是需要拐杖。

一些统计:

纹理:304 / 374.3 MB
网格:295 / 304.0 MB
资料:101 / 148.0 KB(此处可能有差异)
动画剪辑:24 / 2.8 MB
音频剪辑:22 / 30.3 MB
资产:21761
场景中的GameObjects:29450
场景中的对象总数:111645
对象总数:133406
每帧的GC分配:70 / 2.0 KB
总共4800行C#代码。

有人告诉我,这样的游戏可以在一周内完成。 也许我效率不高,也许这个人很有才华,但对我自己来说我明白一件事-很难独自制作这样的游戏。 我想在随意的“手指”的背景下创造一些有趣的东西,在我看来,我实现了自己的梦想。

您可以运行一个公开的Beta测试,并在此处进行测试: play.google.com/store/apps/details?id=com.ag.mafiaProject01 (如果该程序集无法正常工作,则需要稍加欣赏,每晚都有更新。) 我希望这不会被视为广告链接,因为此测试版和下载不会给我带来评分和分红。 另外,我不认为habr是我游戏的目标受众。

屏幕截图:



Source: https://habr.com/ru/post/zh-CN439220/


All Articles