关于Unity3D自定义编辑器的学习
被人物编辑器折腾了一个月,最终还是交了点成品上去(还要很多优化都还么做)。
刚接手这项工作时觉得没概念,没想法,不知道。后来就去看<<Unity5.X从入门到精通>>中有关于自定义编辑器(自定义Inspector和自定义Scene或GUI)的一些例子,还包括看了 雨松的编辑器教程 和 自定义结构显示在Inspector的方法 看完之后也实战了一下就算入了门,就分析自己项目的人物对应的数据,如下图:
上述数据其实很简单但是对于我这种初学者来说就有点难度,首先因为Actions 和 Frames(动作对应的帧集合) 需要有类似数组或者链表这些数据结构来存储。就去查了一些资料发现 几篇好的关于序列化和反序列化的博文 , 比如 大表哥的博文 提到 哪些数据能够序列化和反序列化 ,其中我们就可以用List<T>的结构来存储数据集合,关于为什么涉及到序列化和反序列化, 因为我们需要将一些数据保存到本地,而不是仅仅的放在内存中,再从本地取回到内存中就需要反序列化了。
理解了上述的基础知识 ,便自己定义了特定的数据类(主要的):
[System.Serializable]
public class CharacterEditorStateData : ISerializationCallbackReceiver
{
public string m_animationName;
public int m_totFrame;
public CharacterStateType m_stateType;
[HideInInspector]
public CharacterStateType m_oldStateType;
[HideInInspector][NonSerialized]
public List<CharacterEditorAttackData> m_attackDatas = new List<CharacterEditorAttackData>();
[HideInInspector][SerializeField]
public List<CharacterEditorBombAttackData> m_attackBmDatas = new List<CharacterEditorBombAttackData>();
[HideInInspector][SerializeField]
public List<CharacterEditorNormalAttackData> m_attackNmDatas = new List<CharacterEditorNormalAttackData>();
public CharacterEditorAttackData IsFrameDataExist(int frame)
{
foreach (CharacterEditorAttackData dt in m_attackDatas)
{
if (frame == dt.m_iFrame)
return dt;
}
return null;
} public bool AddFrameData(int newFrame)
{
CharacterEditorAttackData dt = CharacterEditorAttackData.CreateData(CharacterAttackType.BOMB);
if (dt == null)
return false;
dt.m_iFrame = newFrame;
dt.m_attackType = CharacterAttackType.BOMB;
this.m_attackDatas.Add(dt);
return true;
} public bool RemoveFrameData(int oldFrame)
{
CharacterEditorAttackData dt = this.IsFrameDataExist(oldFrame);
if (dt == null)
return false;
this.m_attackDatas.Remove(dt);
return true;
} public void ChangeFrameData(int index , CharacterAttackType attType)
{
CharacterEditorAttackData dt = this.m_attackDatas[index];
int iFrame = dt.m_iFrame;
if (attType != dt.m_attackType)
{
dt = CharacterEditorAttackData.CreateData(attType);
dt.m_iFrame = iFrame;
dt.m_attackType = attType;
this.m_attackDatas[index] = dt;
}
} public int GetNewFrame()
{
if (m_attackDatas.Count == )
return ;
int frame = -;
foreach (CharacterEditorAttackData dt in m_attackDatas)
{
if (frame <= dt.m_iFrame)
frame = dt.m_iFrame;
}
if (frame == this.m_totFrame)
return -;
return frame + ;
} public bool IsLegalFrame(int frame)
{
if (IsFrameDataExist(frame) != null || frame < || frame > m_totFrame)
return false;
return true;
} public bool UpdateFramesSz()
{
int count = m_attackDatas.Count;
for (int i = count - ; i > m_totFrame - ; i--)
{
this.m_attackDatas.RemoveAt(i);
}
return true;
} public void Init(CharacterStateType state)
{
m_animationName = "";
m_totFrame = ;
m_stateType = state;
m_oldStateType = state;
m_attackDatas.Clear();
m_attackBmDatas.Clear();
m_attackNmDatas.Clear();
} void ISerializationCallbackReceiver.OnBeforeSerialize()
{
if (m_attackBmDatas == null || m_attackNmDatas == null || m_attackDatas == null)
return;
m_attackBmDatas.Clear();
m_attackNmDatas.Clear();
foreach(CharacterEditorAttackData item in m_attackDatas)
{
switch(item.m_attackType)
{
case CharacterAttackType.BOMB:m_attackBmDatas.Add((CharacterEditorBombAttackData)item); break;
case CharacterAttackType.NORMAL: m_attackNmDatas.Add((CharacterEditorNormalAttackData)item);break;
}
}
} void ISerializationCallbackReceiver.OnAfterDeserialize()
{
if (m_attackBmDatas == null || m_attackNmDatas == null || m_attackDatas == null)
return;
m_attackDatas.Clear();
foreach (CharacterEditorAttackData item in m_attackBmDatas)
{
m_attackDatas.Add(item);
}
foreach (CharacterEditorAttackData item in m_attackNmDatas)
{
m_attackDatas.Add(item);
}
}
}
对于上述的数据结构,可能会有疑问,首先为什么需要实现 ISerializationCallbackReceiver , 和这个接口的作用;为什么需要用到三个List结构。首先,来
理解一下 ISerializationCallbackReceiver 这个接口的作用 。 关于这个 接口介绍的博文 ,其实看完这个博文,我还是没有理解作者想讲的意思,后来自己翻阅
了其他资料,
void ISerializationCallbackReceiver.OnBeforeSerialize() 这个接口的作用就是 序列化快开始了 , 你可以在序列化开始前做些操作 。 比如C#结构中Dict是不能够序列化的,所以在开始前可以将Dict的键和值都保存在List中 这样就达到了序列化的目的。
void ISerializationCallbackReceiver.OnAfterDeserialize() 这个接口的作用就是 反序列化结束了 , 你可以在反序列化后做一些操作 。还是上面的例子,我们可以在反序列化后从list中拿到对应的数据,把List中的键和值存储在对应的Dict中。 关于两个接口的作用已经讲完了,来解决下为什么要用那么多List的原因,首先先说一下我遇到的问题,之前访问子类和存储都是通过new子类对象后,用父类的指针保存在List上,所以就会存在问题,在序列化时,序列化的是父类而不是子类,在反序列化后,就会出现数据丢失。所以需要在上述的两个接口做一个序列化前和反序列化后的数据操作,保证数据的正确。 通过上述,我们可以得到可靠的序列化流程,接下来就可以就可以自定义编辑器了,编辑器代码相对简单,由于界面主要在InspectorUI上操作,就写在OnInspector上。其实在写之前对于OnInspectorUI这个函数的调用是有疑问的,后来亲自实践了一下,发现只有有UI发生更改时或者切入切出(调到另一个)显示对象都会调用,代码如下:
using UnityEngine;
using UnityEditor;
using System.Collections;
using TKGame;
using System.Collections.Generic;
using System; [CustomEditor(typeof(CharacterEditorData))]
public class CharacterEditor : Editor { enum AddState { NONE, NEWSTATE, FULLSTATE };
public const string TAG = "[CharacterEditor]";
public CharacterEditorData m_chaEditData = null;
private AddState m_addState;
public void OnEnable(){
m_chaEditData = target as CharacterEditorData;
if (m_chaEditData == null)
return;
m_addState = AddState.NONE;
} public override void OnInspectorGUI()
{
if (m_chaEditData == null)
{
PrintLog("the chaEditorData is null");
return;
}
m_chaEditData.m_id = EditorGUILayout.IntField("PlayerID: ", m_chaEditData.m_id);
m_chaEditData.m_resID = EditorGUILayout.IntField("PlayerResourceID:", m_chaEditData.m_resID);
m_chaEditData.m_defaultName = EditorGUILayout.TextField("PlayerDefaultName: ", m_chaEditData.m_defaultName);
m_chaEditData.m_scale = EditorGUILayout.FloatField("PlayerScale:", m_chaEditData.m_scale);
m_chaEditData.m_walkSpeedX = EditorGUILayout.IntField("PlayerXSpeed: ", m_chaEditData.m_walkSpeedX);
m_chaEditData.m_walkSpeedY = EditorGUILayout.IntField("PlayerYSpeed:", m_chaEditData.m_walkSpeedY);
m_chaEditData.m_hatred = EditorGUILayout.FloatField("PlayerHatred:", m_chaEditData.m_hatred);
m_chaEditData.m_lowFireAngle = EditorGUILayout.FloatField("PlayerLowFireAngle:", m_chaEditData.m_lowFireAngle);
m_chaEditData.m_higFireAngle = EditorGUILayout.FloatField("PlayerHigFireAngle:", m_chaEditData.m_higFireAngle);
m_chaEditData.m_fireRange = EditorGUILayout.IntField("PlayerFireRange:", m_chaEditData.m_fireRange);
m_chaEditData.m_weaponPosition = EditorGUILayout.Vector2Field("PlayerWeaponPos", m_chaEditData.m_weaponPosition);
m_chaEditData.m_beAttackBoxMinX = EditorGUILayout.IntField("PlayerBAtkBoxMinX:", m_chaEditData.m_beAttackBoxMinX);
m_chaEditData.m_beAttackBoxMinY = EditorGUILayout.IntField("PlayerBAtkBoxMinY:", m_chaEditData.m_beAttackBoxMinY);
m_chaEditData.m_beAttackBoxMaxX = EditorGUILayout.IntField("PlayerBAtkBoxMaxX:", m_chaEditData.m_beAttackBoxMaxX);
m_chaEditData.m_beAttackBoxMaxY = EditorGUILayout.IntField("PlayerBAtkBoxMaxY:", m_chaEditData.m_beAttackBoxMaxY);
if (GUILayout.Button("Add New State"))
{
if (m_chaEditData.IsAllStateExist())
m_addState = AddState.FULLSTATE;
else
m_addState = AddState.NEWSTATE;
}
EditorGUILayout.Space();
if (m_addState == AddState.FULLSTATE)
EditorGUILayout.LabelField("all states is used");
else if (m_addState == AddState.NEWSTATE)
{
CharacterStateType newestState = m_chaEditData.GetNewestState();
m_chaEditData.AddNewState(newestState);
m_addState = AddState.NONE;
} EditorGUILayout.Space();
///Debug.Log("yes");
for (int index = ; index < m_chaEditData.m_lsStates.Count; index++)
{
CharacterEditorData.CharacterEditorStateData chaState = m_chaEditData.m_lsStates[index];
//Debug.Log(EditorGUILayout.EnumPopup("state:", chaState.m_newState));
CharacterStateType state = (CharacterStateType)EditorGUILayout.EnumPopup("state:", chaState.m_stateType);
m_chaEditData.ChangeByState(chaState, state);
chaState.m_animationName = EditorGUILayout.TextField("AnimationName:", chaState.m_animationName);
int totFrame = EditorGUILayout.IntField("TotalFrame:", chaState.m_totFrame);
if (totFrame != chaState.m_totFrame)
{
chaState.m_totFrame = totFrame;
chaState.UpdateFramesSz();
}
if (chaState.m_stateType == CharacterStateType.ATTACK)
{
if (GUILayout.Button("Add Frame Data", GUILayout.MaxWidth(), GUILayout.MaxHeight()))
{
int newFrame = chaState.GetNewFrame();
//Debug.Log(newFrame);
if (newFrame != -)
{
chaState.AddFrameData(newFrame);
}
}
EditorGUILayout.Space();
for (int i = ; i < chaState.m_attackDatas.Count; i++)
{
CharacterEditorData.CharacterEditorAttackData frameData = chaState.m_attackDatas[i];
int frame = EditorGUILayout.IntField("Frame:", frameData.m_iFrame);
if (chaState.IsLegalFrame(frame))
{
//Debug.Log(frame);
frameData.m_iFrame = frame;
}
CharacterAttackType attackType = (CharacterAttackType)EditorGUILayout.EnumPopup("AttackType:", frameData.m_attackType);
chaState.ChangeFrameData(i , attackType);
EditorGUILayout.Space();
if (frameData.m_attackType == CharacterAttackType.BOMB)
{
CharacterEditorData.CharacterEditorBombAttackData bomb = (CharacterEditorData.CharacterEditorBombAttackData)frameData;
bomb.m_bombCofigID = EditorGUILayout.IntField("BombConfigID:", bomb.m_bombCofigID);
bomb.m_damage = EditorGUILayout.IntField("Damge:", bomb.m_damage);
bomb.m_centerDamage = EditorGUILayout.IntField("CenterDamage:", bomb.m_centerDamage);
}
if (GUILayout.Button("Remove this Frame"))
{
chaState.RemoveFrameData(frameData.m_iFrame);
}
EditorGUILayout.Space();
EditorGUILayout.Space();
}
}
if (GUILayout.Button("remove this state"))
{
m_chaEditData.RemoveOldState(index);
}
EditorGUILayout.Space();
EditorGUILayout.Space();
}
EditorUtility.SetDirty(m_chaEditData);
} private void PrintLog(string str)
{
Debug.Log(TAG+" "+ str);
}
}
最后一句的SetDirty表示当前的数据对象有更改,可以通知UI刷新。
关于Unity3D自定义编辑器的学习的更多相关文章
- Unity3D自定义编辑器简单实例
MenuItem:在标题栏自定义菜单.需要在Editor文件夹内创建脚本,无需挂载.但是注意其下的函数必须为静态函数. using UnityEngine; using UnityEditor; pu ...
- Unity3d编辑器扩展学习笔记
编辑器扩展 1.添加菜单栏:把特性应用于静态方法 参数1:菜单名的空格后面是定义快捷键(单符号得用"_"开头,组合键%=Ctrl,#=Shift,&=Alt) 参数2:通过 ...
- 【Unity】自定义编辑器窗口——拓展编辑器功能
最近学习了Unity自定义编辑器窗口,下面简单总结,方便用到时回顾. 新建一个脚本: using UnityEngine; using System.Collections; using UnityE ...
- (转)Unity3d UnityEditor编辑器定制和开发插件
在阅读本教程之前,你需要对Unity的操作流程有一些基础的认识,并且最好了解内置的GUI系统如何使用. 如何让编辑器运行你的代码 Unity3D可以通过事件触发来执行你的编辑器代码,但是我们需要一些编 ...
- 【转载】Unity3d UnityEditor编辑器定制和开发插件
在阅读本教程之前,你需要对Unity的操作流程有一些基础的认识,并且最好了解内置的GUI系统如何使用. 如何让编辑器运行你的代码 Unity3D可以通过事件触发来执行你的编辑器代码,但是我们需要一些编 ...
- [转]Unity3D Editor 编辑器简易教程
Star 自定义编辑器简易教程 an introduction to custom editors 原文地址 http://catlikecoding.com/unity/tutorials/star ...
- Web Essentials之Markdown和自定义编辑器(Web Essentials完结)
返回Web Essentials功能目录 本篇目录 功能 自定义编辑器 开源项目都会在项目的根目录放一个Readme.md文件来告诉读者一些重要的说明,那么就可以在VS中直接编辑Markdown文件. ...
- unity3d拓展编辑器MenuItem的使用
MenuItem是自定义菜单栏显示 比如:[MenuItem("new/My Window")] 这样就会显示菜单new/My Window 把这个放在一个静态方法上就可以了.记住 ...
- markdown编辑器的学习
markdown编辑器的学习 1 标题 一级标题 二级标题 三级标题 四级标题 五级标题 六级标题 2列表 无序列表 1 2 3 4 有序列表 1 2 3 4 3引用 这里是引用,哈哈我也不知道到我引 ...
随机推荐
- win10 环境 gitbash 显示中文乱码问题处理
gitbash 是 windows 环境下非常好用的命令行终端,可以模拟一下linux下的命令如ls / mkdir 等等,如果使用过程中遇到中文显示不完整或乱码的情况,多半是因为编码问题导致的,修改 ...
- Markdown 图片助手-MarkdownPicPicker
title: Markdown 图片助手 v0.1 toc: true comments: true date: 2016-06-04 16:40:06 tags: [Python, Markdown ...
- 如何优雅的使用RabbitMQ
RabbitMQ无疑是目前最流行的消息队列之一,对各种语言环境的支持也很丰富,作为一个.NET developer有必要学习和了解这一工具.消息队列的使用场景大概有3种: 1.系统集成,分布式系统的设 ...
- 有朋友问了数据库ID不连续,怎么获取上一篇和下一篇的文章?(不是所有情况都适用)
呃 (⊙o⊙)…,逆天好久没写SQL了,EF用的时间长了,SQL都不怎么熟悉了......[SQL水平比较菜,大牛勿喷] 方法很多种,说个最常见的处理 因为id是自增长的,所以一般情况下下一篇文章的I ...
- ASP.NET MVC5+EF6+EasyUI 后台管理系统 (源码购买说明)
系列目录 升级日志 !!!重大版本更新:于2016-12-20日完成了系统的结构重构并合并简化了T4(这是一次重要的更新,不需要修改现有功能的代码),代码总行数比上个版本又少了1/3.更新了代码生成器 ...
- ASP.NET Core框架揭秘(持续更新中…)
之前写了一系列关于.NET Core/ASP.NET Core的文章,但是大都是针对RC版本.到了正式的RTM,很多地方都发生了改变,所以我会将之前发布的文章针对正式版本的.NET Core 1.0进 ...
- InstallShield 脚本语言学习笔记
InstallShield脚本语言是类似C语言,利用InstallShield的向导或模板都可以生成基本的脚本程序框架,可以在此基础上按自己的意愿进行修改和添加. 一.基本语法规则 ...
- EntityFramework.Extended 支持 MySql
EntityFramework.Extended 默认不支持 MySql,需要配置如下代码: [DbConfigurationType(typeof(DbContextConfiguration))] ...
- node中子进程同步输出
管道 通过"child_process"模块fork出来的子进程都是返回一个ChildProcess对象实例,ChildProcess类比较特殊无法手动创建该对象实例,只能使用fo ...
- 【知识必备】RxJava+Retrofit二次封装最佳结合体验,打造懒人封装框架~
一.写在前面 相信各位看官对retrofit和rxjava已经耳熟能详了,最近一直在学习retrofit+rxjava的各种封装姿势,也结合自己的理解,一步一步的做起来. 骚年,如果你还没有掌握ret ...