【Unity】工具類(lèi)系列教程——配置化和規(guī)范流程

前言:
程序員是看重效率的群體,如果凡事都事必躬親,一行一行的碼代碼,進(jìn)行重復(fù)的勞動(dòng),最后只會(huì)淪為碼農(nóng)(知乎很多大神都用這個(gè)自稱(chēng),這個(gè)詞不是褒義詞?。。?/p>
我們想象一下這個(gè)場(chǎng)景:
策劃:這個(gè)場(chǎng)景的出場(chǎng)位置太遠(yuǎn)了,調(diào)近一點(diǎn)點(diǎn)
程序:(臥槽,還有很多新需求還沒(méi)解決,這貨又來(lái)?。┖?,我等下就調(diào)。
過(guò)了一會(huì)……
策劃:角色位置還是不對(duì),你再調(diào)遠(yuǎn)一點(diǎn)點(diǎn)?
程序:……
反復(fù)幾次后,程序暴走
程序:還做不做新功能了???

(還是不敢不改啊)
還有這種場(chǎng)景:
程序:美術(shù)大爺,你這個(gè)角色的動(dòng)畫(huà)丟失了。
美術(shù):好的,我改一下
一會(huì)后……
程序:美術(shù)大爺,動(dòng)畫(huà)是沒(méi)丟失了,但是卻沒(méi)設(shè)置成循環(huán)……,您?您再改一下?
美術(shù):怎么會(huì)?!好的……
過(guò)了幾天,這個(gè)情況依然還會(huì)上演。
很多項(xiàng)目遇到的時(shí)間消耗,其實(shí)都在“舉手之勞”上,最后程序、美術(shù)、策劃三方互相都有怨氣。但是如果一個(gè)項(xiàng)目里面有規(guī)范流程化的工具。
比如程序策劃交流場(chǎng)景變成了這樣
程序:這個(gè)場(chǎng)景的出生點(diǎn)坐標(biāo)我已經(jīng)暴露在這個(gè)腳本上了,你直接在場(chǎng)景里面隨便調(diào),調(diào)好了點(diǎn)這個(gè)上傳就行,有BUG或者不懂的再問(wèn)我。
策劃:好的大爺。
程序美術(shù)交流的場(chǎng)景是這樣:
程序:你每次上傳美術(shù)資源的時(shí)候,點(diǎn)一下菜單欄的這個(gè)按鈕,它會(huì)把你丟失或者不對(duì)的都報(bào)錯(cuò)出來(lái)。
美術(shù)一查,就看到了所有美術(shù)資源出錯(cuò)的地方。
因此引出這篇教程的主題,配置化和規(guī)范流程。
ScriptableObject
做游戲配置有很多方法,有Excel保存,有導(dǎo)出Json、TXT,這里對(duì)Unity自帶ScriptableObject序列化方法配置做介紹。
(如果對(duì)配置有興趣的朋友可以去試試LitJson將類(lèi)導(dǎo)出成Json格式,或者自己試著寫(xiě)格式)
開(kāi)始具體使用前,我們普及幾個(gè)概念。
ScriptableObject 有什么好處?
1.Unity用于創(chuàng)建不需要綁定到物體的對(duì)象,即非繼承于Mono,該類(lèi)繼承于UnityEngine.Object
2.存放編輯器或數(shù)據(jù)配置文件
3.方便操作管理,可以可視化編輯
4.取數(shù)據(jù)方便,ScriptableObject已經(jīng)是可序列化的數(shù)據(jù),不用像讀取純文本或xml那樣還要繁瑣耗時(shí)的數(shù)據(jù)解析過(guò)程
(當(dāng)然也有壞處,如果不進(jìn)行Editor編寫(xiě)變量,可讀性其實(shí)很低,而且它和代碼綁定,如果配置類(lèi)的代碼修改,序列化的數(shù)據(jù)就會(huì)丟失,但是總的來(lái)說(shuō)不使用其他插件的情況下,用ScriptableObject 來(lái)學(xué)習(xí)游戲內(nèi)容配置是不錯(cuò)的)
序列化和反序列化的概念
把對(duì)象轉(zhuǎn)換為字節(jié)序列的過(guò)程稱(chēng)為對(duì)象的序列化;把字節(jié)序列恢復(fù)為對(duì)象的過(guò)程稱(chēng)為對(duì)象的反序列化。
/*強(qiáng)調(diào)一下,數(shù)據(jù)解析和序列化目的是一致的,就是將不可用轉(zhuǎn)換為可用,但是實(shí)際的方式方法不同*/
我們先做一個(gè)ScriptableObject的 數(shù)據(jù)類(lèi)
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class sceneConfigObject : ScriptableObject
{
/// <summary>
/// 配置名字
/// </summary>
public string mIndex;
/// <summary>
/// 出生點(diǎn)位置
/// </summary>
public Vector3 spawnPos;
}
這個(gè)腳本是不能直接掛載到物體上的,只有繼承了mono類(lèi)的腳本才能夠。

然后為了我們直觀的修改數(shù)據(jù),我們用一個(gè)mono腳本做中間層。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class SceneConfig : MonoBehaviour
{
public sceneConfigObject mInfo;
}
現(xiàn)在我們可以將SceneConfig 掛載物體上,但是顯示并不是我們想要看到的數(shù)據(jù)。

我們?nèi)绻孶nity顯示出來(lái)我們要編輯的數(shù)據(jù),就比如去修改它顯示的內(nèi)容。而我們?nèi)绾稳プ远x化腳本的顯示內(nèi)容呢?
這里就需要UnityEditor擴(kuò)展編輯器功能。
編寫(xiě)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>
/// 腳本激活的時(shí)候進(jìn)入,target就是對(duì)應(yīng)[CustomEditor(typeof(SceneConfig))]的SceneConfig類(lèi)
/// </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("場(chǎng)景配置名", mScript.mInfo.mIndex);
mScript.mInfo.spawnPos = EditorGUILayout.Vector3Field("出生點(diǎn)位置", mScript.mInfo.spawnPos);
}
}
此時(shí)我們的數(shù)據(jù)就顯示出來(lái)了:

