欢迎光临散文网 会员登陆 & 注册

【Unity】工具类系列教程——配置化和规范流程

2017-10-23 17:43 作者:皮皮关做游戏  | 我要投稿

前言:

程序员是看重效率的群体,如果凡事都事必躬亲,一行一行的码代码,进行重复的劳动,最后只会沦为码农(知乎很多大神都用这个自称,这个词不是褒义词啊!)。


我们想象一下这个场景:

策划:这个场景的出场位置太远了,调近一点点
程序:(卧槽,还有很多新需求还没解决,这货又来!)好,我等下就调。
过了一会……
策划:角色位置还是不对,你再调远一点点?
程序:……
反复几次后,程序暴走
程序:还做不做新功能了!?
(还是不敢不改啊)
还有这种场景:
程序:美术大爷,你这个角色的动画丢失了。
美术:好的,我改一下
一会后……
程序:美术大爷,动画是没丢失了,但是却没设置成循环……,您?您再改一下?
美术:怎么会?!好的……
过了几天,这个情况依然还会上演。
很多项目遇到的时间消耗,其实都在“举手之劳”上,最后程序、美术、策划三方互相都有怨气。但是如果一个项目里面有规范流程化的工具。
比如程序策划交流场景变成了这样
程序:这个场景的出生点坐标我已经暴露在这个脚本上了,你直接在场景里面随便调,调好了点这个上传就行,有BUG或者不懂的再问我。
策划:好的大爷。
程序美术交流的场景是这样:
程序:你每次上传美术资源的时候,点一下菜单栏的这个按钮,它会把你丢失或者不对的都报错出来。
美术一查,就看到了所有美术资源出错的地方。

因此引出这篇教程的主题,配置化和规范流程。


ScriptableObject

做游戏配置有很多方法,有Excel保存,有导出Json、TXT,这里对Unity自带ScriptableObject序列化方法配置做介绍。

(如果对配置有兴趣的朋友可以去试试LitJson将类导出成Json格式,或者自己试着写格式)

开始具体使用前,我们普及几个概念。

ScriptableObject 有什么好处?

1.Unity用于创建不需要绑定到物体的对象,即非继承于Mono,该类继承于UnityEngine.Object

2.存放编辑器或数据配置文件

3.方便操作管理,可以可视化编辑

4.取数据方便,ScriptableObject已经是可序列化的数据,不用像读取纯文本或xml那样还要繁琐耗时的数据解析过程

(当然也有坏处,如果不进行Editor编写变量,可读性其实很低,而且它和代码绑定,如果配置类的代码修改,序列化的数据就会丢失,但是总的来说不使用其他插件的情况下,用ScriptableObject 来学习游戏内容配置是不错的)

序列化和反序列化的概念

把对象转换为字节序列的过程称为对象的序列化;把字节序列恢复为对象的过程称为对象的反序列化。

/*强调一下,数据解析和序列化目的是一致的,就是将不可用转换为可用,但是实际的方式方法不同*/


我们先做一个ScriptableObject的 数据类


using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class sceneConfigObject : ScriptableObject
{
   /// <summary>
   /// 配置名字
   /// </summary>
   public string mIndex;
   /// <summary>
   /// 出生点位置
   /// </summary>
   public Vector3 spawnPos;   
}

这个脚本是不能直接挂载到物体上的,只有继承了mono类的脚本才能够。



然后为了我们直观的修改数据,我们用一个mono脚本做中间层。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class SceneConfig : MonoBehaviour
{
   public sceneConfigObject mInfo;
}

现在我们可以将SceneConfig 挂载物体上,但是显示并不是我们想要看到的数据。



我们如果要让Unity显示出来我们要编辑的数据,就比如去修改它显示的内容。而我们如何去自定义化脚本的显示内容呢?

这里就需要UnityEditor扩展编辑器功能。


编写Editor代码:


using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
using System.IO;

[CustomEditor(typeof(SceneConfig))]
public class SceneConfigEditor : Editor
{

   SceneConfig mScript;

   /// <summary>
   /// 脚本激活的时候进入,target就是对应[CustomEditor(typeof(SceneConfig))]的SceneConfig类
   /// </summary>
   public void OnEnable()
   {
       mScript = target as SceneConfig;
       if (mScript.mInfo == null)
       {
           mScript.mInfo = new sceneConfigObject();
       }
   }

   /// <summary>
   /// 重载脚本的界面
   /// </summary>
   public override void OnInspectorGUI()
   {
       mScript.mInfo.mIndex = EditorGUILayout.TextField("场景配置名", mScript.mInfo.mIndex);

       mScript.mInfo.spawnPos = EditorGUILayout.Vector3Field("出生点位置", mScript.mInfo.spawnPos);
   }

}

此时我们的数据就显示出来了:



但是我们不可能就这样存储数据,所以最后我们加上载入配置和导出配置的功能



