u3d SwipeControl教程
作为一名新手,我刚开始学习Unity3D不久,正在广泛查阅各种资料。除了官方手册,他人的经验分享也十分有益。偶然间看到一篇国外的文章,觉得内容不错,便翻译过来与大家分享。
工具与环境
- 软件:Unity软件
- 硬件:电脑
Unity3D开发技巧与规范
1. 避免Assets分支
所有的Asset都应只有唯一版本。若确实需要Prefab、Scene或Mesh的分支版本,需制定清晰流程以确定正确版本。错误的分支应采用特殊命名,如双下划线前缀,像 __MainScene_Backup。Prefab版本分支需特定流程确保安全。
2. 版本控制下的项目拷贝
若使用版本控制,每个团队成员应保留项目的Second Copy用于测试修改。Second Copy和Clean Copy都要更新和测试,且不要修改Clean Copy,这对测试Asset丢失很有用。
3. 考虑使用外部关卡编辑工具
Unity并非完美的关卡编辑器。例如,可使用TuDee创建3D Tile - Based游戏,这样能获得对Tile友好工具的优势,如网格约束、90度倍数旋转、2D视图、快速Tile选择等,且从XML文件实例化Prefab也很简单。
4. 考虑将关卡保存为XML而非Scene
将关卡保存为XML有诸多优势:
- 无需为每个场景重复设置。
- 加载速度更快(若多数对象在场景间共享)。
- 场景版本合并更简单(即便Unity新的文本格式Scene,因数据过多,版本合并仍不实际)。
- 关卡间数据保持更简便。
仍可使用Unity作为关卡编辑器,不过需编写数据的序列化和反序列化代码,实现编辑器和游戏运行时加载关卡、编辑器中保存关卡,可能还需模仿Unity的ID系统维护对象间引用关系。
5. 考虑编写通用的自定义Inspector代码
实现自定义Inspector虽直接,但Unity系统存在缺点:
- 不支持从继承中获益。
- 只能定义class类型级别的Inspector组件,不能定义字段级别的。例如,若每个游戏对象都有
ScomeCoolType字段,想在Inspector中不同渲染,需为所有class编写Inspector代码。
可通过重新实现Inspector系统解决这些问题,借助反射机制,实现并不复杂,文章底部(日后另作翻译)将提供更多细节。
6. 使用命名的空Game Object做场景目录
仔细组织场景,便于查找对象。
7. 控制对象和场景目录放置在原点
若对象位置不重要,将其放于原点 (0, 0, 0),可避免处理Local Space和World Space的麻烦,使代码更简洁。
8. 尽量减少使用GUI组件的offset
通常由控件的Layout父对象控制Offset,不应依赖爷爷节点位置,避免位移相互抵消以达正确显示目的。例如,父容器放于 (100, -50),子节点应在 (10, 10),不应通过 (90, 60)(父节点相对位置)放置。这种错误常见于容器不可见时。
9. 世界地面置于Y = 0
将世界地面放在 Y = 0 可方便将对象置于地面,且在合适情况下,游戏逻辑可将世界作为2D空间处理,如AI和物理模拟。
10. 使游戏可从每个Scene启动
这能大幅降低测试时间。为使所有场景可运行,需做两件事:
- 若需前面场景运行产生的数据,进行模拟。
- 生成场景切换时必要保存的对象,示例代码如下:
myObject = FindMyObjectInScene(); if (myObject == null) { myObject = SpawnMyObject(); }
11. 角色和地面物体中心点放底部
将角色和地面物体的中心点(Pivot)放底部,便于精确放置到地板上,合适时可使游戏逻辑、AI甚至物理用2D逻辑表现3D。
12. 统一模型面朝向
所有有面朝向的对象(如角色)应统一面朝向(Z轴正向或反向),可简化很多算法。
13. 开始时确定正确的Scale
请美术将所有导入的缩放系数设为1,Transform的Scale设为 (1, 1, 1),可使用Unity的Cube作为参考对象进行缩放比较。为游戏选择世界单位系数并坚持使用。
14. 为GUI组件或手动创建的粒子制作双面平面模型
设置平面面朝向Z轴正向,可简化Billboard和GUI创建。
15. 制作并使用测试资源
- 为SkyBox创建带文字的方形贴图。
- 准备一个网格(Grid)。
- 为Shader测试使用各种颜色平面:白色、黑色、50%灰度、红、绿、蓝、紫、黄、青。
- 为Shader测试使用渐进色:黑到白、红到绿、红到蓝、绿到蓝。
- 准备黑白格子。
- 准备平滑或粗糙的法线贴图。
- 准备一套用于快速搭建场景的灯光(使用Prefab)。
16. 所有东西使用Prefab
除场景中的“目录”对象,其他对象(包括仅使用一次的唯一对象)都应使用Prefab,便于在不改动场景的情况下修改。使用EZGUI时,还可创建稳定的Sprite Atlases。
17. 特例使用单独的Prefab
若有不同属性的敌人类型,为不同属性分别创建Prefab并链接,可在同一地方修改所有类型,且不改动场景。若敌人类型多,可程序化处理或使用核心文件/Prefab,通过下拉列表创建不同敌人或根据敌人位置、玩家进度计算。
18. Prefab之间链接
Prefab放置到场景中时,链接关系会被维护,而实例的链接关系不会。尽量使用Prefab之间的链接,可减少场景创建操作和修改。
19. 自动产生实例对象间链接关系
若需在实例间链接,应在程序代码中创建。例如,Player 对象在 Start 时向 GameManager 注册,或 GameManager 在 Start 时查找 Player 对象。
制作Prefab时,不要用Mesh作为根节点,先创建空的GameObject作为父对象和根节点,将脚本放于根节点,替换Mesh时可避免丢失Inspector中设置的值。使用互相链接的Prefab实现Prefab嵌套,Unity不支持Prefab嵌套,团队合作中第三方实现方案可能有风险,因嵌套的Prefab关系不明确。
20. 使用安全流程处理Prefab分支
以 Player Prefab为例,修改流程如下:
- 复制
PlayerPrefab。 - 将复制的Prefab重命名为
__Player_Backup。 - 修改
PlayerPrefab。 - 测试正常后,删除
__Player_Backup。
避免将新复制的命名为 Player_New 再修改。
若修改复杂,涉及多人且耗时短,仍可使用上述流程;若耗时久,可采用以下流程:
- 第一个人:
- 复制
PlayerPrefab。 - 重命名为
__Player_WithNewFeature或__Player_ForPerson2。 - 在复制对象上修改并提交给第二个人。
- 第二个人:
- 在新Prefab上修改。
- 复制
PlayerPrefab并命名为__Player_Backup。 - 将
__Player_WithNewFeature拖到场景创建实例。 - 将实例拖到原始
PlayerPrefab中。 - 若正常,删除
__Player_Backup和__Player_WithNewFeature。
21. 扩展自定义MonoBehaviour基类
扩展自己的 MonoBehaviour 基类,让所有组件从中派生,便于实现通用函数,如类型安全的 Invoke 或更复杂的调用。
22. 定义安全调用方法
为 Invoke、StartCoroutine 和 Instantiate 定义安全调用方法,使用委托任务(delegate Task)定义调用方法,示例代码如下:
public void Invoke(Task task, float time) {
Invoke(task.Method.Name, time);
}
23. 为共享接口的组件扩展
将获得组件、查找对象实现在组件接口中,可使用 typeof 而非泛型函数,示例代码如下:
// Defined in the common base class for all mono behaviours
public I GetInterfaceComponent<I>() where I : class {
return GetComponent(typeof(I)) as I;
}
public static List<I> FindObjectsOfInterface<I>() where I : class {
MonoBehaviour[] monoBehaviours = FindObjectsOfType<MonoBehaviour>();
List<I> list = new List<I>();
foreach(MonoBehaviour behaviour in monoBehaviours) {
I component = behaviour.GetComponent(typeof(I)) as I;
if(component != null) {
list.Add(component);
}
}
return list;
}
24. 使用扩展让代码书写更便捷
示例代码如下:
public static class CSTransform {
public static void SetX(this Transform transform, float x) {
Vector3 newPosition = new Vector3(x, transform.position.y, transform.position.z);
transform.position = newPosition;
}
// ...
}
25. 使用防御性的GetComponent()
强制性组件依赖(通过 RequiredComponent)可能不便,可使用替代方案,未找到必要组件时输出错误信息,示例代码如下:
public static T GetSafeComponent<T>(this GameObject obj) where T : MonoBehaviour {
T component = obj.GetComponent<T>();
if(component == null) {
Debug.LogError("Expected to find component of type " + typeof(T) + " but found none", obj);
}
return component;
}
26. 避免不同处理风格
项目中对于同一件事,若有多种惯用手法,应明确选择一种。原因如下:
- 不同做法协作性可能不佳,统一风格可明确设计方向。
- 团队成员使用统一风格,便于相互理解,减少错误。
常见风格选择示例:
- 协程与状态机(Coroutines vs. state machines)。
- 嵌套的Prefab、互相链接的Prefab、超级Prefab(Nested prefabs vs. linked prefabs vs. God prefabs)。
- 数据分离策略。
- 2D游戏使用Sprite的方法。
- Prefab的结构。
- 对象生成策略。
- 定位对象的方法:使用类型、名称、层、引用关系。
- 对象分组的方法:使用类型、名称、层、引用数组。
- 找到一组对象,还是让它们自己来注册。
- 控制执行次序(使用Unity的执行次序设置,还是使用Awake/Start/Update/LateUpdate,还是使用纯手动的方法,或者是次序无关的架构)。
- 在游戏中使用鼠标选择对象/位置/目标:SelectionManager或者是对象自主管理。
- 在场景变换时保存数据:通过PlayerPrefs,或者是在新场景加载时不要销毁的对象。
- 组合动画的方法:混合、叠加、分层。
27. 维护自己的Time类
维护自己的 Time 类,包装 Time.DeltaTime 和 Time.TimeSinceLevelLoad,实现暂停和游戏速度缩放,虽使用稍麻烦,但对象运行在不同时钟速率下时更方便,如界面动画和游戏内动画。
28. 避免运行时生成对象打乱场景层次结构
游戏运行时,为动态生成的对象设置父对象,便于查找,可使用空对象或无行为的单件简化代码访问,可命名为 DynamicObjects。
29. 使用单件(Singleton)模式
从以下类派生的类可自动获得单件功能:
public class Singleton<T> : MonoBehaviour where T : MonoBehaviour {
protected static T instance;
/**
* Returns the instance of this singleton.
*/
public static T Instance {
get {
if(instance == null) {
instance = (T) FindObjectOfType(typeof(T));
if (instance == null) {
Debug.LogError("An instance of " + typeof(T) + " is needed in the scene, but there is none.");
}
}
return instance;
}
}
}
单件可作为管理器,如 ParticleManager、AudioManager 或 GUIManager。非唯一的prefab实例可使用单件管理器,避免为遵循原则使类层次关系复杂,可在 GameManager 或其他合适管理器中持有引用。对于外部常用的公共变量和方法定义为 static,如 GameManager.Player 替代 GameManager.Instance.player。
30. 组件中谨慎使用public成员变量
除非变量需在Inspector中调节,特别是含义不明确的变量,不要声明为 public。特殊情况下无法避免时,可使用两个或四个下划线表明不要从外部调节,如 public float __aVariable;。
31. 界面和游戏逻辑分离
本质上是MVC模式:
- 输入控制器:只负责向相应组件发送命令,不改变玩家状态。例如,玩家对象根据自身状态设置速度和移动方式,控制器仅做与自身状态相关的事。切换武器时,玩家提供
SwitchWeapon(Weapon newWeapon)函数供GUI调用,GUI不维护对象的Transform和父子关系。 - 界面组件:只负责维护和处理自身状态相关数据,显示游戏状态数据,这些数据应在其他地方维护,如地图数据可在
GameManager中维护。 - 游戏玩法对象:不关心GUI,除处理游戏暂停(通过控制
Time.timeScale并非好主意)外,只需知道游戏是否暂停。
32. 分离状态控制和簿记变量
簿记变量为使用方便或提高查找速度设置,可根据状态控制覆盖。分离两者可简化保存和调试游戏状态,可通过为每个游戏逻辑定义 SaveData 类实现,示例代码如下:
[Serializable]
public class PlayerSaveData {
public float health; // public for serialisation, not exposed in inspector
}
public class Player : MonoBehaviour {
// ... bookkeeping variables
// Don't expose state in inspector. State is not tweakable.
private PlayerSaveData playerSaveData;
}
33. 分离特殊的配置
若有使用同一Mesh但属性不同的敌人,可通过以下方式分离数据:
- 为每个游戏逻辑类定义模板类,如
EnemyTemplate保存敌人属性设置变量。 - 在游戏逻辑类中定义模板类型变量。
- 制作敌人Prefab和模板Prefab,如
WeakEnemyTemplate和StrongEnemyTemplate。 - 加载或生成对象时,正确复制模板变量。
可使用泛型定义类,示例代码如下:
public class BaseTemplate {
// ...
}
public class ActorTemplate : BaseTemplate {
// ...
}
public class Entity<EntityTemplateType> where EntityTemplateType : BaseTemplate {
EntityTemplateType template;
// ...
}
public class Actor : Entity <ActorTemplate> {
// ...
}
34. 避免使用字符串
除显示用文本外,避免使用字符串作为对象或prefab等的ID标识,动画系统除外,其需用字符串访问动画。
35. 避免使用public的数组
定义多个数组会使代码在Inspector中设置值时易出错,可定义类封装变量,使用其实例数组,示例代码如下:
[Serializable]
public class Weapon {
public GameObject prefab;
public ParticleSystem particles;
public Bullet bullet;
}
36. 结构中避免使用数组
玩家有多种攻击形式时,使用数组在Inspector中设置不便,可使用单独变量并起有意义的名称,示例代码如下:
[Serializable]
public class Bullets {
public Bullet FireBullet;
public Bullet IceBullet;
public Bullet WindBullet;
}
37. 数据组织到可序列化的类中
对象有大量可调节变量时,将变量分组到不同可序列化类中,在主要类中定义这些类的实例为公共成员变量,可使Inspector更整洁,示例代码如下:
[Serializable]
public class MovementProperties // Not a MonoBehaviour! {
public float movementSpeed;
public float turnSpeed = 1; // default provided
}
public class HealthProperties // Not a MonoBehaviour! {
public float maxHealth;
public float regenerationRate;
}
public class Player : MonoBehaviour {
public MovementProperties movementProperties;
public HealthProperties healthProperties;
}
38. 剧情文本处理
若有大量剧情文本,将其放于文件中,不在Inspector字段中编辑,以便不打开Unity、不保存Scene就能修改。
39. 字符串本地化
若计划实现本地化,将字符串分离到统一位置。可定义文本Class,为每个字符串定义公共字符串字段,默认值设为英文,其他语言定义为子类并重新初始化字段。也可读取单独表单,根据所选语言选取正确字符串,适用于文本量大或支持语言多的情况。
40. 实现图形化的Log
用于调试物理、动画和AI,可加速调试工作,详见[此处](待补充链接)。
41. 实现HTML的Log
日志在很多情况下有用,便于分析的Log(颜色编码、多视图、记录屏幕截图等)可使基于Log的调试更愉悦,详见[此处](待补充链接)。
42. 实现自己的帧速率计算器
Unity的FPS计算器可能不准确,实现自己的计算器,使数字符合直觉并可视化。
43. 实现截屏快捷键
很多BUG是图形化的,有截图便于报告。理想系统应在 PlayerPrefes 中保存计数,使截屏文件不被覆盖,且保存于工程文件夹外,防止提交到版本库。
44. 实现打印玩家坐标快捷键
汇报位置相关BUG时,可明确位置,便于Debug。
45. 实现Debug选项
方便测试,如解锁所有道具、关闭所有敌人、关闭GUI、让玩家无敌、关闭所有游戏逻辑等。
46. 创建适合团队的Debug选项Prefab
设置用户标识文件,不提交到版本库,游戏运行时读取,避免团队成员意外提交Debug设置影响他人,修改Debug设置无需修改场景。
47. 维护包含所有游戏元素的场景
包含所有敌人、可交互对象等,便于全面功能测试。
48. 定义Debug快捷键常量
将其保存在统一地方,避免快捷键冲突,可在一个地方处理所有按键输入。
49. 为设置建立文档
代码应有详细文档,代码外的设置也需文档记录,如Layer的使用、Tag的使用、GUI的depth层级、惯用处理方式、Prefab结构、动画Layer等,可提高效率。
50. 遵从命名规范和目录结构并建立文档
命名和目录结构一致,便于查找。命名规则示例:
- 名字代表对象,如
Bird。 - 选择可发音、易记忆的名字,避免生僻命名。
- 保持唯一性。
- 使用Pascal风格大小写,如
ComplicatedVerySpecificObject。 - 除特殊情况,不使用空格、下划线或连字符。
- 不使用版本数字或进度标示词(WIP、final)。
- 不使用缩写,如
DarkVampire@Walk而非DVamp@W。 - 使用设计文档中的术语,如
DarkVampire@Die而非DarkVampire@Death。 - 细节修饰词放左侧,如
DarkVampire而非VampireDark。 - 序列使用同一名字加数字,从0开始,如
PathNode0, PathNode1。 - 非序列情况不使用数字,如
Flamingo, Eagle, Swallow而非Bird0, Bird1, Bird2。 - 临时对象添加双下划线前缀,如
__Player_Backup。 - 同一事物不同方面命名,在核心名称后加下划线,如
EnterButton_Active、DarkVampire_Diffuse、JungleSky_Top、DarkVampire_LOD0。
以上就是Unity3D开发中的一些实用技巧和规范,希望对大家有所帮助。