但是我們不可能就這樣存儲(chǔ)數(shù)據(jù),所以最后我們加上載入配置和導(dǎo)出配置的功能
/// <summary>
/// 重載腳本的界面
/// </summary>
public override void OnInspectorGUI()
{
……
if (GUILayout.Button("導(dǎo)入"))
{
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("導(dǎo)出"))
{
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();
}
}
這樣我們導(dǎo)出的ScriptableObject類(lèi)就存放到硬盤(pán)上。
/*AssetDatabase類(lèi)是Unity專(zhuān)門(mén)針對(duì)編輯模式下的數(shù)據(jù)存儲(chǔ)基類(lèi)*/

如果要使用,我們就將它當(dāng)成資源加載,轉(zhuǎn)換成對(duì)應(yīng)的腳本類(lèi)型,就可以實(shí)現(xiàn)調(diào)用
public void LoadScriptableObject()
{
var configObj = Instantiate(Resources.Load("config/test01") as sceneConfigObject);
Debug.Log(configObj.mIndex);
Debug.Log(configObj.spawnPos);
}
[查錯(cuò)工具]
游戲中的查錯(cuò)工具很多,因?yàn)閳F(tuán)隊(duì)項(xiàng)目在工作中合并資源出錯(cuò)會(huì)導(dǎo)致資源丟失,如果一個(gè)一個(gè)去尋找是非?;〞r(shí)間的。這里以檢測(cè)動(dòng)畫(huà)文件狀態(tài)為例。
依然是Editor擴(kuò)展編輯器功能,它有一個(gè)屬性可以修改菜單欄。
[MenuItem("Tools/動(dòng)畫(huà)查錯(cuò)", priority = 0)]
MenuItem后跟上的路徑,為選項(xiàng)的父子目錄關(guān)系。
priority為優(yōu)先級(jí),可以調(diào)整選項(xiàng)顯示的順序。
效果圖:

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/動(dòng)畫(huà)查錯(cuò)", 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;
//設(shè)置動(dòng)畫(huà)控制器內(nèi)參數(shù)
AnimatorController AnimatorTemplate = AssetDatabase.LoadAssetAtPath(animtorPath, typeof(AnimatorController)) as AnimatorController;
if (AnimatorTemplate == null)
{
if (EditorUtility.DisplayDialog("錯(cuò)誤的路徑", "尋找動(dòng)畫(huà)路徑失?。?quot; + animtorPath + ",檢查動(dòng)畫(huà)控制器名字是否和模型名字匹配", "繼續(xù)"))
{
return;
}
}
foreach (var obj in AnimatorTemplate.layers[0].stateMachine.states)
{
if (obj.state.motion == null)
{
if (EditorUtility.DisplayDialog("存在空的動(dòng)畫(huà)Clip", "動(dòng)畫(huà)" + animtorPath + "的狀態(tài)" + obj.state.name + "為空", "繼續(xù)"))
{
continue;
}
}
}
}
}
}
最后注意,Editor代碼一定要放在Editor目錄中,目錄中的代碼不參與打包。

總結(jié)
我個(gè)人理解的程序員職責(zé)應(yīng)當(dāng)是除了解決項(xiàng)目實(shí)際問(wèn)題外,還要致力于優(yōu)化項(xiàng)目流程。畢竟修改自己的代碼容易,修改項(xiàng)目的BUG難。如果不以團(tuán)隊(duì)合作為目的,僅僅想著自身單方面能力的提高,是不能將個(gè)人價(jià)值發(fā)揮到最大,IT行業(yè)不比傳統(tǒng)行業(yè),更注重的是一個(gè)人的整體能力,有經(jīng)驗(yàn)的程序員一個(gè)能打十個(gè)就是因?yàn)槟芨纳祈?xiàng)目工作流程。
回到正題,Editor代碼因?yàn)椴粎⑴c打包,完全是游戲項(xiàng)目的擴(kuò)展工具,因此普適性很強(qiáng),能混用很多個(gè)項(xiàng)目中去,之后幾篇文章會(huì)針對(duì)工具類(lèi)腳本進(jìn)行教學(xué),盡請(qǐng)期待。
對(duì)游戲開(kāi)發(fā)感興趣的同學(xué),歡迎圍觀我們:【皮皮關(guān)游戲開(kāi)發(fā)教育】 ,會(huì)定期更新各種教程干貨,更有別具一格的線(xiàn)下小班教育。
我們的官網(wǎng)地址:http://levelpp.com/
我們的游戲開(kāi)發(fā)技術(shù)交流群:610475807
我們的微信公眾號(hào):皮皮關(guān)