u3d SwipeControl教程

2015年03月09日 11:22 0 点赞 0 评论 更新于 2025-11-21 16:47

作为一名新手,我刚开始学习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 注册,或 GameManagerStart 时查找 Player 对象。

制作Prefab时,不要用Mesh作为根节点,先创建空的GameObject作为父对象和根节点,将脚本放于根节点,替换Mesh时可避免丢失Inspector中设置的值。使用互相链接的Prefab实现Prefab嵌套,Unity不支持Prefab嵌套,团队合作中第三方实现方案可能有风险,因嵌套的Prefab关系不明确。

20. 使用安全流程处理Prefab分支

Player Prefab为例,修改流程如下:

  • 复制 Player Prefab。
  • 将复制的Prefab重命名为 __Player_Backup
  • 修改 Player Prefab。
  • 测试正常后,删除 __Player_Backup

避免将新复制的命名为 Player_New 再修改。

若修改复杂,涉及多人且耗时短,仍可使用上述流程;若耗时久,可采用以下流程:

  • 第一个人:
  • 复制 Player Prefab。
  • 重命名为 __Player_WithNewFeature__Player_ForPerson2
  • 在复制对象上修改并提交给第二个人。
  • 第二个人:
  • 在新Prefab上修改。
  • 复制 Player Prefab并命名为 __Player_Backup
  • __Player_WithNewFeature 拖到场景创建实例。
  • 将实例拖到原始 Player Prefab中。
  • 若正常,删除 __Player_Backup__Player_WithNewFeature

21. 扩展自定义MonoBehaviour基类

扩展自己的 MonoBehaviour 基类,让所有组件从中派生,便于实现通用函数,如类型安全的 Invoke 或更复杂的调用。

22. 定义安全调用方法

InvokeStartCoroutineInstantiate 定义安全调用方法,使用委托任务(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.DeltaTimeTime.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;
}
}
}

单件可作为管理器,如 ParticleManagerAudioManagerGUIManager。非唯一的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,如 WeakEnemyTemplateStrongEnemyTemplate
  • 加载或生成对象时,正确复制模板变量。

可使用泛型定义类,示例代码如下:

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_ActiveDarkVampire_DiffuseJungleSky_TopDarkVampire_LOD0

以上就是Unity3D开发中的一些实用技巧和规范,希望对大家有所帮助。

作者信息

boke

boke

共发布了 3994 篇文章