前言:
程序员是看重效率的群体,如果凡事都事必躬亲,一行一行的码代码,进行重复的劳动,最后只会沦为码农(知乎很多大神都用这个自称,这个词不是褒义词啊!)。
我们想象一下这个场景:
策划:这个场景的出场位置太远了,调近一点点
程序:(卧槽,还有很多新需求还没解决,这货又来!)好,我等下就调。
过了一会……
策划:角色位置还是不对,你再调远一点点?
程序:……
反复几次后,程序暴走
程序:还做不做新功能了!?
(还是不敢不改啊)
还有这种场景:
程序:美术大爷,你这个角色的动画丢失了。
美术:好的,我改一下
一会后……
程序:美术大爷,动画是没丢失了,但是却没设置成循环……,您?您再改一下?
美术:怎么会?!好的……
过了几天,这个情况依然还会上演。
很多项目遇到的时间消耗,其实都在“举手之劳”上,最后程序、美术、策划三方互相都有怨气。但是如果一个项目里面有规范流程化的工具。
比如程序策划交流场景变成了这样
程序:这个场景的出生点坐标我已经暴露在这个脚本上了,你直接在场景里面随便调,调好了点这个上传就行,有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代码因为不参与打包,完全是游戏项目的扩展工具,因此普适性很强,能混用很多个项目中去,之后几篇文章会针对工具类脚本进行教学,尽请期待。
对游戏开发感兴趣的同学,欢迎围观我们:【皮皮关游戏开发教育】 ,会定期更新各种教程干货,更有别具一格的线下小班教育。
我们的官网地址:http://levelpp.com/
我们的游戏开发技术交流群:610475807
我们的微信公众号:皮皮关
暂无关于此日志的评论。