文章目录
- 前言
- 一、Editor操作
- 二、快捷导出unity包
- 三、快捷打开存储目录
- 四、封装transform操作
- 1、localPosition赋值简化
- 2、封装修改transform.localPosition X Y Z
- 3、封装transform.localPosition XY、XZ 和YZ
- 4、Transform 重置
- 五、封装概率函数
- 六、方法过时
- 七、partial 关键字,拆开合并类
- 八、从数组中随机取⼀个数值并进⾏返回
- 1、实现
- 2、object 类优化
- 3、泛型,结构复⽤利器
- 4、params 关键字优化
- 九、abstract 关键字定义抽象类和抽象方法
- 1、抽象类
- 2、抽象方法
- 十、封装继承MonoBehaviour的基类
- 1、封装重置transform
- 2、封装协程定时功能
- 完结
前言
在游戏开发中,Unity作为一个强大的引擎,提供了丰富的功能和灵活的开发环境。然而,随着项目规模的扩大和复杂度的增加,仅依赖Unity的基本功能往往无法满足高效、可维护的开发需求。因此,建立一个完善的框架,以及进行工具类的封装,成为了每个Unity开发者提升工作效率和项目质量的重要一步。
在本篇文章中,我们将探讨一些基本的框架开发思维,同时介绍如何有效地进行工具类的封装。希望通过这些实践经验,能够帮助你在Unity开发的旅程中打下坚实的基础,提高开发效率,并最终实现更加优秀的游戏作品。无论你是刚入门的初学者,还是有一定经验的开发者,掌握框架思维和工具类封装都将为你的项目带来意想不到的便利和提升。
一、Editor操作
# 菜单按钮
[MenuItem("XYFrame/XYFrame/1.一键打包XYFrame")]
# 快捷键调用MenuItem菜单按钮
//最后有个 “ %e”,这个就是快捷键的符号,意思是 ctrl/cmd + e
[MenuItem("XYFrame/XYFrame/1.一键打包XYFrame %e")]
# MenuItem菜单按钮排序
//第三个参数,意思是优先级,表示 MenuItem 所在的显示顺序,数值越⼤越在底部。
[MenuItem("XYFrame/XYFrame/2.打开存储目录, false, 1")]
# 直接调⽤MenuItem菜单按钮【XYFrame/XYFrame/1.一键打包XYFrame】
EditorApplication.ExecuteMenuItem("XYFrame/XYFrame/1.一键打包XYFrame");
# 这⾏代码执⾏之后,会⾃动运⾏Unity
UnityEditor.EditorApplication.isPlaying = true;
如果不⽤ Editor ⽂件夹了,取⽽代之的是在代码中给每个 UnityEditor API 都加上#if UNITY_EDITOR
宏判断,这样做的⽬的,是为了不影响⾃⼰所在项⽬打包。放在Editor ⽂件夹就不需要了
#if UNITY_EDITOR
using UnityEditor;
#endif
#if UNITY_EDITOR
[MenuItem("XYFrame/XYFrame/1.一键打包XYFrame")]
#endif
二、快捷导出unity包
using UnityEditor;
using System;
namespace XYFrame
{
/// <summary>
/// 打包XYFrame框架文件夹
/// </summary>
public class ExportUnityPackage
{
[MenuItem("XYFrame/XYFrame/1.一键打包XYFrame %e")]
private static void MenuClicked()
{
//要打包的文件夹路径
string assetPathName = "Assets/XYFrame";
//要导出的文件路径和⽂件名字
string fileName = "Assets/XYFrame_" + DateTime.Now.ToString("yyyyMMdd_hh") + ".unitypackage";//获取当前时间
//Recurse:递归选项,意思是说包含⼦⽬录。
AssetDatabase.ExportPackage(assetPathName, fileName, ExportPackageOptions.Recurse);
}
}
}
效果
打包结果
三、快捷打开存储目录
using UnityEditor;
using UnityEngine;
namespace XYFrame
{
/// <summary>
/// 打开存储目录
/// </summary>
public class OpenSaveInFolder
{
[MenuItem("XYFrame/XYFrame/2.打开存储目录")]
private static void OpenInSaveFolder()
{
OpenInFolder(Application.persistentDataPath);
}
/// <summary>
/// 打开某个文件夹
/// </summary>
/// <param name="folderPath">文件夹路径</param>
public static void OpenInFolder(string folderPath)
{
Application.OpenURL("file:///" + folderPath);
}
}
}
效果
点击就打开了目录
四、封装transform操作
1、localPosition赋值简化
经常使用Unity会发现有一个⾮常不习惯的地⽅。就是对 transform 的位置、⻆度、缩放进⾏赋值。
⽐如,如果仅仅是对 transform.localPosition.x 进⾏赋值。代码要这样写。
var localPosition = transform.localPosition;
localPosition.x = 5.0f;
transform.localPosition = localPosition;
或者这样写
transform.localPosition = new
Vector3(5.0f,transform.localPosition.y,transform.localPosition.z)
原因是因为 Vector3 是 struct 类型的。我们可以把它理解成值类型,在接收 Vector3 对象的时候是值的拷⻉,⽽不是引⽤的赋值。这个是 C# 的⼀点语法细节。
2、封装修改transform.localPosition X Y Z
public static void SetLocalPosX(Transform transform, float x)
{
Vector3 localPos = transform.localPosition;
localPos.x = x;
transform.localPosition = localPos;
}
public static void SetLocalPosY(Transform transform, float y)
{
Vector3 localPos = transform.localPosition;
localPos.y = y;
transform.localPosition = localPos;
}
public static void SetLocalPosZ(Transform transform, float z)
{
Vector3 localPos = transform.localPosition;
localPos.z = z;
transform.localPosition = localPos;
}
调用
var transform = new GameObject("transform").transform;
SetLocalPosX(transform, 5.0f);
3、封装transform.localPosition XY、XZ 和YZ
当然还 XY、XZ 和 YZ 也是同样需要⽀持的。但是这⾥呢,其实可以有两个选择:
-
- 调⽤ SetPositionX、SetPositionY 、SetPositionZ
-
- 逻辑全部实现
第⼀种好处就是代码能够复⽤,但是每次进⾏⼀次调⽤,其实是⼀次值类型的复制操作。所以从性能的⻆度来讲不推荐。
第⼆种的好处就是性能相对更好⼀点,但是代码量会增多。
综合考虑,选择第⼆种。
代码如下
public static void SetLocalPosXY(Transform transform, float x, float y)
{
Vector3 localPos = transform.localPosition;
localPos.x = x;
localPos.y = y;
transform.localPosition = localPos;
}
public static void SetLocalPosXZ(Transform transform, float x, float z)
{
Vector3 localPos = transform.localPosition;
localPos.x = x;
localPos.z = z;
transform.localPosition = localPos;
}
public static void SetLocalPosYZ(Transform transform, float y, float z)
{
Vector3 localPos = transform.localPosition;
localPos.y = y;
localPos.z = z;
transform.localPosition = localPos;
}
4、Transform 重置
我们经常要写这样的逻辑,对⼀个 Transform 的位置、旋转、缩放值进⾏重置。
代码如下:
transform.localPosition = Vector3.zero;
transform.localScale = Vector3.one;
transform.localRotation = Quaternion.identity;
我们也可以进行封装
/// <summary>
/// 重置操作
/// </summary>
public static void Identity(Transform transform)
{
transform.localPosition = Vector3.zero;
transform.localScale = Vector3.one;
transform.localRotation = Quaternion.identity;
}
调用
var transform = new GameObject("transform").transform;
Identity(transform);
五、封装概率函数
输⼊百分⽐返回是否命中概率
/// <summary>
/// 输⼊百分⽐返回是否命中概率
/// </summary>
public static bool Percent(int percent)
{
return Random.Range (0, 100) <= percent;
}
调用
Debug.Log(Percent(50));
输出结果为,有⼀半的概率会输出 true。
六、方法过时
一般修改方法时,为了不影响旧版本的使用,我们会做中转,然后提示过时
[Obsolete("⽅法以过时,请使⽤ Exporter.GenerateUnityPackageName();")]
public static string GenerateUnityPackageName()
{
//调用新方法位置
return Exporter.GenerateUnityPackageName();
}
调用会报警告
代码编辑器可能也会提示
七、partial 关键字,拆开合并类
在C#中,partial关键字用于定义一个类、结构或接口的部分实现。这意味着一个类型的定义可以被拆分到多个文件中。这样做的好处是可以使代码更加组织化和易于管理,尤其是在大型项目中。
例如,使用工具自动生成代码时,可以将自动生成的代码与手动编写的代码分开。多个开发者可以同时在不同的文件中工作,而不会相互冲突。
你可以将一个类的实现分散到多个文件中。例如:
// File1.cs
partial class MyClass
{
public void MethodA()
{
// 方法实现
}
}
// File2.cs
partial class MyClass
{
public void MethodB()
{
// 方法实现
}
}
在编译时,编译器会将这些部分合并为一个完整的类MyClass。
partial关键字不仅可以用于类,还可以用于结构体和接口。例如:
// PartialStruct1.cs
partial struct MyStruct
{
public int Value;
}
// PartialStruct2.cs
partial struct MyStruct
{
public void DisplayValue()
{
Console.WriteLine(Value);
}
}
八、从数组中随机取⼀个数值并进⾏返回
1、实现
实现从 int
类型数组中随机取⼀个数值进⾏返回
public static int GetRandomValueFrom(int[] values)
{
return values[Random.Range(0, values.Length)];
}
这里只是int
类型,如果还有string、float
其他类型怎么办?再复制一份代码改改类型吗,当然可以,但是很麻烦。
public static int GetRandomValueFrom(int[] values)
{
return values[Random.Range(0, values.Length)];
}
public static string GetRandomValueFrom(string[] values)
{
return values[Random.Range(0, values.Length)];
}
public static float GetRandomValueFrom(float[] values)
{
return values[Random.Range(0, values.Length)];
}
2、object 类优化
我们可以使用⾯向对象特性(封装、继承、多态
)中的继承特性
。
那么 int、string、float 有没有共同的⽗类?答案是有的,它们共同继承了 object
类。不⽌是int、string、float
,C# 中的所有类型都继承了 object
类。
改完的代码如下:
public static object GetRandomValueFrom(object[] values)
{
return values[Random.Range(0, values.Length)];
}
调用
var intRandomValue = (int)GetRandomValueFrom(new int[]{1,2,3});
var stringRandomValue = (string)GetRandomValueFrom(new string[]{"asdasd","123123"});
var floatRandomValue = (float)GetRandomValueFrom(new float[]{ 0.1f,0.2f });
调用起来的代码虽然⽐较难看(1.强制类型转换;2.再加上每次传⼊参数都要构造数组;)
使⽤上有⼀点麻烦,不过还好,我们最起码解决了结构重复的问题,⽽且我们还复习了⼀下继承。
这样搞其实还有个问题,就是值类型转引⽤类型会造成效率问题,当然除了使⽤ object
接收,还有更好的⽅法-使⽤泛型
。
3、泛型,结构复⽤利器
泛型对很多初学者来说是⽐较⾼级的概念,这⾥我们顺便复习⼀下泛型。
泛型是什么呢?对于⽅法来说,⽅法结构中的部分或全部类型都可以先不进⾏定义,⽽是到调⽤⽅法的时候再去定义。
前面的代码通过泛型优化
public static T GetRandomValueFrom<T>(T[] values)
{
return values[Random.Range(0, values.Length)];
}
测试调用
var intRandomValue = GetRandomValueFrom(new int[]{1,2,3});
var stringRandomValue = GetRandomValueFrom(new string[]{"asdasd","123123"});
var floatRandomValue = GetRandomValueFrom(new float[]{ 0.1f,0.2f });
从测试调用代码中可以⽐较出来,使⽤泛型之后的代码确实好⽤了很多。
⼤家思考下泛型是不是这样的?结构中的部分或全部类型都可以先不进⾏定义,⽽是到调⽤的时候再去定义。
我们右收获了⼀个法宝泛型。要说⽅法是逻辑上的复⽤,那么泛型就是结构上的复⽤。两⼤复⽤法宝。
4、params 关键字优化
⽬前⽐较麻烦的是数组构造代码了。
这个也是有办法搞定的。我们把⽅法的实现改成如下:
public static T GetRandomValueFrom<T>(params T[] values)
{
return values[Random.Range(0, values.Length)];
}
⼤家注意下,参数前边加上了⼀个 params 关键字。这个是什么意思呢?意思是 GetRandomValueFrom 可以传任意数量的参数。
测试调用代码
var intRandomValue = GetRandomValueFrom(1, 2, 3);
var stringRandomValue = GetRandomValueFrom("asdasd", "123123");
var floatRandomValue = GetRandomValueFrom(0.1f, 0.2f);
是不是清爽了很多?这就是 params 的⽤法。
⽽通过 params 修饰的 values 参数来说,如果传⼊的是⼀个数组,那么 values 本身就是这个数组,如果传⼊的是若⼲个值,那么 values 中就包含了这若⼲个值。
总结⼀句话,就是可以传⼀整个数组,也可以传若⼲个参数,设计得⾮常⼈性化。
九、abstract 关键字定义抽象类和抽象方法
- 使用 abstract 关键字可以创建灵活的类层次结构,允许不同的子类实现特定的行为。
- 抽象类可以包含常规方法和属性,提供共享功能,而抽象方法则强制子类提供具体实现。
1、抽象类
定义:抽象类不能被实例化,通常作为其他类的基类。
用途:可以包含抽象方法(没有实现)和具体方法(有实现),提供子类共享的基本功能。
public abstract class MonoBehaviourXY
{
public abstract void MakeSound(); // 抽象方法,没有实现
public void Sleep() // 具体方法,有实现
{
Debug.Log("The animal is sleeping.");
}
}
如果想通过外部实例化 这个MonoBehaviourXY,调用会报错
2、抽象方法
- 定义:抽象方法必须在抽象类中声明,并且没有方法体。
- 用途:
强制
子类实现这些方法,以便提供特定的功能。
public class Dog : MonoBehaviourXY
{
public override void MakeSound() // 实现抽象方法
{
Debug.Log("Woof!");
}
}
public class Cat : MonoBehaviourXY
{
public override void MakeSound() // 实现抽象方法
{
Debug.Log("Meow!");
}
}
十、封装继承MonoBehaviour的基类
在 Unity 中,我们的脚本都往往继承⾃ MonoBehaviour,继承了之后我们就可以在脚本内编写很多功能。⽐如访问 transform/gameObject,再⽐如控制动画接收碰撞事件等等。另外我们继承了MonoBehaviour 才能被作为脚本挂到 GameObject 上。
仅仅是通过继承,MonoBehaviour 的很多功能都能够进⾏复⽤。所以继承的⼀个作⽤就是代码复⽤。
1、封装重置transform
像前面对transform封装的一些方法的调用,⽬前其实通过继承来实现会更好⼀点。因为这个类中的⽅法全部都是要传⼀个固定的对象进去的,⽐如每个⽅法第⼀个参数都是Transform参数,他们使⽤起来也不是很⽅便。
原本调用我们写的调用重置transform,使⽤代码如下
TransformUtil.Identity(transform);
可以看出都需要把所有的类名字全部打出来,⽽使⽤继承就会好很多。
比如我封装一个MonoBehaviourXY
public abstract partial class MonoBehaviourXY : MonoBehaviour
{
public void Identity()
{
TransformUtil.Identity(transform);
}
}
MonoBehaviourXY 为了可以在之后的示例中进⾏扩展,所以加上了 partial 关键字。
后面的代码我们可以选择不再继承MonoBehaviour而是我们自己写MonoBehaviourXY ,他能包含原来MonoBehaviour的所有功能和自定义方法
调用重置transform,可以看到简单很多
Identity();
ps:前面提到的其他的transform操作也可以进行封装,方法都一样,就留给大家自己操作了
2、封装协程定时功能
public abstract partial class MonoBehaviourXY
{
/// <summary>
/// 定时功能
/// </summary>
/// <param name="seconds">时间秒</param>
/// <param name="onFinished">执行的方法</param>
public void Delay(float seconds, Action onFinished)
{
StartCoroutine(DelayCoroutine(seconds, onFinished));
}
private static IEnumerator DelayCoroutine(float seconds, Action onFinished)
{
yield return new WaitForSeconds(seconds);
onFinished();
}
}
测试调用
public class DelayWithCoroutine : MonoBehaviourXY
{
[UnityEditor.MenuItem("QFramework/MonoBehaviourXY/2.定时功能", false, 2)]
private static void MenuClickd()
{
UnityEditor.EditorApplication.isPlaying = true;
new GameObject("DelayWithCoroutine").AddComponent<DelayWithCoroutine>();
}
private void Start()
{
Delay(5.0f, () =>
{
UnityEditor.EditorApplication.isPlaying = false;
});
}
}
运行效果,调用后会运行unity,然后过5秒后,结束运行
完结
赠人玫瑰,手有余香!如果文章内容对你有所帮助,请不要吝啬你的点赞评论和关注
,你的每一次支持
都是我不断创作的最大动力。当然如果你发现了文章中存在错误
或者有更好的解决方法
,也欢迎评论私信告诉我哦!
好了,我是向宇
,https://xiangyu.blog.****.net
一位在小公司默默奋斗的开发者,闲暇之余,边学习边记录分享,站在巨人的肩膀上,通过学习前辈们的经验总是会给我很多帮助和启发!如果你遇到任何问题,也欢迎你评论私信或者加群找我, 虽然有些问题我也不一定会,但是我会查阅各方资料,争取给出最好的建议,希望可以帮助更多想学编程的人,共勉~