在游戏开发中,常见的经典错误如下:
策划人员把表配错了,程序运行报错。
美术人员制作的模型缺少规定的挂载点,或者起名不规范,程序运行后,找不到就报错了。
想要彻底解决这类问题,需要给美术或策划人员提供资源检查工具
主动检查工具 程序人员开发一套工具来检查资源的规范性,策划人员或者美术人员在需要提交资源之前,运行此工具,如果资源不符合规范,就会输出错误日志,接着修改好后再重复检查,直到没问题后再提交资源。
被动检查工具 被动检查工具就是依赖Unity监听资源导入事件、程序来动态分析资源的合理性。这种检查方法比主动检查工具更安全了,因为资源只要拖入就会自动检查,不需要使用者主动操作,这样就不会有遗漏检查的情况。
导出类检查工具 无论是主动检查还是被动检查,其实最核心的问题是,策划人员和美术人员可以跳过检查直接提交SVN。理论上,各种资源都可能被提交。所以要想彻底解决这个问题,程序员就不能直接使用他们提交的资源,而采取间接使用的方式。
程序提供一个导出工具,即根据美术资源来自动生成一个新资源(程序用的资源),导出的同时做资源检查,如果资源不符合规范,那么提示错误并且不生成新资源。因为程序使用的是导出后的资源,所以错误的资源并不能被导出新的,也就不存在上传错误的情况,此时这个问题就可以彻底解决了。数据表、UI界面Prefab、模型Prefab、场景Scene都可以导出,导出的时候不仅检查错误,还可以智能地修改一些资源,比如自动生成UI代码,自动删除场景中没有的模型等。
导出UI 在Hierarchy视图中做好UI后,可以给UI的顶层节点添加一个自定义的Tag,使用一会介绍的方法导出时会以它的父节点自动生成Prefab以及部分代码。
在这里先定义顶层节点的tag为“UIView”
然后将需要代码访问的UI组件,比如说Text、Image、Button等添加新的自定义tag,在这里定义为“UIProperty”
在Project窗口中,在Assets文件夹内新建UI文件夹,在UI文件夹内部再分别新建UIBase文件夹和UIPrefab文件夹,它们分别用来存储自动导出时生成的UI基类和Prefab,然后新建一个UIBase.cs.txt文件,用来作为UI基类的代码模板。
UIBase.cs.txt模板
1 2 3 4 5 6 7 8 9 10 11 12 13 using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; public class #Name# : MonoBehaviour{ #Property# void Awake(){ #Get# } }
使用下面的代码,用来自动生成Prefab和对应的UI基类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 using System.Collections;using System.Collections.Generic;using UnityEngine;using UnityEditor;using System.Text;using UnityEngine.UI;using System.IO;public class ExportUIPrefab { [MenuItem("UI/Tools/Export" ) ] static void ExportUI () { string prefabPathRoot = "Assets/UI/UIPrefab" ; string scriptPathRoot = "Assets/UI/UIBase" ; if (Selection.activeTransform){ if (Selection.activeGameObject.tag == "UIView" ){ string prefabPath = string .Format("{0}/{1}{2}" ,prefabPathRoot,Selection.activeGameObject.name,".prefab" ); GameObject prefab = AssetDatabase.LoadAssetAtPath<GameObject>(prefabPath); PrefabUtility.SaveAsPrefabAssetAndConnect(Selection.activeGameObject,prefabPath,InteractionMode.UserAction); string basePath = string .Format("{0}/{1}{2}{3}" ,scriptPathRoot,"Base" ,Selection.activeGameObject.name,".cs" ); string script = AssetDatabase.LoadAssetAtPath<TextAsset>("Assets/UI/UIBase.cs.txt" ).text; StringBuilder property = new StringBuilder(); StringBuilder get = new StringBuilder(); Dictionary<string ,int > uniqueName = new Dictionary<string , int >(); HashSet<string > uniquePath = new HashSet<string >(); foreach (var transform in Selection.activeGameObject.GetComponentsInChildren<Transform>(true )) { if (transform.tag == "UIProperty" ) { Component component = transform.GetComponent<Button>(); if (component == null ) component = transform.GetComponent<Image>(); if (component == null ) component = transform.GetComponent<Text>(); if (component == null ) component = transform.GetComponent<RectTransform>(); if (component != null ){ string propertyName = component.name.ToLower(); int uniqueValue; if (uniqueName.TryGetValue(propertyName,out uniqueValue)){ uniqueValue = uniqueName[propertyName] += 1 ; }else { uniqueValue = uniqueName[propertyName] = 0 ; } if (uniqueValue > 0 ){ propertyName = string .Format("{0}_{1}" ,propertyName,uniqueValue); } property.AppendFormat("\tprotected {0} m_{1};" ,component.GetType().ToString(),propertyName).AppendLine(); string findPath = AnimationUtility.CalculateTransformPath(component.transform,Selection.activeTransform); if (!uniquePath.Contains(findPath)) uniquePath.Add(findPath); else Debug.LogErrorFormat("读取路径发生重复: {0}" ,findPath); get .AppendFormat("\t\tm_{0}=transform.Find(\"{1}\").GetComponent<{2}>();" ,propertyName,findPath,component.GetType().ToString()).AppendLine(); } } } script = script.Replace("#Name#" ,Path.GetFileNameWithoutExtension(basePath)); script = script.Replace("#Property#" ,property.ToString()); script = script.Replace("#Get#" ,get .ToString()); File.WriteAllText(basePath,script); AssetDatabase.Refresh(); } } } }
然后我们在Scene里面选中“UIMain”对象,然后在最上方导航栏中选择UI——Tools——Export
接在我们就能看见导出的Prefab和基类
基类中的代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 using System.Collections;using System.Collections.Generic;using UnityEngine;using UnityEngine.UI;public class BaseUIMain : MonoBehaviour { protected UnityEngine.UI.Button m_button; protected UnityEngine.RectTransform m_gameobject; protected UnityEngine.RectTransform m_gameobject_1; void Awake () { m_button=transform.Find("Button" ).GetComponent<UnityEngine.UI.Button>(); m_gameobject=transform.Find("GameObject" ).GetComponent<UnityEngine.RectTransform>(); m_gameobject_1=transform.Find("GameObject" ).GetComponent<UnityEngine.RectTransform>(); } }
既然使用到了基类,那肯定是要继承的,这样导出就是为了在子类中更方便地访问需要交互的UI元素
1 2 3 4 5 6 7 8 9 10 using UnityEngine;public class UIMainTest : BaseUIMain { private void Start () { this .m_button.onClick.AddListener(() => { Debug.Log(gameObject.name); }); } }
导出模型 例如,程序员和美术人员约定,所有模型都必须要有effect_0和effect_1这两个挂载点。我们可以做工具来帮助美术人员生成Prefab,同时可以检查资源是不是没有这两个挂载点。如果美术人员没有提供挂载点或者挂载点的名字写错了,那么Prefab就不会被导出了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 using System.Collections.Generic;using UnityEngine;using UnityEditor;using System.IO;public class ExportModelPrefab { [MenuItem("UI/Tools/Export" ) ] static void ExportModel () { string prefabPathRoot = "Assets/Model" ; List<string > checkPoint = new List<string >() { "effect_0" , "effect_1" }; if (Selection.activeTransform) { foreach (var transform in Selection.activeTransform.GetComponentsInChildren<Transform>(true )) { for (int i = 0 ; i < checkPoint.Count; i++) { if (transform.name == checkPoint[i]) checkPoint.RemoveAt(i); } } if (checkPoint.Count > 0 ) Debug.LogErrorFormat("缺少挂载点{0} 不予导出" , string .Join("," , checkPoint.ToArray())); else { string prefabPath = Path.ChangeExtension(Path.Combine(prefabPathRoot, Selection.activeGameObject.name), "prefab" ); PrefabUtility.SaveAsPrefabAssetAndConnect(Selection.activeGameObject, prefabPath, InteractionMode.UserAction); } } } }
不参与保存对象 当美术人员做好场景以后,可能需要放进去一些摄像机以及一些模型来作为参照物,这样他们可以检查场景做得是否正确。如果一不小心这些参照物的游戏对象也被保存在在场景上,那么导出就肯定不正确。
Unity内置EditorOnly的Tag,使用此Tag的对象不会被打包导出
导出场景 我们可以和美术人员约定一个节点。这里导出一份新场景。其中只保留Map节点下的元素,别的节点统统删除。另外,Map节点下还有static节点,此节点下的对象导出时自动全部设置static属性,为了避免美术人员忘记勾选。
应用下面的代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 using System.Collections.Generic;using UnityEngine;using UnityEditor;using System.IO;using UnityEditor.SceneManagement;using UnityEngine.SceneManagement;public class ExportScenePrefab : MonoBehaviour { [MenuItem("Tools/Map/ExportScene" ) ] static void ExportSceneMap () { string scenePath = EditorSceneManager.GetActiveScene().path; string sceneName = System.IO.Path.GetFileName(scenePath); string generateScenePath = Path.Combine("Assets/ExportScenes/" , sceneName); if (AssetDatabase.CopyAsset(scenePath, generateScenePath)) { EditorSceneManager.SaveOpenScenes(); Scene baseScene = EditorSceneManager.OpenScene(generateScenePath); if (!GameObject.Find("Map" )) { Debug.LogError("场景缺失Map节点,不予导出" ); return ; } foreach (var gameObject in GetRootGameObjects ()) { if (gameObject.name != "Map" ) GameObject.DestroyImmediate(gameObject); } GameObject staticGameObject = GameObject.Find("Map/static" ); if (staticGameObject) { foreach (var transform in staticGameObject.GetComponentsInChildren<Transform>(true )) { transform.gameObject.isStatic = true ; } } EditorSceneManager.SaveScene(baseScene); EditorSceneManager.OpenScene(scenePath); } } static private List<GameObject> GetRootGameObjects () { GameObject[] pAllObjects = Resources.FindObjectsOfTypeAll<GameObject>(); List<GameObject> list = new List<GameObject>(); foreach (GameObject pObject in pAllObjects) { if (pObject.transform.parent) continue ; if (pObject.hideFlags == HideFlags.NotEditable || pObject.hideFlags == HideFlags.HideAndDontSave) continue ; if (Application.isEditor) { string sAssetPath = AssetDatabase.GetAssetPath(pObject.transform.root.gameObject); if (!string .IsNullOrEmpty(sAssetPath)) continue ; } if (!pObject.transform.parent && GameObject.Find(pObject.name)) { if (!list.Contains(pObject)) list.Add(pObject); } } return list; } }
导出场景后,自动删除无用节点并且保留Map节点,static节点下的游戏对象将标记Static属性。另外,复制的对象会保留原场景的烘焙以及寻路信息,无需执行复制他们的操作
过滤无用场景 只要场景被添加到Build Settings面板中,打包就会带上这个场景。由于场景的制作是阶段性的,美术人员会陆陆续续地制作,有一些开发中的场景可能会添加在Build Settings中,需要注意。另外,一般情况下策划人员并不会接触所有场景,而是全部在数据表中配置场景,打包之前可以读取这个表,提取出来需要用到的场景,过滤掉哪些无用场景。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 using UnityEngine;using UnityEditor;public class SetBuildSettingScenes : MonoBehaviour { [MenuItem("Tools/Map/SetScenes" ) ] static void SetScene () { UnityEditor.EditorBuildSettings.scenes = new EditorBuildSettingsScene[] { new EditorBuildSettingsScene("Assets/Scene.unity" ,true ), new EditorBuildSettingsScene("Assets/Scene1.unity" ,true ) }; } }