/// <summary>
   /// 重载脚本的界面
   /// </summary>
   public override void OnInspectorGUI()
   {
   ……

       if (GUILayout.Button("导入"))
       {
           if (string.IsNullOrEmpty(mScript.mInfo.mIndex))
           {
               Debug.LogError("未输入配置名");
               return;
           }

           string path = "config/" + mScript.mInfo.mIndex;

           var configObj = Resources.Load(path) as sceneConfigObject;
           if (configObj != null)
           {
               configObj = Instantiate(configObj);
               configObj.name = mScript.mInfo.mIndex;
           }
           mScript.mInfo = configObj;
       }

       if (GUILayout.Button("导出"))
       {
           if (string.IsNullOrEmpty(mScript.mInfo.mIndex))
           {
               Debug.LogError("未输入配置名");
               return;
           }

           string path = "Assets/Resources/config/" + mScript.mInfo.mIndex + ".asset";

           if (File.Exists(path))
           {
               AssetDatabase.DeleteAsset(path);
               AssetDatabase.SaveAssets();
           }


           AssetDatabase.CreateAsset(Instantiate(mScript.mInfo), "Assets/Resources/config/" + mScript.mInfo.mIndex + ".asset");
           AssetDatabase.SaveAssets();
           AssetDatabase.Refresh();
       }
   }

这样我们导出的ScriptableObject类就存放到硬盘上。

/*AssetDatabase类是Unity专门针对编辑模式下的数据存储基类*/



如果要使用,我们就将它当成资源加载,转换成对应的脚本类型,就可以实现调用



public void LoadScriptableObject()
   {
       var configObj = Instantiate(Resources.Load("config/test01") as sceneConfigObject);
       Debug.Log(configObj.mIndex);
       Debug.Log(configObj.spawnPos);
   }

[查错工具]

游戏中的查错工具很多,因为团队项目在工作中合并资源出错会导致资源丢失,如果一个一个去寻找是非常花时间的。这里以检测动画文件状态为例。

依然是Editor扩展编辑器功能,它有一个属性可以修改菜单栏。

[MenuItem("Tools/动画查错", priority = 0)]

MenuItem后跟上的路径,为选项的父子目录关系。

priority为优先级,可以调整选项显示的顺序。

效果图:



EditorUtility.DisplayDialog

提供弹窗功能



以下为源码:



using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
using System.IO;
using UnityEditor.Animations;

public class AnimtorTool : Editor {


   public static string modePrefabsPath = "Assets/Resources/animator/";

   [MenuItem("Tools/动画查错", priority = 0)]
   public static void FreshAnimtor()
   {
       FileInfo[] modeDirect = new DirectoryInfo(modePrefabsPath).GetFiles();

       for (int i = 0; i < modeDirect.Length; i++)
       {
           if (modeDirect[i].Name.Contains("meta"))
           {
               continue;
           }
           string modelName = modeDirect[i].Name;

           string animtorPath = modePrefabsPath + modelName;

           //设置动画控制器内参数
           AnimatorController AnimatorTemplate = AssetDatabase.LoadAssetAtPath(animtorPath, typeof(AnimatorController)) as AnimatorController;

           if (AnimatorTemplate == null)
           {
               if (EditorUtility.DisplayDialog("错误的路径", "寻找动画路径失败:" + animtorPath + ",检查动画控制器名字是否和模型名字匹配", "继续"))
               {
                   return;
               }
           }

           foreach (var obj in AnimatorTemplate.layers[0].stateMachine.states)
           {
               if (obj.state.motion == null)
               {
                   if (EditorUtility.DisplayDialog("存在空的动画Clip", "动画" + animtorPath + "的状态" + obj.state.name + "为空", "继续"))
                   {
                       continue;
                   }
               }
           }

       }
   }
}


最后注意,Editor代码一定要放在Editor目录中,目录中的代码不参与打包。


总结

我个人理解的程序员职责应当是除了解决项目实际问题外,还要致力于优化项目流程。毕竟修改自己的代码容易,修改项目的BUG难。如果不以团队合作为目的,仅仅想着自身单方面能力的提高,是不能将个人价值发挥到最大,IT行业不比传统行业,更注重的是一个人的整体能力,有经验的程序员一个能打十个就是因为能改善项目工作流程。

回到正题,Editor代码因为不参与打包,完全是游戏项目的扩展工具,因此普适性很强,能混用很多个项目中去,之后几篇文章会针对工具类脚本进行教学,尽请期待。


对游戏开发感兴趣的同学,欢迎围观我们:【皮皮关游戏开发教育】 ,会定期更新各种教程干货,更有别具一格的线下小班教育。

我们的官网地址:levelpp.com/

我们的游戏开发技术交流群:610475807

我们的微信公众号:皮皮关


【Unity】工具类系列教程——配置化和规范流程的评论 (共 条)

分享到微博请遵守国家法律